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()
+ })
+ })
})