From b45c411a253cb1b4222bad65841a3f24625afe34 Mon Sep 17 00:00:00 2001 From: CodoPixel Date: Wed, 28 Apr 2021 16:45:38 +0200 Subject: [PATCH] feat(): add new methods to make the panel dynamic --- CHANGELOG.md | 18 +++++ README.md | 21 +++++ src/JSPanel.js | 178 ++++++++++++++++++++++++++++++++++++++---- src/JSPanel.ts | 185 ++++++++++++++++++++++++++++++++++++++++---- src/example.html | 2 + src/panel-style.css | 5 ++ 6 files changed, 382 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdd1dfc..096ebae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [1.2.0] - April 28, 2021 + +New methods: + ++ toggleItem ++ removeItem ++ removeItems ++ getAllIDS ++ getItem ++ addItem ++ replaceItemWith ++ deletePanel ++ isOpen + +New option: + ++ id + ## [1.1.3] - April 28, 2021 Bug fixes diff --git a/README.md b/README.md index 371822c..c01c427 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ The items have also specific options: |name|type|default value|description| |----|----|-------------|-----------| |`title`|string|(_required_)|The title of the item.| +|`id`|positive number|from 0, incrementing|The id of the item. Used to recognize items via some methods like `removeItem()` etc.| |`icon`|string|null|The path to an image.| |`fontawesome_icon`|string|null|The className of a Fontawesome icon.| |`fontawesome_color`|string|null|The color of the Fontawesome icon.| @@ -81,6 +82,26 @@ The items have also specific options: In order to use `fontawesome_icon` and `fontawesome_color`, make sure you've installed [Fontawesome](https://cdnjs.com/libraries/font-awesome) in your project. +## Make the panel dynamic + +Use the following methods to modify the content of the panel after its creation: + +* `toggleItem(id:number, disable=false)`: set disable to true if you want to disable the items. Set disable to false if you just want the item to disappear (display:none). Select the item with its ID (by default the first item has an ID of 0, then 1 etc.). + +* `removeItem(id:number)`: removes an item. + +* `removeItems(ids:number[])`: removes several items. + +* `getAllIDS()`: gets the id of each item. + +* `getItem(id:number)`: gets an item according to its ID. + +* `addItem(item)`: adds a new item. + +* `replaceItemWith(item, id:number)`: selects an item with its ID and replaces it by the new one. + +* `deletePanel()`: deletes the panel. + ## Customize the panel You can change the style of the panel by modifying the CSS file. There are the main variables defined at the beginning of the file: diff --git a/src/JSPanel.js b/src/JSPanel.js index 2e1e6ec..cb78765 100644 --- a/src/JSPanel.js +++ b/src/JSPanel.js @@ -6,7 +6,7 @@ class JSPanel { /** * @constructs JSPanel * @param {HTMLButtonElement} button The button which will display the panel. - * @param {{top?:number,right?:number,bottom?:number,left?:number,items:Array<{title:string,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}>}} options The options to customize the panel. + * @param {{top?:number,right?:number,bottom?:number,left?:number,items:Array<{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}>}} options The options to customize the panel. */ constructor(button, options) { /** @@ -56,8 +56,11 @@ class JSPanel { // if (this.options.items) { const container = this._createEl("div", { className: "container-items" }); - for (let item of this.options.items) { + for (let i = 0; i < this.options.items.length; i++) { + const item = this.options.items[i]; if (item) { + if (!item.id) + item.id = i; const built_item = this._buildItem(item); container.appendChild(built_item); } @@ -73,7 +76,7 @@ class JSPanel { document.addEventListener("click", (e) => { const target = e.target; if (target && this.panel) { - if (!this.panel.contains(target) && this._isOpen()) { + if (!this.panel.contains(target) && this.isOpen()) { this._closePanel(); } } @@ -83,7 +86,7 @@ class JSPanel { this._insertAfterButton(this.panel); this.panel.onkeydown = (e) => { if (e.key === "Tab" || e.keyCode === 9) { - if (this._isOpen()) + if (this.isOpen()) this._focusInPanel(e); } }; @@ -93,7 +96,7 @@ class JSPanel { // So that it's easier for the user to close the panel with his/her keyboard. this.button.onkeydown = (e) => { if (e.key === "Tab" || e.keyCode === 9) { - if (this._isOpen()) { + if (this.isOpen()) { e.preventDefault(); const active_elements = this._getAllActiveItems(); if (active_elements && active_elements[0]) { @@ -124,9 +127,9 @@ class JSPanel { /** * Checks if the panel is currently opened or not. * @returns {boolean} True if the panel is opened. - * @private + * @public */ - _isOpen() { + isOpen() { if (this.panel) { return !this.panel.classList.contains("panel-hidden"); } @@ -142,7 +145,7 @@ class JSPanel { _togglePanel(e) { if (this.button && this.panel) { e.stopPropagation(); - if (this._isOpen()) { + if (this.isOpen()) { this._closePanel(); } else { @@ -157,9 +160,10 @@ class JSPanel { /** * Gets all the items from the panel if it's open. * @returns {NodeListOf|null} All the items. + * @private */ _getAllItems() { - if (this._isOpen()) { + if (this.isOpen()) { return this.panel.querySelectorAll("button"); } else { @@ -169,12 +173,13 @@ class JSPanel { /** * Gets all the active items from the panel if it's open. * @returns {Array|null} All the items that have an onclick property. + * @private */ _getAllActiveItems() { - if (this._isOpen()) { + if (this.isOpen()) { const active_elements = Array.from(this.panel.querySelectorAll("button")); active_elements.push(this.button); - return active_elements; + return active_elements.filter((e) => e.style.display !== "none" && !e.hasAttribute("disabled")); } else { return null; @@ -229,17 +234,19 @@ class JSPanel { } /** * Builds an item. - * @param {{title:string,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} item The item to build. + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} item The item to build. * @returns {HTMLElement} The item as an HTML element. * @private */ _buildItem(item) { + const id = item.id.toString(); if (item.separator) { - const div = this._createEl("div", { className: 'jspanel-separator' }); + const div = this._createEl("div", { className: 'jspanel-separator', attributes: [["data-id", id]] }); return div; } else { const button = this._createEl("button"); + button.setAttribute("data-id", id); button.setAttribute("aria-label", item.title); if ((item.icon && !item.fontawesome_icon) || (item.icon && item.fontawesome_icon)) { const icon = this._createEl("img", { attributes: [["src", item.icon]] }); @@ -277,6 +284,7 @@ class JSPanel { /** * Blocks the focus inside the panel while it's open. * @param {KeyboardEvent} e The keyboard event. + * @private */ _focusInPanel(e) { const all_items = this._getAllActiveItems(); @@ -341,4 +349,148 @@ class JSPanel { max = Math.floor(max); return Math.floor(Math.random() * (max - min)) + min; } + /** + * Toggles an item. + * @param {number} id The id of the item. + * @param {boolean} disable If the item is a button (not a separator), then, instead of display:none, we disable it. By default: false. + * @public + * @since 1.2.0 + */ + toggleItem(id, disable = false) { + if (this.panel) { + const items = Array.from(this.panel.querySelectorAll("[data-id='" + id + "']")); + if (disable) { + if (items) + for (let item of items) { + if (item.tagName.toLowerCase() === "button") { + item.hasAttribute("disabled") ? item.removeAttribute("disabled") : item.setAttribute("disabled", "disabled"); + } + else { + if (items) + for (let item of items) + item.style.display = item.style.display == "none" ? null : "none"; + } + } + } + else { + if (items) + for (let item of items) + item.style.display = item.style.display == "none" ? null : "none"; + } + } + } + /** + * Removes an item. + * @param {number} id The id of the item to remove. + * @public + * @since 1.2.0 + */ + removeItem(id) { + if (this.panel) { + const item = this.getItem(id); + if (item && item.parentElement) { + item.parentElement.removeChild(item); + } + } + } + /** + * Removes several items. + * @param {Array} ids The ids of the items. + * @public + * @since 1.2.0 + */ + removeItems(ids) { + if (this.panel) { + for (let id of ids) { + this.removeItem(id); + } + } + } + /** + * Gets the id of each item. + * @returns {Array} The list of ids. + * @public + * @since 1.2.0 + */ + getAllIDS() { + if (this.panel) { + const all_items = Array.from(this.panel.querySelectorAll("[data-id]")); + const all_ids = []; + if (all_items) { + for (let item of all_items) { + all_ids.push(parseInt(item.getAttribute("data-id"))); + } + return all_ids; + } + } + return []; + } + /** + * Gets an item. + * @param id The id of the item. + * @returns {HTMLElement|null} The item. + * @public + * @since 1.2.0 + */ + getItem(id) { + if (this.panel) { + return this.panel.querySelector("[data-id='" + id + "']"); + } + return null; + } + /** + * Builds a new item to be added to the panel after its creation. + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} new_item The new item to be built. + * @param default_new_id The ID of the item, just in case the user did not specify any. + * @returns The HTML element of an item. + * @private + * @since 1.2.0 + */ + _buildNewItem(new_item, default_new_id) { + if (!new_item.id && (default_new_id === null || default_new_id === undefined)) + throw new Error("An item must have an ID."); + if (!new_item.id) + new_item.id = default_new_id; + const build_item = this._buildItem(new_item); + return build_item; + } + /** + * Adds an item + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} new_item The new item to be built. + * @public + * @since 1.2.0 + */ + addItem(new_item) { + if (this.panel) { + this.panel.appendChild(this._buildNewItem(new_item, Math.max(...this.getAllIDS()))); + } + } + /** + * Replaces an item by another one. + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} new_item The new item. + * @param {number} id The id of the item to be replaced. + * @public + * @since 1.2.0 + */ + replaceItemWith(new_item, id) { + if (this.panel) { + const current_item = this.getItem(id); + if (current_item) { + const new_built_item = this._buildNewItem(new_item, parseInt(current_item.getAttribute("data-id"))); + current_item.replaceWith(new_built_item); + } + } + } + /** + * Deletes the panel. + * @public + * @since 1.2.0 + */ + deletePanel() { + if (this.panel && this.panel.parentElement) { + this.panel.parentElement.removeChild(this.panel); + this._closePanel(); + this.panel = null; + } + } } diff --git a/src/JSPanel.ts b/src/JSPanel.ts index 17ac1d9..c62beb7 100644 --- a/src/JSPanel.ts +++ b/src/JSPanel.ts @@ -8,6 +8,8 @@ interface PanelOptions { interface PanelItem { title: string; + /** @since 1.2.0 */ + id: number; icon?: string; fontawesome_icon?: string; fontawesome_color?: string; @@ -46,7 +48,7 @@ class JSPanel { /** * The options to customize the panel. - * @type {{top?:number,right?:number,bottom?:number,left?:number,items:Array<{title:string,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}>}} + * @type {{top?:number,right?:number,bottom?:number,left?:number,items:Array<{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}>}} * @private */ private options: PanelOptions; @@ -61,7 +63,7 @@ class JSPanel { /** * @constructs JSPanel * @param {HTMLButtonElement} button The button which will display the panel. - * @param {{top?:number,right?:number,bottom?:number,left?:number,items:Array<{title:string,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}>}} options The options to customize the panel. + * @param {{top?:number,right?:number,bottom?:number,left?:number,items:Array<{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}>}} options The options to customize the panel. */ public constructor(button: HTMLButtonElement, options: PanelOptions) { this.button = button; @@ -103,8 +105,10 @@ class JSPanel { if (this.options.items) { const container = this._createEl("div", { className: "container-items" }); - for (let item of this.options.items) { + for (let i = 0; i < this.options.items.length; i++) { + const item = this.options.items[i]; if (item) { + if (!item.id) item.id = i; const built_item = this._buildItem(item); container.appendChild(built_item); } @@ -121,7 +125,7 @@ class JSPanel { document.addEventListener("click", (e) => { const target = e.target as HTMLElement; if (target && this.panel) { - if (!this.panel.contains(target) && this._isOpen()) { + if (!this.panel.contains(target) && this.isOpen()) { this._closePanel(); } } @@ -134,7 +138,7 @@ class JSPanel { this.panel.onkeydown = (e: KeyboardEvent) => { if (e.key === "Tab" || e.keyCode === 9) { - if (this._isOpen()) this._focusInPanel(e); + if (this.isOpen()) this._focusInPanel(e); } }; @@ -144,7 +148,7 @@ class JSPanel { // So that it's easier for the user to close the panel with his/her keyboard. this.button.onkeydown = (e: KeyboardEvent) => { if (e.key === "Tab" || e.keyCode === 9) { - if (this._isOpen()) { + if (this.isOpen()) { e.preventDefault(); const active_elements = this._getAllActiveItems(); @@ -177,9 +181,9 @@ class JSPanel { /** * Checks if the panel is currently opened or not. * @returns {boolean} True if the panel is opened. - * @private + * @public */ - private _isOpen(): boolean { + public isOpen(): boolean { if (this.panel) { return !this.panel.classList.contains("panel-hidden"); } else { @@ -196,7 +200,7 @@ class JSPanel { if (this.button && this.panel) { e.stopPropagation(); - if (this._isOpen()) { + if (this.isOpen()) { this._closePanel(); } else { this.button.setAttribute("aria-expanded", "true"); @@ -211,9 +215,10 @@ class JSPanel { /** * Gets all the items from the panel if it's open. * @returns {NodeListOf|null} All the items. + * @private */ private _getAllItems(): NodeListOf | null { - if (this._isOpen()) { + if (this.isOpen()) { return (this.panel as HTMLElement).querySelectorAll("button"); } else { return null; @@ -223,12 +228,13 @@ class JSPanel { /** * Gets all the active items from the panel if it's open. * @returns {Array|null} All the items that have an onclick property. + * @private */ private _getAllActiveItems(): HTMLElement[] | null { - if (this._isOpen()) { + if (this.isOpen()) { const active_elements: HTMLElement[] = Array.from((this.panel as HTMLElement).querySelectorAll("button")); active_elements.push(this.button as HTMLElement); - return active_elements; + return active_elements.filter((e) => e.style.display !== "none" && !e.hasAttribute("disabled")); } else { return null; } @@ -286,16 +292,19 @@ class JSPanel { /** * Builds an item. - * @param {{title:string,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} item The item to build. + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} item The item to build. * @returns {HTMLElement} The item as an HTML element. * @private */ private _buildItem(item: PanelItem): HTMLElement { + const id = (item.id as number).toString(); + if (item.separator) { - const div = this._createEl("div", { className: 'jspanel-separator' }); + const div = this._createEl("div", { className: 'jspanel-separator', attributes: [["data-id", id]] }); return div; } else { const button = this._createEl("button"); + button.setAttribute("data-id", id); button.setAttribute("aria-label", item.title); if ((item.icon && !item.fontawesome_icon) || (item.icon && item.fontawesome_icon)) { @@ -337,6 +346,7 @@ class JSPanel { /** * Blocks the focus inside the panel while it's open. * @param {KeyboardEvent} e The keyboard event. + * @private */ private _focusInPanel(e: KeyboardEvent): void { const all_items = this._getAllActiveItems(); @@ -400,4 +410,151 @@ class JSPanel { max = Math.floor(max); return Math.floor(Math.random() * (max - min)) + min; } + + /** + * Toggles an item. + * @param {number} id The id of the item. + * @param {boolean} disable If the item is a button (not a separator), then, instead of display:none, we disable it. By default: false. + * @public + * @since 1.2.0 + */ + public toggleItem(id: number, disable: boolean = false): void { + if (this.panel) { + const items = Array.from(this.panel.querySelectorAll("[data-id='" + id + "']")) as HTMLElement[]; + if (disable) { + if (items) for (let item of items) { + if (item.tagName.toLowerCase() === "button") { + item.hasAttribute("disabled") ? item.removeAttribute("disabled") : item.setAttribute("disabled", "disabled"); + } else { + if (items) for (let item of items) (item.style.display as any) = item.style.display == "none" ? null : "none"; + } + } + } else { + if (items) for (let item of items) (item.style.display as any) = item.style.display == "none" ? null : "none"; + } + } + } + + /** + * Removes an item. + * @param {number} id The id of the item to remove. + * @public + * @since 1.2.0 + */ + public removeItem(id: number): void { + if (this.panel) { + const item = this.getItem(id); + if (item && item.parentElement) { + item.parentElement.removeChild(item); + } + } + } + + /** + * Removes several items. + * @param {Array} ids The ids of the items. + * @public + * @since 1.2.0 + */ + public removeItems(ids: number[]): void { + if (this.panel) { + for (let id of ids) { + this.removeItem(id); + } + } + } + + /** + * Gets the id of each item. + * @returns {Array} The list of ids. + * @public + * @since 1.2.0 + */ + public getAllIDS(): number[] { + if (this.panel) { + const all_items = Array.from(this.panel.querySelectorAll("[data-id]")); + const all_ids: number[] = []; + if (all_items) { + for (let item of all_items) { + all_ids.push(parseInt(item.getAttribute("data-id") as string)); + } + + return all_ids; + } + } + + return []; + } + + /** + * Gets an item. + * @param id The id of the item. + * @returns {HTMLElement|null} The item. + * @public + * @since 1.2.0 + */ + public getItem(id: number): HTMLElement | null { + if (this.panel) { + return this.panel.querySelector("[data-id='" + id + "']"); + } + + return null; + } + + /** + * Builds a new item to be added to the panel after its creation. + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} new_item The new item to be built. + * @param default_new_id The ID of the item, just in case the user did not specify any. + * @returns The HTML element of an item. + * @private + * @since 1.2.0 + */ + private _buildNewItem(new_item: PanelItem, default_new_id: number): HTMLElement { + if (!new_item.id && (default_new_id === null || default_new_id === undefined)) throw new Error("An item must have an ID."); + if (!new_item.id) new_item.id = default_new_id; + const build_item = this._buildItem(new_item); + return build_item; + } + + /** + * Adds an item + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} new_item The new item to be built. + * @public + * @since 1.2.0 + */ + public addItem(new_item: PanelItem): void { + if (this.panel) { + this.panel.appendChild(this._buildNewItem(new_item, Math.max(...this.getAllIDS()))); + } + } + + /** + * Replaces an item by another one. + * @param {{title:string,id?:number,icon?:string,fontawesome_icon?:string,fontawesome_color?:string,className?:string,attributes?:Array>,onclick?:Function,separator?:boolean}} new_item The new item. + * @param {number} id The id of the item to be replaced. + * @public + * @since 1.2.0 + */ + public replaceItemWith(new_item: PanelItem, id: number): void { + if (this.panel) { + const current_item = this.getItem(id); + if (current_item) { + const new_built_item = this._buildNewItem(new_item, parseInt(current_item.getAttribute("data-id") as string)); + current_item.replaceWith(new_built_item); + } + } + } + + /** + * Deletes the panel. + * @public + * @since 1.2.0 + */ + public deletePanel(): void { + if (this.panel && this.panel.parentElement) { + this.panel.parentElement.removeChild(this.panel); + this._closePanel(); + this.panel = null; + } + } } diff --git a/src/example.html b/src/example.html index 400313e..b68ad6a 100644 --- a/src/example.html +++ b/src/example.html @@ -54,6 +54,8 @@

Look the button right there, here, you see it ?

+ +
diff --git a/src/panel-style.css b/src/panel-style.css index 35d17f3..57c8cce 100644 --- a/src/panel-style.css +++ b/src/panel-style.css @@ -51,6 +51,11 @@ align-items: center; } +.jspanel button[disabled] { + opacity: 0.5; + cursor: not-allowed; +} + .jspanel button:hover { background-color: var(--panel-hover-item-background-color, #f4f6fa); color: var(--panel-hover-item-color, #385074);