UNPKG

plotboilerplate

Version:

A simple javascript plotting boilerplate for 2d stuff.

339 lines (333 loc) 15.4 kB
/** * A simple class for rendering color gradient dropdowns. * * As native dropdown do not support custom stylings in the way we need, we will implement * our own dropdown class. * * @author Ikaros Kappler * @date 2025-08-19 * @version 1.0.0 */ import * as NoReact from "noreact"; import { Color } from "../../datastructures/Color"; import { ColorGradient } from "../../datastructures/ColorGradient"; export class ColorGradientSelector { /** * The constructor: creates a new color gradient picker in the given container. * If no container or ID is given then a new unbound `container` will be created (DIV). * * Pass a container ID or nothing – in the latter case the constructor will create * a new DIV element. * * @param {string?} containerID - (optional) If you want to use an existing container (should be a DIV). */ constructor(options) { this.isDropdownOpen = false; this.css_buttonWidth = "100px"; this.css_buttonHeight = "1.8em"; this.css_buttonFontSize = "0.725em"; this.css_buttonFontSize_larger = "1.0em"; this.colorGradients = []; // TODO: this does not really need to be a pair, does it? this.colorGradientOptionRefs = []; this.selectedGradientIndex = -1; this.installedChangeListeners = []; /** * Adds custom styles (global STYLE tag). * * @private */ this.__createCustomStylesElement = () => { // Thanks to Ana Tudor // https://css-tricks.com/multi-thumb-sliders-particular-two-thumb-case/ return (NoReact.createElement("style", null, ` #${this.elementID} button { border: 0px solid lightgray; padding: 0.25em; /* background-color: rgba(255,255,255,0.9); */ } #${this.elementID} .positioning-container button { border-radius: 0; } #${this.elementID} .main-button:hover { background-color: rgba(216,216,216,0.9); } #${this.elementID} .option-gradient-button:hover, #${this.elementID} .option-delete-button:hover { background-color: rgba(216,216,216,0.9); } `)); }; options = options || {}; if (options.containerID) { const cont = document.getElementById(options.containerID); if (!cont) { throw "Cannot create ColorGradientPicker. Component ID does not exist."; } this.container = cont; } else { this.container = document.createElement("div"); } this.isMobileMode = Boolean(options.isMobileMode); if (this.isMobileMode) { this.css_buttonWidth = "200px"; this.css_buttonHeight = "2.5em"; this.css_buttonFontSize = "1.25em"; this.css_buttonFontSize_larger = "1.45em"; } this.baseID = Math.floor(Math.random() * 65535); this.elementID = `color-gradient-selector-${this.baseID}`; this.colorGradients = typeof options.initialGradients === "undefined" ? ColorGradientSelector.DEFAULT_COLOR_GRADIENTS : options.initialGradients; this.selectedGradientIndex = typeof options.selectedGradientIndex === "undefined" ? 0 : options.selectedGradientIndex; document.head.appendChild(this.__createCustomStylesElement()); this.container.append(this._render()); } // END constructor // TODO: implement this? setGradients(gradients, selectedIndex) { // First empty the container this.__removeAllChildNodes(this.positioningContainerRef.current); this.colorGradients = []; this.colorGradientOptionRefs = []; this.selectedGradientIndex = selectedIndex; // Re-render button array gradients.map((grad) => { this.addGradient(grad); }); } addGradient(gradient) { // Render a new button const index = this.colorGradients.length; const refMainContainer = NoReact.useRef(); const refActionButton = NoReact.useRef(); const refDeleteButton = NoReact.useRef(); this.__renderOptionButton(gradient, index, refMainContainer, refActionButton, refDeleteButton); this.colorGradients.push(gradient); this.colorGradientOptionRefs.push({ mainButton: refActionButton, deleteButton: refDeleteButton }); // Add to container this.positioningContainerRef.current.appendChild(refMainContainer.current); } // +--------------------------------------------------------------------------------- // | A helper function to remove all child nodes. // +------------------------------- __removeAllChildNodes(node) { while (node.lastChild) { node.removeChild(node.lastChild); } } /** * Adds a new color gradient change listener to this ColorGradientPicker. * * @param {ColorGradientChangeListener} listener - The listener to add. * @returns {boolean} True, if the listener was added and did not exist before. */ addChangeListener(listener) { for (var i = 0; i < this.installedChangeListeners.length; i++) { if (this.installedChangeListeners[i] === listener) { return false; } } this.installedChangeListeners.push(listener); return true; } /** * * @param {ColorGradientChangeListener} listener The listener to remove. * @returns {boolean} True, if the listener existed and has been removed. */ removeChangeListener(listener) { for (var i = 0; i < this.installedChangeListeners.length; i++) { if (this.installedChangeListeners[i] === listener) { this.installedChangeListeners.splice(i, 1); return true; } } return false; } getSelectedColorGradient() { return this.colorGradients[this.selectedGradientIndex]; } __fireChangeEvent() { const newColorGradient = this.getSelectedColorGradient(); for (var i = 0; i < this.installedChangeListeners.length; i++) { this.installedChangeListeners[i](newColorGradient, this.selectedGradientIndex, this); } } /** * Creates a handler for click events on the main button. * * @returns */ __mainButtonClickHandler() { const _self = this; return (_evt) => { // _self.mainButtonContainerRef.current.style.visibility = "hidden"; _self.positioningContainerRef.current.style.visibility = _self.isDropdownOpen ? "hidden" : "visible"; _self.isDropdownOpen = !_self.isDropdownOpen; }; } /** * Creates a handler for click events on one of the option button in the dropdown. * * @returns */ __optionButtonClickHandler() { const _self = this; return (evt) => { _self.positioningContainerRef.current.style.visibility = "hidden"; _self.isDropdownOpen = false; const targetButton = evt.currentTarget; const clickedIndex_raw = targetButton.dataset["gradientIndex"]; const clickedIndex = Number.parseInt(clickedIndex_raw); console.log("clickedIndex", clickedIndex, clickedIndex_raw, targetButton.dataset, targetButton); if (Number.isNaN(clickedIndex)) { // Stop here. This is not what we want. return; } // Find child: and set selected. _self.__setSelectedIndex(clickedIndex); _self.__fireChangeEvent(); }; } __optionButtonDeleteHandler() { const _self = this; return (evt) => { if (this.colorGradientOptionRefs.length === 1) { // Keep the last record return; } const targetButton = evt.currentTarget; const clickedIndex_raw = targetButton.dataset["gradientIndex"]; const clickedIndex = Number.parseInt(clickedIndex_raw); console.log("clickedIndex", clickedIndex, clickedIndex_raw, targetButton.dataset, targetButton); if (Number.isNaN(clickedIndex)) { // Stop here. This is not what we want. return; } // Find child: and remove from DOM console.log("this.colorGradientOptionRefs", this.colorGradientOptionRefs, "clickedIndex", clickedIndex); const refPair = this.colorGradientOptionRefs[clickedIndex]; refPair.mainButton.current.parentElement.remove(); // Remove from logic // Remove 1 element at the given index this.colorGradients.splice(clickedIndex, 1); this.colorGradientOptionRefs.splice(clickedIndex, 1); // Update all following elements in the list. this.__updateOptionDataSetIndices(clickedIndex); if (this.selectedGradientIndex >= clickedIndex) { // this.selectedGradientIndex--; this.__setSelectedIndex(this.selectedGradientIndex); } console.log("All refs", this.colorGradientOptionRefs); }; } /** * Once a slider element was added or removed then the following indices must be updated. * * @param {number} startIndex - The slider index to start updating at. */ __updateOptionDataSetIndices(startIndex) { // Update all elements to the right of the new elelemt for (var i = startIndex; i < this.colorGradientOptionRefs.length; i++) { this.colorGradientOptionRefs[i].mainButton.current.dataset["gradientIndex"] = `${i}`; this.colorGradientOptionRefs[i].deleteButton.current.dataset["gradientIndex"] = `${i}`; } } __setSelectedIndex(newSelectedIndex) { for (var i = 0; i < this.colorGradientOptionRefs.length; i++) { const ref = this.colorGradientOptionRefs[i].mainButton; // Find child: and set selected. ref.current.querySelectorAll(".option-gradient-radio-circle")[0].innerHTML = newSelectedIndex === i ? "🞊" : "🞅"; } this.selectedGradientIndex = newSelectedIndex; const selectedGradient = this.colorGradients[this.selectedGradientIndex]; // Display new gradient in the main button this.__setMainButtonGradient(selectedGradient); } /** * Sets the backround color of the main button (of this dropdown) element. * * @param {ColorGradient} gradient - The gradient color to display. Must not be null. */ __setMainButtonGradient(gradient) { const colorDisplay = this.mainButtonContainerRef.current.querySelectorAll(".main-button-gradient-display")[0]; colorDisplay.style["background"] = gradient.toColorGradientString(); } /** * Renders a new option button for the dropdown menu. * * @param {ColorGradient} gradient * @returns {JsxElement} */ __renderOptionButton(gradient, index, refMainContainer, refActionButton, refDelButton) { return (NoReact.createElement("div", { sx: { d: "flex", flexDirection: "row" }, ref: refMainContainer }, NoReact.createElement("button", { className: "option-gradient-button", onClick: this.__optionButtonClickHandler(), style: { d: "flex", w: "100%", fontSize: this.css_buttonFontSize, minHeight: this.css_buttonHeight, maxHeight: this.css_buttonHeight }, ref: refActionButton, "data-gradientIndex": `${index}` }, NoReact.createElement("div", { className: "option-gradient-radio-circle", sx: { w: "2em", flexShrink: 2, alignContent: "center" } }, index === 0 ? "🞊" : "🞅"), NoReact.createElement("div", { sx: { w: "calc( 100% - 2em )", mr: "1em", background: gradient.toColorGradientString() } }, "\u00A0")), NoReact.createElement("button", { classname: "option-delete-button", sx: { fontSize: this.css_buttonFontSize, minHeight: this.css_buttonHeight, maxHeight: this.css_buttonHeight }, onClick: this.__optionButtonDeleteHandler(), "data-gradientIndex": `${index}`, ref: refDelButton }, NoReact.createElement("div", { sx: { w: "2em", flexShrink: 2, alignContent: "center" } }, "\uD83D\uDDD1")))); } /** * Creates a new array of option buttons (refs). * @returns */ _renderAllOptionButtons() { const _self = this; // First clear all references _self.colorGradientOptionRefs = []; return this.colorGradients.map((colorGradient, index) => { // console.log("num", num, index); const refMainContainer = NoReact.useRef(); const refActionButton = NoReact.useRef(); const refDelButton = NoReact.useRef(); _self.colorGradientOptionRefs.push({ mainButton: refActionButton, deleteButton: refDelButton }); return this.__renderOptionButton(colorGradient, index, refMainContainer, refActionButton, refDelButton); }); } /** * Init the container contents. * * @private */ _render() { const _self = this; // console.log("Rendering ...", NoReact); this.containerRef = NoReact.useRef(); this.mainButtonContainerRef = NoReact.useRef(); this.positioningContainerRef = NoReact.useRef(); const selectedGradient = this.colorGradients[this.selectedGradientIndex]; return (NoReact.createElement("div", { id: this.elementID, className: "color-gradient-selector", style: { minWidth: this.css_buttonWidth, maxWidth: this.css_buttonWidth, pos: "relative" }, ref: this.containerRef }, NoReact.createElement("button", { className: "main-button", ref: this.mainButtonContainerRef, style: { d: "flex", minWidth: this.css_buttonWidth, maxWidth: this.css_buttonWidth, minHeight: this.css_buttonHeight, maxHeight: this.css_buttonHeight }, onClick: this.__mainButtonClickHandler() }, NoReact.createElement("div", { className: "main-button-gradient-display", sx: { w: `calc( 100% - ${this.css_buttonFontSize} )`, background: selectedGradient.toColorGradientString() } }, "\u00A0"), NoReact.createElement("div", { sx: { w: this.css_buttonHeight, flexShrink: 2, fontSize: this.css_buttonFontSize_larger } }, "\u25BE")), NoReact.createElement("div", { className: "positioning-container", ref: this.positioningContainerRef, style: { minWidth: this.css_buttonWidth, maxHeight: "25vh", overflowY: "auto", v: "hidden", d: "flex", fd: "column", pos: "absolute", l: 0, zIndex: 999 } }, _self._renderAllOptionButtons()))); } // END function render() } ColorGradientSelector.DEFAULT_COLOR_GRADIENTS = [ ColorGradient.createDefault(), ColorGradient.createFrom(Color.Red, Color.Green), ColorGradient.createFrom(Color.Blue, Color.Gold) ]; //# sourceMappingURL=ColorGradientSelector.js.map