diff --git a/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee b/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee index 3d9fbfe62a..129ee19a6a 100644 --- a/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee +++ b/app/assets/javascripts/alchemy/alchemy.dialog.js.coffee @@ -1,5 +1,5 @@ # Dialog windows -# +# @deprecated use dialog.js instead class window.Alchemy.Dialog DEFAULTS: @@ -229,6 +229,11 @@ window.Alchemy.closeCurrentDialog = (callback) -> dialog.options.closed = callback dialog.close() + # this is a intermediate solution to use the same method also with the new dialog + new_dialog = document.querySelector("sl-dialog") + if new_dialog + new_dialog.dialogClassInstance.onSubmitSuccess() + # Utility function to open a new Dialog window.Alchemy.openDialog = (url, options) -> if !url diff --git a/app/assets/stylesheets/alchemy/shoelace.scss b/app/assets/stylesheets/alchemy/shoelace.scss index 00bae2bb4d..b07446bd25 100644 --- a/app/assets/stylesheets/alchemy/shoelace.scss +++ b/app/assets/stylesheets/alchemy/shoelace.scss @@ -347,6 +347,7 @@ sl-tooltip { sl-dialog { &::part(panel) { background-color: var(--color-grey_light); + min-height: var(--dialog-min-height); --body-spacing: var(--spacing-4) var(--spacing-3); --footer-spacing: var(--spacing-4) var(--spacing-3); } diff --git a/app/javascript/alchemy_admin/components/dialog_link.js b/app/javascript/alchemy_admin/components/dialog_link.js index 779018c530..916a83a8be 100644 --- a/app/javascript/alchemy_admin/components/dialog_link.js +++ b/app/javascript/alchemy_admin/components/dialog_link.js @@ -1,13 +1,4 @@ -export const DEFAULTS = { - header_height: 36, - size: "400x300", - padding: true, - title: "", - modal: true, - overflow: "visible", - ready: () => {}, - closed: () => {} -} +import { Dialog } from "alchemy_admin/dialog" export class DialogLink extends HTMLAnchorElement { connectedCallback() { @@ -20,21 +11,14 @@ export class DialogLink extends HTMLAnchorElement { } openDialog() { - this.dialog = new Alchemy.Dialog( - this.getAttribute("href"), - this.dialogOptions - ) + this.dialog = new Dialog(this.getAttribute("href"), this.dialogOptions) this.dialog.open() } get dialogOptions() { - const options = this.dataset.dialogOptions + return this.dataset.dialogOptions ? JSON.parse(this.dataset.dialogOptions) : {} - return { - ...DEFAULTS, - ...options - } } get disabled() { diff --git a/app/javascript/alchemy_admin/dialog.js b/app/javascript/alchemy_admin/dialog.js new file mode 100644 index 0000000000..1b39b8e0a6 --- /dev/null +++ b/app/javascript/alchemy_admin/dialog.js @@ -0,0 +1,127 @@ +import { createHtmlElement } from "alchemy_admin/utils/dom_helpers" + +export class Dialog { + #dialogComponent + #onReject + #onResolve + + /** + * @param {string} url + * @param {object} options + */ + constructor(url, options = {}) { + this.url = url + this.options = { title: "", size: "300x400", padding: true, ...options } + } + + /** + * load the content of given url and than open the dialog + * @returns {Promise} + */ + open() { + this.#build() + // Show the dialog with the spinner after a small delay. + // Most of the times the spinner will not be visible, because the requested + // content is already downloaded. These behavior "feels" a little bit better. + // It calls the show - method twice, but this is not a big deal. + setTimeout(() => this.#dialogComponent.show(), 300) + + this.#loadContent().then((content) => { + // create the dialog markup and show the dialog + this.#dialogComponent.innerHTML = content + this.#subscribeFormSubmit() + this.#dialogComponent.show() + + // bind the current class instance to the DOM - element + // this should be an intermediate solution + // the main goal, is to close the dialog with the turbo:submit-end - event + this.#dialogComponent.dialogClassInstance = this + + // the dialog is closing with the overlay, esc - key, or close - button + // the reject - callback will be fired, because the user decided to close the + // dialog without saving anything + this.#dialogComponent.addEventListener("sl-request-close", () => { + this.#dialogComponent.remove() + this.#onReject() + }) + }) + + return new Promise((resolve, reject) => { + this.#onResolve = resolve + this.#onReject = reject + }) + } + + /** + * hide and remove dialog + * the open - promise will be resolved + */ + onSubmitSuccess() { + this.#dialogComponent.hide().then(() => { + this.#dialogComponent.remove() + this.#onResolve() + }) + } + + /** + * load content of the given url + * @returns {Promise} + */ + async #loadContent() { + const response = await fetch(this.url, { + headers: { "X-Requested-With": "XMLHttpRequest" } + }) + return await response.text() + } + + /** + * create and append the dialog container to the DOM + */ + #build() { + this.#dialogComponent = createHtmlElement(` + + + + `) + document.body.append(this.#dialogComponent) + } + + /** + * add event listeners to forms inside the dialog + * + * listen to turbo:submit-end events if a remote form was submitted + * only forms with the attribute `data-close-dialog-on-success` + */ + #subscribeFormSubmit() { + this.#dialogComponent + .querySelectorAll("form[data-close-dialog-on-success]") + .forEach((form) => + form.addEventListener("turbo:submit-end", (evt) => { + if (evt.detail.success) { + this.onSubmitSuccess() + } + }) + ) + } + + /** + * provide the custom properties for the dialog settings + * @returns {string} + */ + get styles() { + const sizes = this.options.size.split("x") + let styles = `--width: ${sizes[0]}px; --dialog-min-height: ${sizes[1]}px;` + if (!this.options.padding) { + styles += " --body-spacing: 0;" + } + return styles + } + + /** + * get the title of the dialog + * @returns {string} + */ + get title() { + return this.options.title + } +} diff --git a/spec/features/admin/edit_elements_feature_spec.rb b/spec/features/admin/edit_elements_feature_spec.rb index 5505115a15..f6edb0ef43 100644 --- a/spec/features/admin/edit_elements_feature_spec.rb +++ b/spec/features/admin/edit_elements_feature_spec.rb @@ -75,8 +75,8 @@ expect(button).to have_content "Add Slide" button.click - expect(page).to have_css(".alchemy-dialog") - within ".alchemy-dialog" do + expect(page).to have_css("sl-dialog") + within "sl-dialog" do expect(page).to have_select("Element") expect(page).to have_css("[panel='paste_element_tab']") end @@ -103,8 +103,8 @@ scenario "the add button now opens add element form with the clipboard tab" do find("a.add-nestable-element-button").click - expect(page).to have_css(".alchemy-dialog") - within ".alchemy-dialog" do + expect(page).to have_css("sl-dialog") + within "sl-dialog" do expect(page).to have_select("Element") expect(page).to have_css("[panel='paste_element_tab']") end @@ -126,8 +126,8 @@ button = page.find(".add-nestable-element-button") expect(button).to have_content "New element" button.click - expect(page).to have_css(".alchemy-dialog") - within ".alchemy-dialog" do + expect(page).to have_css("sl-dialog") + within "sl-dialog" do expect(page).to have_select("Element") select2("Text", from: "Element") click_button("Add") diff --git a/spec/features/admin/page_editing_feature_spec.rb b/spec/features/admin/page_editing_feature_spec.rb index 0c0285bebb..a431f6e748 100644 --- a/spec/features/admin/page_editing_feature_spec.rb +++ b/spec/features/admin/page_editing_feature_spec.rb @@ -65,12 +65,12 @@ visit alchemy.edit_admin_page_path(a_page) expect(page).to have_link_with_tooltip("New element") click_link_with_tooltip("New element") - expect(page).to have_selector(".alchemy-dialog-body .simple_form") - within ".alchemy-dialog-body .simple_form" do + expect(page).to have_selector("sl-dialog .simple_form") + within "sl-dialog .simple_form" do select2("Article", from: "Element") click_button("Add") end - expect(page).to_not have_selector(".alchemy-dialog-body") + expect(page).to_not have_selector("sl-dialog") expect(page).to have_selector('.element-editor[data-element-name="article"]') end end @@ -221,16 +221,16 @@ within ".sitemap_page[name='#{a_page.name}']" do click_icon("settings-3") end - expect(page).to have_selector(".alchemy-dialog-overlay.open") + expect(page).to have_selector("sl-dialog[open]") end context "when updating the name" do it "saves the name" do - within(".alchemy-dialog.modal") do + within("sl-dialog") do find("input#page_name").set("name with some %!x^)'([@!{}]|/?:# characters") find(".edit_page .submit button").click end - expect(page).to_not have_selector(".alchemy-dialog-overlay.open") + expect(page).to_not have_selector("sl-dialog[open]") expect(page).to have_selector("#sitemap a.sitemap_pagename_link", text: "name with some %!x^)'([@!{}]|/?:# characters") end end @@ -239,12 +239,12 @@ let!(:new_parent) { create(:alchemy_page) } it "can change page parent" do - within(".alchemy-dialog.modal") do + within(".simple_form:first-child") do expect(page).to have_css("#s2id_page_parent_id") select2_search(new_parent.name, from: "Parent") find(".edit_page .submit button").click end - expect(page).to_not have_selector(".alchemy-dialog-overlay.open") + expect(page).to_not have_selector("sl-dialog[open]") expect(page).to have_selector("#sitemap .sitemap_url", text: "/#{new_parent.urlname}/#{a_page.urlname}") end end diff --git a/spec/javascript/alchemy_admin/components/dialog_link.spec.js b/spec/javascript/alchemy_admin/components/dialog_link.spec.js index e1ee03c5df..2a687ab6eb 100644 --- a/spec/javascript/alchemy_admin/components/dialog_link.spec.js +++ b/spec/javascript/alchemy_admin/components/dialog_link.spec.js @@ -1,28 +1,18 @@ import "alchemy_admin/components/dialog_link" -import { DEFAULTS } from "alchemy_admin/components/dialog_link" import { renderComponent } from "./component.helper" - -class Dialog { - open() {} -} - -beforeEach(() => { - global.Alchemy = { - Dialog: Dialog - } -}) +import { Dialog } from "alchemy_admin/dialog" describe("alchemy-dialog-link", () => { it("opens a dialog on click", () => { const html = ` Open Dialog ` - const openSpy = jest.spyOn(Dialog.prototype, "open") + Dialog.prototype.open = jest.fn() const dialogLink = renderComponent("alchemy-dialog-link", html) const click = new Event("click", { bubbles: true }) dialogLink.dispatchEvent(click) - expect(openSpy).toHaveBeenCalled() + expect(Dialog.prototype.open).toHaveBeenCalled() }) it("has default dialogOptions", () => { @@ -31,7 +21,7 @@ describe("alchemy-dialog-link", () => { ` const dialogLink = renderComponent("alchemy-dialog-link", html) - expect(dialogLink.dialogOptions).toEqual(DEFAULTS) + expect(dialogLink.dialogOptions).toEqual({}) }) it("parses dialogOptions from dataset", () => {