/* Arguments * * 0: title (string) - The title to be displayed in the titlebox * 1: initialValue (number, optional) - The initial value to display when the stepper is rendered. It will be clamped * between minValue and maxValue. If not provided, options will be the 2nd parameter. * 2: setter (string, optional) - Determines how the current value should be set: * - If a string is provided (e.g., "purity"), it is treated as a reference to the state variable ($purity). * 3: options (object, optional) - An object containing additional optional settings: * - max (number, optional): The max allowed value. Defaults to 100. * - min (number, optional): The min allowed value. Defaults to 0. * - step (number, optional): Defaults to 1% of the range. The amount by which the value increases or decreases with the single arrow buttons. * The double arrow buttons increase or decrease the value by x10. * - percentage (boolean, optional): Defaults to true. If false, the number will not be converted to a percentage. * - colorArr (Array, optional): Defaults to null. Optional array of custom colours instead of the default range. * - reverse (boolean, optional): Defaults to false. If true, reverses the colour range. * - valueFormat (function, optional): Defaults to null. Override callback for formatting the displayed value. * (e.g., (value, percentage) => { return "$" + value }). * - tooltip (object, optional) => Defaults to null. Must be a tooltip object. * Example: tooltip: { message: () => V.physique, delay: 200 } * - values (Array, optional): Captures an array of any values that can be used in the above functions. * - reverseButtons (boolean, optional): Defaults to false. If true, reverses the function of the buttons. * - activeButtons (Array, optional): Defaults to ["single", "double", "minMax"]. Specify which buttons should be active. * - css (string, optional): Defaults to null. If provided, overrides the default container css. * - callback (function, optional): A callback for whenever the value changes. */ Macro.add("numberStepper", { handler() { DOL.Perflog.logWidgetStart("numberStepper"); // Determine arguments and options let [title, initialValue, setter, options] = this.args; // Adjust parameters if initialValue is not provided if (typeof initialValue === "object" && initialValue !== null) { options = initialValue; initialValue = undefined; setter = undefined; } else if (typeof setter === "object" && setter !== null) { options = setter; setter = undefined; } // Set defaults if options were not provided options = options || {}; // Check for missing or invalid mandatory parameters if (typeof title !== "string" || !title.trim()) { $(this.output).append($("<div>", { class: "red", text: "Error: Title is missing or invalid." })); return; } if (initialValue !== undefined && isNaN(initialValue)) { $(this.output).append($("<div>", { class: "red", text: "Error: Initial value is not a valid number." })); return; } if (setter !== undefined && typeof setter !== "string") { $(this.output).append($("<div>", { class: "red", text: "Error: Setter must be a valid string." })); return; } // Default values for options const { min = 0, max = 100, step = (max - min) / 100, percentage = true, reverse = false, colorArr = null, valueFormat = null, tooltip = null, values = null, reverseButtons = false, activeButtons = ["single", "double", "minMax"], css = {}, callback = null, } = options; // Validations if (isNaN(min) || isNaN(max)) { $(this.output).append($("<div>", { class: "red", text: "Error: Min or Max value is not a valid number." })); return; } if (min > max) { console.log("min", min, "max", max); $(this.output).append($("<div>", { class: "red", text: "Error: Min value must be less than max value." })); return; } if (isNaN(step) || step <= 0) { $(this.output).append($("<div>", { class: "red", text: "Error: Step must be a positive number." })); return; } if (typeof reverse !== "boolean") { $(this.output).append($("<div>", { class: "red", text: "Error: reverse must be a boolean." })); return; } if (colorArr !== null && (!Array.isArray(colorArr) || !colorArr.every(color => typeof color === "string"))) { $(this.output).append($("<div>", { class: "red", text: "Error: colorArr must be an array of strings." })); return; } if (valueFormat !== null && typeof valueFormat !== "function") { $(this.output).append($("<div>", { class: "red", text: "Error: valueFormat must be a function." })); return; } if (tooltip !== null && typeof tooltip !== "object") { $(this.output).append($("<div>", { class: "red", text: "Error: tooltip must be an object." })); return; } if (callback !== null && typeof callback !== "function") { $(this.output).append($("<div>", { class: "red", text: "Error: callback must be a function." })); return; } let currentValue = initialValue !== undefined ? Math.min(Math.max(initialValue, min), max) : min; const convertToPercentage = value => { // Ensures it doesn't return the 0/100 when its not the min or max value if (value === min) return 0; if (value === max) return 100; return min === max ? 0 : Math.clamp(((value - min) / (max - min)) * 100, 1, 99); }; window.convertToPercentage = convertToPercentage; // Actual color hex is cached to prevent multiple calls per passage const getColor = factor => { let colors; if (colorArr) { colors = colorArr; } else { if (!T.stepperColorArray) { const colorVars = ["--red", "--pink", "--purple", "--blue", "--blue-secondary", "--teal", "--green"]; T.stepperColorArray = colorVars; } colors = T.stepperColorArray; } if (reverse) { colors = [...colors].reverse(); } if (min === max) { return colors[0]; } return ColourUtils.interpolateMultiple(colors, factor); }; // Update the displayed value with percentage if enabled const updateDisplay = () => { let displayValue; if (valueFormat) { displayValue = valueFormat(currentValue, convertToPercentage(currentValue), values); } else if (percentage) { displayValue = `<b>${Math.round(convertToPercentage(currentValue))}%</b>`; } else { displayValue = `<b>${Math.round(currentValue)}</b>`; } const color = getColor(convertToPercentage(currentValue) / 100); numberText.html(displayValue); numberText.css("color", color); }; // Handles setting the variable to the value after a button click const setValue = value => { currentValue = Math.min(Math.max(value, min), max); updateDisplay(); if (setter) { V[setter] = currentValue; } if (callback) { callback(currentValue, values); } updateButtonStates(); }; // Disabling / enabling of buttons const updateButtonStates = () => { let disableLeft, disableRight; if (percentage) { const percentageValue = Math.round(convertToPercentage(currentValue)); disableLeft = percentageValue <= Math.round(convertToPercentage(min)); disableRight = percentageValue >= Math.round(convertToPercentage(max)); } else { disableLeft = currentValue <= min; disableRight = currentValue >= max; } if (reverseButtons) { [disableLeft, disableRight] = [disableRight, disableLeft]; } const conditions = [ { buttons: [singleLeft, doubleLeft, extremeLeft], disabled: disableLeft, }, { buttons: [singleRight, doubleRight, extremeRight], disabled: disableRight, }, ]; conditions.forEach(({ buttons, disabled }) => { buttons.forEach(button => { button.prop("disabled", disabled); }); }); titleText.fontResizer({ margin: 18 }); }; const createButton = (classes, iconClasses, clickHandler) => { const button = $("<button>", { class: classes }); iconClasses.forEach(iconClass => button.append($("<span>", { class: iconClass }))); if (clickHandler) { button.on("click", clickHandler); } return button; }; // Cache static elements (buttons, etc.) if not cached already if (!T.stepperStaticElements) { T.stepperStaticElements = {}; // Left buttons T.stepperStaticElements.goToMin = createButton("numberStepper doubleIcon", ["fa-icon fa-bar", "fa-icon fa-left"]); T.stepperStaticElements.tripleLeft = createButton("numberStepper doubleIcon", ["fa-icon fa-left", "fa-icon fa-left", "fa-icon fa-left"]); T.stepperStaticElements.doubleLeft = createButton("numberStepper doubleIcon hideable", ["fa-icon fa-left", "fa-icon fa-left"]); T.stepperStaticElements.singleLeft = createButton("numberStepper", ["fa-icon fa-left"]); // Right buttons T.stepperStaticElements.singleRight = createButton("numberStepper", ["fa-icon fa-right"]); T.stepperStaticElements.doubleRight = createButton("numberStepper doubleIcon hideable", ["fa-icon fa-right", "fa-icon fa-right"]); T.stepperStaticElements.tripleRight = createButton("numberStepper doubleIcon", ["fa-icon fa-right", "fa-icon fa-right", "fa-icon fa-right"]); T.stepperStaticElements.goToMax = createButton("numberStepper doubleIcon", ["fa-icon fa-right", "fa-icon fa-bar"]); } // Clone static elements const extremeLeft = activeButtons.includes("triple") ? T.stepperStaticElements.tripleLeft.clone() : T.stepperStaticElements.goToMin.clone(); const doubleLeft = T.stepperStaticElements.doubleLeft.clone(); const singleLeft = T.stepperStaticElements.singleLeft.clone(); const singleRight = T.stepperStaticElements.singleRight.clone(); const doubleRight = T.stepperStaticElements.doubleRight.clone(); const extremeRight = activeButtons.includes("triple") ? T.stepperStaticElements.tripleRight.clone() : T.stepperStaticElements.goToMax.clone(); // Draw the elements const container = $("<div>", { class: "numberStepperContainer" }).appendTo(this.output); if (css && typeof css === "object") { container.css(css); } // Correctly append the title const titleContainer = $("<div>", { class: "titleRow" }).appendTo(container); $(Wikifier.wikifyEval(title)).appendTo(titleContainer); // Steps are separated in case buttons are reversed const increment = reverseButtons ? step : -step; const increment10 = reverseButtons ? step * 10 : -step * 10; const increment100 = reverseButtons ? step * 100 : -step * 100; const activeCount = activeButtons.length; const groupClass = activeCount === 3 ? "three-buttons" : activeCount === 2 ? "two-buttons" : "one-button"; const group = $("<div>", { class: `numberStepperGroup ${groupClass}` }).appendTo(container); // Left buttons if (activeButtons.includes("minMax")) { extremeLeft.on("click", () => setValue(reverseButtons ? max : min)).appendTo(group); } if (activeButtons.includes("triple")) { extremeLeft.on("click", () => setValue(currentValue + increment100)).appendTo(group); } if (activeButtons.includes("double")) { doubleLeft.on("click", () => setValue(currentValue + increment10)).appendTo(group); } if (activeButtons.includes("single")) { singleLeft.on("click", () => setValue(currentValue + increment)).appendTo(group); } // Title const numberText = $("<span>", { class: "red percentage" }); const divider = $("<span>", { class: "numberStepperDivider" }).append(Wikifier.wikifyEval(title)).append(": "); const titleText = $("<div>", { class: "titlePercentage" }).append(divider).append(numberText).appendTo(group); // Tooltip if (tooltip) { titleText.tooltip(tooltip); } // Right buttons if (activeButtons.includes("single")) { singleRight.on("click", () => setValue(currentValue - increment)).appendTo(group); } if (activeButtons.includes("double")) { doubleRight.on("click", () => setValue(currentValue - increment10)).appendTo(group); } if (activeButtons.includes("triple")) { extremeRight.on("click", () => setValue(currentValue - increment100)).appendTo(group); } if (activeButtons.includes("minMax")) { extremeRight.on("click", () => setValue(reverseButtons ? min : max)).appendTo(group); } updateDisplay(); updateButtonStates(); DOL.Perflog.logWidgetEnd("numberStepper"); }, });