From 92b3d9c4dc39c31961a354e04d7e59fe350897f8 Mon Sep 17 00:00:00 2001 From: Sascha Karnatz <122262394+sascha-karnatz@users.noreply.github.com> Date: Mon, 25 Mar 2024 09:46:01 +0100 Subject: [PATCH] Rename Anchor Select into Dom Id Select It was previously a dom id - select and technically it is search for dom nodes with ids. It also adds an enabled and disabled state on the dom id - select. Also separate both dom id selects into slightly different components and add an enable and disable mechanic to alchemy-select. --- .../alchemy/admin/link_dialog/anchor_tab.rb | 8 +-- .../alchemy/admin/link_dialog/internal_tab.rb | 10 +-- .../alchemy_admin/components/anchor_select.js | 59 ---------------- .../alchemy_admin/components/dom_id_select.js | 69 +++++++++++++++++++ .../alchemy_admin/components/index.js | 2 +- .../alchemy_admin/components/select.js | 38 ++++++---- app/javascript/alchemy_admin/link_dialog.js | 36 ++++------ app/javascript/alchemy_admin/locales/en.js | 3 + .../admin/link_dialog/internal_tab_spec.rb | 2 +- .../alchemy_admin/components/select.spec.js | 52 +++++++++++--- 10 files changed, 164 insertions(+), 115 deletions(-) delete mode 100644 app/javascript/alchemy_admin/components/anchor_select.js create mode 100644 app/javascript/alchemy_admin/components/dom_id_select.js diff --git a/app/components/alchemy/admin/link_dialog/anchor_tab.rb b/app/components/alchemy/admin/link_dialog/anchor_tab.rb index 8bb02b7171..ab61b8f089 100644 --- a/app/components/alchemy/admin/link_dialog/anchor_tab.rb +++ b/app/components/alchemy/admin/link_dialog/anchor_tab.rb @@ -14,7 +14,7 @@ def self.panel_name def fields [ - anchor_select, + dom_id_select, title_input ] end @@ -25,13 +25,13 @@ def message private - def anchor_select + def dom_id_select label = label_tag("anchor_link", Alchemy.t(:anchor), class: "control-label") - options = [[Alchemy.t("Please choose"), ""]] + options = [[Alchemy.t("None"), ""]] options += [[@url, @url]] if is_selected? && @url select = select_tag(:anchor_link, options_for_select(options, @url), is: "alchemy-select") - select_component = content_tag("alchemy-anchor-select", select, {type: "preview"}) + select_component = content_tag("alchemy-dom-id-preview-select", select) content_tag("div", label + select_component, class: "input select") end diff --git a/app/components/alchemy/admin/link_dialog/internal_tab.rb b/app/components/alchemy/admin/link_dialog/internal_tab.rb index 69ec19fc5f..af41006447 100644 --- a/app/components/alchemy/admin/link_dialog/internal_tab.rb +++ b/app/components/alchemy/admin/link_dialog/internal_tab.rb @@ -50,13 +50,13 @@ def page_select end def dom_id_select - fragment = uri.fragment if uri + fragment = "##{uri.fragment}" if uri&.fragment label = label_tag("element_anchor", Alchemy.t(:anchor), class: "control-label") - options = [[Alchemy.t("Please choose"), ""]] - options += [["##{fragment}", fragment]] if is_selected? && fragment + options = [[page.nil? ? Alchemy.t("Select a page first") : Alchemy.t("None"), ""]] + options += [[fragment, fragment]] if is_selected? && fragment - select = select_tag("element_anchor", options_for_select(options, fragment), is: "alchemy-select") - select_component = content_tag("alchemy-anchor-select", select, {page: page&.id}) + select = select_tag("element_anchor", options_for_select(options, fragment), is: "alchemy-select", disabled: page.nil?) + select_component = content_tag("alchemy-dom-id-api-select", select, {page: page&.id}) content_tag("div", label + select_component, class: "input select") end diff --git a/app/javascript/alchemy_admin/components/anchor_select.js b/app/javascript/alchemy_admin/components/anchor_select.js deleted file mode 100644 index d18c38141c..0000000000 --- a/app/javascript/alchemy_admin/components/anchor_select.js +++ /dev/null @@ -1,59 +0,0 @@ -import { get } from "alchemy_admin/utils/ajax" - -class AnchorSelect extends HTMLElement { - #pageId = undefined - - connectedCallback() { - // get the anchors from the API or from the preview window - if (this.type === "preview") { - this.#fetchAnchorsFromPreview() - } else { - this.page = this.getAttribute("page") - } - } - - #fetchAnchors() { - get(Alchemy.routes.api_ingredients_path, { page_id: this.#pageId }).then( - (result) => { - this.selectElement.data = result.data.ingredients - .filter((ingredient) => ingredient.data?.dom_id) - .map((ingredient) => this.#dataItem(ingredient.data.dom_id)) - } - ) - } - - #fetchAnchorsFromPreview() { - // wait a tick to let the browser initialize the inner select component - setTimeout(() => { - const frame = document.getElementById("alchemy_preview_window") - const elements = frame.contentDocument?.querySelectorAll("[id]") || [] - if (elements.length > 0) { - this.selectElement.data = Array.from(elements).map((element) => { - return this.#dataItem(element.id) - }) - } - }) - } - - #dataItem(hash) { - return { - id: hash, - text: `#${hash}` - } - } - - set page(pageId) { - this.#pageId = pageId - this.#fetchAnchors() - } - - get selectElement() { - return this.querySelector("select") - } - - get type() { - return this.getAttribute("type") - } -} - -customElements.define("alchemy-anchor-select", AnchorSelect) diff --git a/app/javascript/alchemy_admin/components/dom_id_select.js b/app/javascript/alchemy_admin/components/dom_id_select.js new file mode 100644 index 0000000000..b263d7997d --- /dev/null +++ b/app/javascript/alchemy_admin/components/dom_id_select.js @@ -0,0 +1,69 @@ +import { get } from "alchemy_admin/utils/ajax" +import { translate } from "alchemy_admin/i18n" + +class DomIdSelect extends HTMLElement { + dataItem(hash) { + return { + id: `#${hash}`, + text: `#${hash}` + } + } + + get selectElement() { + return this.querySelector('select[is="alchemy-select"]') + } +} + +class DomIdApiSelect extends DomIdSelect { + #pageId = undefined + + connectedCallback() { + this.page = this.getAttribute("page") + } + + async #fetchDomIds() { + const result = await get(Alchemy.routes.api_ingredients_path, { + page_id: this.#pageId + }) + const options = result.data.ingredients + .filter((ingredient) => ingredient.data?.dom_id) + .map((ingredient) => this.dataItem(ingredient.data.dom_id)) + const prompt = + options.length > 0 ? translate("None") : translate("No anchors found") + + this.selectElement.setOptions(options, prompt) + this.selectElement.enable() + } + + #reset() { + // wait a tick to initialize the alchemy-select + requestAnimationFrame(() => { + this.selectElement.disable() + this.selectElement.setOptions([], translate("Select a page first")) + }) + } + + set page(pageId) { + this.#pageId = pageId + pageId ? this.#fetchDomIds() : this.#reset() + } +} + +class DomIdPreviewSelect extends DomIdSelect { + connectedCallback() { + // wait a tick to let the browser initialize the inner select component + requestAnimationFrame(() => { + const frame = document.getElementById("alchemy_preview_window") + const elements = frame.contentDocument?.querySelectorAll("[id]") || [] + if (elements.length > 0) { + const options = Array.from(elements).map((element) => { + return this.dataItem(element.id) + }) + this.selectElement.setOptions(options, translate("None")) + } + }) + } +} + +customElements.define("alchemy-dom-id-api-select", DomIdApiSelect) +customElements.define("alchemy-dom-id-preview-select", DomIdPreviewSelect) diff --git a/app/javascript/alchemy_admin/components/index.js b/app/javascript/alchemy_admin/components/index.js index 5395fb7995..d1304f3418 100644 --- a/app/javascript/alchemy_admin/components/index.js +++ b/app/javascript/alchemy_admin/components/index.js @@ -1,9 +1,9 @@ -import "alchemy_admin/components/anchor_select" import "alchemy_admin/components/button" import "alchemy_admin/components/char_counter" import "alchemy_admin/components/clipboard_button" import "alchemy_admin/components/datepicker" import "alchemy_admin/components/dialog_link" +import "alchemy_admin/components/dom_id_select" import "alchemy_admin/components/element_editor" import "alchemy_admin/components/elements_window" import "alchemy_admin/components/list_filter" diff --git a/app/javascript/alchemy_admin/components/select.js b/app/javascript/alchemy_admin/components/select.js index 0474e1eb18..3ff8e8ecca 100644 --- a/app/javascript/alchemy_admin/components/select.js +++ b/app/javascript/alchemy_admin/components/select.js @@ -1,5 +1,5 @@ class Select extends HTMLSelectElement { - #select2Element = undefined + #select2Element connectedCallback() { this.classList.add("alchemy_selectbox") @@ -10,25 +10,37 @@ class Select extends HTMLSelectElement { }) } - set data(data) { - let selected = this.value - // remove all previous entries except the default please select entry which has no value or is selected - const emptyOption = - this.options[0]?.value === "" - ? this.options[0].cloneNode(true) - : undefined + enable() { + this.removeAttribute("disabled") + this.#updateSelect2() + } + + disable() { + this.setAttribute("disabled", "disabled") + this.#updateSelect2() + } + + setOptions(data, prompt = undefined) { + let selectedValue = this.value + // reset the old options and insert the placeholder(s) first this.innerHTML = "" - if (emptyOption) { - this.add(emptyOption) + if (prompt) { + this.add(new Option(prompt, "")) } + // add the new options to the select data.forEach((item) => { - const option = new Option(item.text, item.id, false, item.id === selected) - this.add(option) + this.add(new Option(item.text, item.id, false, item.id === selectedValue)) }) - // inform Select2 to update + this.#updateSelect2() + } + + /** + * inform Select2 to update + */ + #updateSelect2() { this.#select2Element.trigger("change") } } diff --git a/app/javascript/alchemy_admin/link_dialog.js b/app/javascript/alchemy_admin/link_dialog.js index 1d530ad779..c55de6505e 100644 --- a/app/javascript/alchemy_admin/link_dialog.js +++ b/app/javascript/alchemy_admin/link_dialog.js @@ -75,12 +75,13 @@ export class LinkDialog extends Alchemy.Dialog { * @param page */ #updatePage(page = null) { - document.getElementById("internal_link").value = - page != null ? page.url_path : undefined + const internalLink = document.getElementById("internal_link") + const domIdSelect = document.querySelector( + '[data-link-form-type="internal"] alchemy-dom-id-api-select' + ) - document.querySelector( - '[data-link-form-type="internal"] alchemy-anchor-select' - ).page = page != null ? page.id : undefined + internalLink.value = page != null ? page.url_path : undefined + domIdSelect.page = page != null ? page.id : undefined } /** @@ -94,33 +95,20 @@ export class LinkDialog extends Alchemy.Dialog { if (linkType === "internal" && elementAnchor.value !== "") { // remove possible fragments on the url and attach the fragment (which contains the #) url = url.replace(/#\w+$/, "") + elementAnchor.value + } else if (linkType === "external" && !url.match(Alchemy.link_url_regexp)) { + // show validation error and prevent link creation + this.#showValidationError() + return } // Create the link - this.#createLink({ + this.#onCreateLink({ url: url.trim(), title: document.getElementById(`${linkType}_link_title`).value, target: document.getElementById(`${linkType}_link_target`)?.value, type: linkType }) - } - - /** - * Creates a link if no validation errors are present. - * Otherwise shows an error notice. - * @param linkOptions - */ - #createLink(linkOptions) { - const invalidInput = - linkOptions.type === "external" && - !linkOptions.url.match(Alchemy.link_url_regexp) - - if (invalidInput) { - this.#showValidationError() - } else { - this.#onCreateLink(linkOptions) - this.close() - } + this.close() } /** diff --git a/app/javascript/alchemy_admin/locales/en.js b/app/javascript/alchemy_admin/locales/en.js index 01f1d27b66..9a61436cca 100644 --- a/app/javascript/alchemy_admin/locales/en.js +++ b/app/javascript/alchemy_admin/locales/en.js @@ -21,6 +21,9 @@ export const en = { "Uploaded bytes exceed file size": "Uploaded bytes exceed file size", "Abort upload": "Abort upload", "Cancel all uploads": "Cancel all uploads", + None: "None", + "No anchors found": "No anchors found", + "Select a page first": "Select a page first", Close: "Close", formats: { datetime: "Y-m-d H:i", diff --git a/spec/components/alchemy/admin/link_dialog/internal_tab_spec.rb b/spec/components/alchemy/admin/link_dialog/internal_tab_spec.rb index 961ab4d653..a7066861d2 100644 --- a/spec/components/alchemy/admin/link_dialog/internal_tab_spec.rb +++ b/spec/components/alchemy/admin/link_dialog/internal_tab_spec.rb @@ -37,7 +37,7 @@ end it "should not have the value of the hash fragment" do - expect(page.find(:css, "select[name=element_anchor]").value).to eq(fragment) + expect(page.find(:css, "select[name=element_anchor]").value).to eq("#" + fragment) end end end diff --git a/spec/javascript/alchemy_admin/components/select.spec.js b/spec/javascript/alchemy_admin/components/select.spec.js index c39702c759..2ef91af580 100644 --- a/spec/javascript/alchemy_admin/components/select.spec.js +++ b/spec/javascript/alchemy_admin/components/select.spec.js @@ -40,12 +40,15 @@ describe("alchemy-select", () => { }) }) - describe("data", () => { + describe("setOptions", () => { it("adds the new entry and replace the old ones", () => { - component.data = [ - { id: "foo", text: "bar" }, - { id: "bar", text: "last" } - ] + component.setOptions( + [ + { id: "foo", text: "bar" }, + { id: "bar", text: "last" } + ], + "Please Select" + ) expect(component.options.length).toEqual(3) expect(component.options[0].text).toEqual("Please Select") @@ -53,11 +56,22 @@ describe("alchemy-select", () => { expect(component.options[2].text).toEqual("last") }) + it("does not add a prompt, if no prompt value is given", () => { + component.setOptions([ + { id: "foo", text: "bar" }, + { id: "bar", text: "last" } + ]) + + expect(component.options.length).toEqual(2) + expect(component.options[0].text).toEqual("bar") + expect(component.options[1].text).toEqual("last") + }) + it("resets without any options", () => { const html = `` component = renderComponent("alchemy-select", html) - component.data = [{ id: "foo", text: "bar" }] + component.setOptions([{ id: "foo", text: "bar" }]) expect(component.options.length).toEqual(1) expect(component.options[0].text).toEqual("bar") @@ -73,10 +87,10 @@ describe("alchemy-select", () => { ` component = renderComponent("alchemy-select", html) - component.data = [ + component.setOptions([ { id: "foo", text: "bar" }, { id: "2", text: "Second" } - ] + ]) expect(component.options.length).toEqual(2) expect(component.options[0].text).toEqual("bar") @@ -84,4 +98,26 @@ describe("alchemy-select", () => { expect(component.options[1].selected).toBeTruthy() }) }) + + describe("enable", () => { + it("removes the disabled attribute", () => { + const html = `` + + component = renderComponent("alchemy-select", html) + component.enable() + + expect(component.hasAttribute("disabled")).toBeFalsy() + }) + }) + + describe("disable", () => { + it("adds the disabled attribute", () => { + const html = `` + + component = renderComponent("alchemy-select", html) + component.disable() + + expect(component.hasAttribute("disabled")).toBeTruthy() + }) + }) })