Skip to content
Snippets Groups Projects
numberStepper.js 12 KiB
Newer Older
  • Learn to ignore specific revisions
  • xao's avatar
    xao committed
    /* Arguments
     *
     * 0: title (string) - The title to be displayed in the titlebox
    
    xao's avatar
    xao committed
     * 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:
    
    xao's avatar
    xao committed
     *    - 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 }
    
    xao's avatar
    xao committed
     *	  - values (Array, optional): Captures an array of any values that can be used in the above functions.
    
    xao's avatar
    xao committed
     *    - 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.
    
    xao's avatar
    xao committed
     *    - css (string, optional): Defaults to null. If provided, overrides the default container css.
     *    - callback (function, optional): A callback for whenever the value changes.
    
    xao's avatar
    xao committed
     */
    
    xao's avatar
    xao committed
    Macro.add("numberStepper", {
    
    xao's avatar
    xao committed
    	handler() {
    		DOL.Perflog.logWidgetStart("numberStepper");
    
    
    xao's avatar
    xao committed
    		// Determine arguments and options
    		let [title, initialValue, setter, options] = this.args;
    
    		// Adjust parameters if initialValue is not provided
    
    xao's avatar
    xao committed
    		if (typeof initialValue === "object" && initialValue !== null) {
    
    xao's avatar
    xao committed
    			options = initialValue;
    			initialValue = undefined;
    			setter = undefined;
    
    xao's avatar
    xao committed
    		} else if (typeof setter === "object" && setter !== null) {
    
    xao's avatar
    xao committed
    			options = setter;
    			setter = undefined;
    		}
    
    		// Set defaults if options were not provided
    		options = options || {};
    
    		// Check for missing or invalid mandatory parameters
    
    xao's avatar
    xao committed
    		if (typeof title !== "string" || !title.trim()) {
    			$(this.output).append($("<div>", { class: "red", text: "Error: Title is missing or invalid." }));
    			return;
    		}
    
    xao's avatar
    xao committed
    		if (initialValue !== undefined && isNaN(initialValue)) {
    
    xao's avatar
    xao committed
    			$(this.output).append($("<div>", { class: "red", text: "Error: Initial value is not a valid number." }));
    			return;
    		}
    
    xao's avatar
    xao committed
    		if (setter !== undefined && typeof setter !== "string") {
    			$(this.output).append($("<div>", { class: "red", text: "Error: Setter must be a valid string." }));
    
    xao's avatar
    xao committed
    			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"],
    
    xao's avatar
    xao committed
    			css = {},
    			callback = null,
    		} = options;
    
    xao's avatar
    xao committed
    
    		// Validations
    		if (isNaN(min) || isNaN(max)) {
    			$(this.output).append($("<div>", { class: "red", text: "Error: Min or Max value is not a valid number." }));
    			return;
    		}
    
    xao's avatar
    xao committed
    		if (min > max) {
    			console.log("min", min, "max", max);
    
    xao's avatar
    xao committed
    			$(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;
    		}
    
    xao's avatar
    xao committed
    		if (callback !== null && typeof callback !== "function") {
    			$(this.output).append($("<div>", { class: "red", text: "Error: callback must be a function." }));
    			return;
    		}
    
    xao's avatar
    xao committed
    
    
    xao's avatar
    xao committed
    		let currentValue = initialValue !== undefined ? Math.min(Math.max(initialValue, min), max) : min;
    
    xao's avatar
    xao committed
    
    
    LollipopScythe's avatar
    LollipopScythe committed
    		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;
    
    xao's avatar
    xao committed
    		// 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();
    			}
    
    
    xao's avatar
    xao committed
    			if (min === max) {
    				return colors[0];
    			}
    
    
    xao's avatar
    xao committed
    			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();
    
    xao's avatar
    xao committed
    			if (setter) {
    
    xao's avatar
    xao committed
    				V[setter] = currentValue;
    
    xao's avatar
    xao committed
    			}
    			if (callback) {
    				callback(currentValue, values);
    
    xao's avatar
    xao committed
    			}
    			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);
    				});
    			});
    
    Kirsty's avatar
    Kirsty committed
    			titleText.fontResizer({ margin: 18 });
    
    xao's avatar
    xao committed
    		};
    
    		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);
    
    xao's avatar
    xao committed
    		if (css && typeof css === "object") {
    			container.css(css);
    		}
    
    xao's avatar
    xao committed
    
    		// 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
    
    xao's avatar
    xao committed
    		if (activeButtons.includes("minMax")) {
    
    xao's avatar
    xao committed
    			extremeLeft.on("click", () => setValue(reverseButtons ? max : min)).appendTo(group);
    
    xao's avatar
    xao committed
    		}
    		if (activeButtons.includes("triple")) {
    
    xao's avatar
    xao committed
    			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
    
    Kirsty's avatar
    Kirsty committed
    		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);
    
    xao's avatar
    xao committed
    
    		// 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);
    		}
    
    xao's avatar
    xao committed
    		if (activeButtons.includes("triple")) {
    
    xao's avatar
    xao committed
    			extremeRight.on("click", () => setValue(currentValue - increment100)).appendTo(group);
    		}
    
    xao's avatar
    xao committed
    		if (activeButtons.includes("minMax")) {
    
    xao's avatar
    xao committed
    			extremeRight.on("click", () => setValue(reverseButtons ? min : max)).appendTo(group);
    
    xao's avatar
    xao committed
    		}
    
    xao's avatar
    xao committed
    
    		updateDisplay();
    		updateButtonStates();
    		DOL.Perflog.logWidgetEnd("numberStepper");
    	},
    });