Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
82ad198
docs: Clarify JavaScript build commands
tvdeyen Apr 20, 2026
12f9ce5
fix(PictureThumbnail): Move DOM mutation out of constructor
tvdeyen Apr 20, 2026
892b0ff
fix(Message): Move DOM mutation out of constructor
tvdeyen Apr 20, 2026
6a14d59
fix(ClipboardButton): Move DOM mutation out of constructor
tvdeyen Apr 20, 2026
17174ea
fix(LinkButton): Move DOM mutation out of constructor
tvdeyen Apr 20, 2026
f45590d
fix(UnlinkButton): Move DOM mutation out of constructor
tvdeyen Apr 20, 2026
936732d
fix(FileEditor): Move child inspection out of constructor
tvdeyen Apr 20, 2026
ad6482e
fix(PictureEditor): Move child inspection out of constructor
tvdeyen Apr 20, 2026
051d358
fix(ElementsWindow): Move event wiring out of constructor
tvdeyen Apr 20, 2026
13569d1
refactor(ElementEditor): Move DOM wiring to connectedCallback
tvdeyen Apr 21, 2026
717ff81
refactor(ElementsWindowHandle): Move listeners to connectedCallback
tvdeyen Apr 21, 2026
d2bc504
refactor(ListFilter): Move DOM and hotkey wiring to connectedCallback
tvdeyen Apr 21, 2026
6b83280
refactor(IngredientGroup): Move wiring to connectedCallback
tvdeyen Apr 21, 2026
0f1af23
refactor(DeleteElementButton): Move click wiring to connectedCallback
tvdeyen Apr 21, 2026
acb3036
refactor(LinkButtons): Move listeners to connectedCallback
tvdeyen Apr 21, 2026
5af9d69
refactor(PublishPageButton): Move submit listener to connectedCallback
tvdeyen Apr 21, 2026
94be79f
refactor(PreviewWindow): Move load listener to connectedCallback
tvdeyen Apr 21, 2026
13618fa
refactor(DialogLink): Move click listener to connectedCallback
tvdeyen Apr 21, 2026
b4773dd
refactor(PictureDescriptionSelect): Move change listener to connected…
tvdeyen Apr 21, 2026
8e918c4
fix(TagsAutocomplete): Destroy select2 on disconnect
tvdeyen Apr 21, 2026
e8f288e
fix(AutoSubmit): Remove jQuery change handler on disconnect
tvdeyen Apr 21, 2026
bf50348
fix(ColorSelect): Destroy select2 and remove change handler on discon…
tvdeyen Apr 21, 2026
94b2351
fix(Select): Destroy select2 on disconnect
tvdeyen Apr 21, 2026
a960b3b
fix(ElementSelect): Destroy select2 on disconnect
tvdeyen Apr 21, 2026
64fb898
fix(PagePublicationFields): Remove click listener on disconnect
tvdeyen Apr 21, 2026
97465c6
fix(styleguide): Use view compoent for tags-autocomplete
tvdeyen Apr 21, 2026
e62fe70
fix(Menubar): Move shadow root setup to connectedCallback
tvdeyen Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ bun run test
bun run build

# Build individual components
bun run build:js # Rollup JavaScript bundling
bun run build:admin # Bundle admin JavaScript (app/javascript/alchemy_admin/**)
bun run build:js # Bundle vendored dependencies (sortablejs, shoelace, tinymce, etc.)
bun run build:css # Sass compilation
bun run handlebars:compile # Compile Handlebars templates
bun run build:icons # Generate icon sprite
Expand Down
2 changes: 1 addition & 1 deletion app/assets/builds/alchemy/alchemy_admin.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion app/assets/builds/alchemy/alchemy_admin.min.js.map

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions app/javascript/alchemy_admin/components/auto_submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ class AutoSubmit extends HTMLElement {
connectedCallback() {
// Still using jQuery here, because select2 does not emit
// the event from the original select element.
$(this).on("change", function (event) {
// We need to dispatch a submit event, so that Turbo that listens
// to it submits the search form us.
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true
})
event.target.form.dispatchEvent(submitEvent)
return false
$(this).on("change", this.#onChange)
}

disconnectedCallback() {
$(this).off("change", this.#onChange)
}

#onChange = (event) => {
// We need to dispatch a submit event, so that Turbo that listens
// to it submits the search form us.
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true
})
event.target.form.dispatchEvent(submitEvent)
return false
}
}

Expand Down
8 changes: 2 additions & 6 deletions app/javascript/alchemy_admin/components/clipboard_button.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import "clipboard"
import { growl } from "alchemy_admin/growler"

class ClipboardButton extends HTMLElement {
constructor() {
super()

this.innerHTML = `
<alchemy-icon name="clipboard"></alchemy-icon>
`
connectedCallback() {
this.innerHTML = '<alchemy-icon name="clipboard"></alchemy-icon>'

this.clipboard = new ClipboardJS(this, {
text: () => {
Expand Down
17 changes: 13 additions & 4 deletions app/javascript/alchemy_admin/components/color_select.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ const formatItem = (object) => {
}

class ColorSelect extends HTMLElement {
#select2 = null

connectedCallback() {
if (this.select) {
this.#initializeSelect2()
$(this.select).on("change", (event) =>
this.#toggleColorPicker(event.val === "custom_color")
)
this.#select2.on("change", this.#onSelectChange)
} else {
this.colorInput?.addEventListener("input", this)
this.textInput?.addEventListener("input", this)
Expand All @@ -41,6 +41,15 @@ class ColorSelect extends HTMLElement {
disconnectedCallback() {
this.colorInput?.removeEventListener("input", this)
this.textInput?.removeEventListener("input", this)
if (this.#select2) {
this.#select2.off("change", this.#onSelectChange)
this.#select2.select2("destroy")
this.#select2 = null
}
}

#onSelectChange = (event) => {
this.#toggleColorPicker(event.val === "custom_color")
}

#initializeSelect2() {
Expand All @@ -50,7 +59,7 @@ class ColorSelect extends HTMLElement {
formatResult: formatItem,
formatSelection: formatItem
}
$(this.select).select2(options)
this.#select2 = $(this.select).select2(options)
}

#toggleColorPicker(enabled = true) {
Expand Down
7 changes: 5 additions & 2 deletions app/javascript/alchemy_admin/components/dialog_link.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Dialog } from "alchemy_admin/dialog"

export class DialogLink extends HTMLAnchorElement {
constructor() {
super()
connectedCallback() {
this.addEventListener("click", this)
}

disconnectedCallback() {
this.removeEventListener("click", this)
}

handleEvent(evt) {
if (!this.disabled) {
this.openDialog()
Expand Down
64 changes: 44 additions & 20 deletions app/javascript/alchemy_admin/components/element_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ export function dispatchPageDirtyEvent(data) {
}

export class ElementEditor extends HTMLElement {
constructor() {
super()
#form = null
#header = null
#toggleButton = null

connectedCallback() {
// The placeholder while be being dragged is empty.
if (this.classList.contains("ui-sortable-placeholder")) {
return
}

// Add event listeners
this.addEventListener("click", this)
Expand All @@ -27,24 +34,16 @@ export class ElementEditor extends HTMLElement {

// Dirty observer still needs to be jQuery
// in order to support select2.
$(this.form).on("change", this.onChange)
this.#form = this.form
if (this.#form) {
$(this.#form).on("change", this.onChange)
}

this.header?.addEventListener("dblclick", () => {
this.toggle()
})
this.toggleButton?.addEventListener("click", (evt) => {
const elementEditor = evt.target.closest("alchemy-element-editor")
if (elementEditor === this) {
this.toggle()
}
})
}
this.#header = this.header
this.#header?.addEventListener("dblclick", this.#onHeaderDblclick)

connectedCallback() {
// The placeholder while be being dragged is empty.
if (this.classList.contains("ui-sortable-placeholder")) {
return
}
this.#toggleButton = this.toggleButton
this.#toggleButton?.addEventListener("click", this.#onToggleClick)

// When newly created, focus the element and refresh the preview
if (this.hasAttribute("created")) {
Expand All @@ -56,6 +55,20 @@ export class ElementEditor extends HTMLElement {
}
}

disconnectedCallback() {
this.removeEventListener("click", this)
this.removeEventListener("alchemy:element-update-title", this)
this.removeEventListener("ajax:complete", this)
if (this.#form) {
$(this.#form).off("change", this.onChange)
this.#form = null
}
this.#header?.removeEventListener("dblclick", this.#onHeaderDblclick)
this.#header = null
this.#toggleButton?.removeEventListener("click", this.#onToggleClick)
this.#toggleButton = null
}

handleEvent(event) {
switch (event.type) {
case "click":
Expand All @@ -79,15 +92,15 @@ export class ElementEditor extends HTMLElement {
}
}

onChange(event) {
onChange = (event) => {
const target = event.target
// SortableJS fires a native change event :/
// and we do not want to set the element editor dirty
// when this happens
if (target.classList.contains("nested-elements")) {
return
}
this.closest("alchemy-element-editor").setDirty(target)
this.setDirty(target)
event.stopPropagation()
return false
}
Expand Down Expand Up @@ -576,6 +589,17 @@ export class ElementEditor extends HTMLElement {
get previewWindow() {
return document.getElementById("alchemy_preview_window")
}

#onHeaderDblclick = () => {
this.toggle()
}

#onToggleClick = (evt) => {
const elementEditor = evt.target.closest("alchemy-element-editor")
if (elementEditor === this) {
this.toggle()
}
}
}

customElements.define("alchemy-element-editor", ElementEditor)
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import { openConfirmDialog } from "alchemy_admin/confirm_dialog"
import { dispatchPageDirtyEvent } from "alchemy_admin/components/element_editor"

export class DeleteElementButton extends HTMLElement {
constructor() {
super()
connectedCallback() {
this.button?.addEventListener("click", this)
}

disconnectedCallback() {
this.button?.removeEventListener("click", this)
}

async handleEvent() {
const confirmed = await openConfirmDialog(this.message)
if (confirmed) {
Expand Down
11 changes: 7 additions & 4 deletions app/javascript/alchemy_admin/components/element_select.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ const formatItem = (icon, text, hint) => {
}

class ElementSelect extends HTMLElement {
constructor() {
super()
}
#select2 = null

connectedCallback() {
const results = this.options
Expand All @@ -48,7 +46,12 @@ class ElementSelect extends HTMLElement {
formatSelection,
placeholder: this.placeholder
}
$(this.inputField).select2(options)
this.#select2 = $(this.inputField).select2(options)
}

disconnectedCallback() {
this.#select2?.select2("destroy")
this.#select2 = null
}

get options() {
Expand Down
69 changes: 38 additions & 31 deletions app/javascript/alchemy_admin/components/elements_window.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,51 @@ class ElementsWindow extends HTMLElement {
#visible = true
#turboFrame = null

constructor() {
super()
this.#attachEvents()
}

connectedCallback() {
this.toggleButton?.addEventListener("click", (evt) => {
evt.preventDefault()
this.toggle()
})
this.toggleButton?.addEventListener("click", this.#onToggleClick)
this.collapseButton?.addEventListener("click", this.#onCollapseClick)
window.addEventListener("message", this.#onWindowMessage)
document.body.addEventListener("click", this.#onBodyClick)
if (window.location.hash) {
this.focusElementEditor(window.location.hash)
}
this.resize()
}

disconnectedCallback() {
this.toggleButton?.removeEventListener("click", this.#onToggleClick)
this.collapseButton?.removeEventListener("click", this.#onCollapseClick)
window.removeEventListener("message", this.#onWindowMessage)
document.body.removeEventListener("click", this.#onBodyClick)
}

#onToggleClick = (evt) => {
evt.preventDefault()
this.toggle()
}

#onCollapseClick = () => {
this.collapseAllElements()
}

#onWindowMessage = (event) => {
const data = event.data
if (data?.message == "Alchemy.focusElementEditor") {
const element = document.getElementById(`element_${data.element_id}`)
this.show()
element?.focusElement()
}
}

#onBodyClick = (evt) => {
if (!evt.target.closest("alchemy-element-editor")) {
this.querySelectorAll("alchemy-element-editor").forEach((editor) => {
editor.classList.remove("selected")
})
this.previewWindow?.postMessage({ message: "Alchemy.blurElements" })
}
}

collapseAllElements() {
this.querySelectorAll(
"alchemy-element-editor:not([compact]):not([fixed])"
Expand Down Expand Up @@ -101,28 +130,6 @@ class ElementsWindow extends HTMLElement {
this.turboFrame.style.transitionProperty = dragged ? "none" : null
this.turboFrame.style.pointerEvents = dragged ? "none" : null
}

#attachEvents() {
this.collapseButton?.addEventListener("click", () => {
this.collapseAllElements()
})
window.addEventListener("message", (event) => {
const data = event.data
if (data?.message == "Alchemy.focusElementEditor") {
const element = document.getElementById(`element_${data.element_id}`)
this.show()
element?.focusElement()
}
})
document.body.addEventListener("click", (evt) => {
if (!evt.target.closest("alchemy-element-editor")) {
this.querySelectorAll("alchemy-element-editor").forEach((editor) => {
editor.classList.remove("selected")
})
this.previewWindow?.postMessage({ message: "Alchemy.blurElements" })
}
})
}
}

customElements.define("alchemy-elements-window", ElementsWindow)
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ class ElementsWindowHandle extends HTMLElement {
#elementsWindow = null
#previewWindow = null

constructor() {
super()

connectedCallback() {
this.addEventListener("mousedown", this)
window.addEventListener("mousemove", this)
window.addEventListener("mouseup", this)
}

disconnectedCallback() {
this.removeEventListener("mousedown", this)
window.removeEventListener("mousemove", this)
window.removeEventListener("mouseup", this)
}

handleEvent(event) {
switch (event.type) {
case "mousedown":
Expand Down
7 changes: 5 additions & 2 deletions app/javascript/alchemy_admin/components/file_editor.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
class FileEditor extends HTMLElement {
constructor() {
super()
connectedCallback() {
this.deleteLink = this.querySelector(".remove_file_link")
this.fileIcon = this.querySelector(".file_icon")
this.fileName = this.querySelector(".file_name")
Expand All @@ -9,6 +8,10 @@ class FileEditor extends HTMLElement {
this.deleteLink?.addEventListener("click", this)
}

disconnectedCallback() {
this.deleteLink?.removeEventListener("click", this)
}

handleEvent(event) {
if (event.type === "click") this.removeFile()
event.stopPropagation()
Expand Down
Loading
Loading