alchemy_cms 8.2.7 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -1
  3. data/app/assets/builds/alchemy/admin.css +1 -1
  4. data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
  5. data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
  6. data/app/assets/builds/alchemy/dark-theme.css +1 -1
  7. data/app/assets/builds/alchemy/light-theme.css +1 -1
  8. data/app/assets/builds/alchemy/preview.min.js +1 -1
  9. data/app/assets/builds/alchemy/theme.css +1 -1
  10. data/app/assets/builds/alchemy/welcome.css +1 -1
  11. data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
  12. data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
  13. data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
  14. data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
  15. data/app/assets/images/alchemy/admin/logo.svg +27 -0
  16. data/app/assets/images/alchemy/icons-sprite.svg +1 -1
  17. data/app/components/alchemy/admin/dashboard/widget.rb +40 -0
  18. data/app/components/alchemy/admin/dashboard/widgets/attachment_counts.rb +17 -0
  19. data/app/components/alchemy/admin/dashboard/widgets/element_usage.rb +37 -0
  20. data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +13 -0
  21. data/app/components/alchemy/admin/dashboard/widgets/greeting.rb +21 -0
  22. data/app/components/alchemy/admin/dashboard/widgets/locked_pages.html.erb +54 -0
  23. data/app/components/alchemy/admin/dashboard/widgets/locked_pages.rb +20 -0
  24. data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +22 -0
  25. data/app/components/alchemy/admin/dashboard/widgets/online_users.rb +19 -0
  26. data/app/components/alchemy/admin/dashboard/widgets/page_counts.rb +23 -0
  27. data/app/components/alchemy/admin/dashboard/widgets/page_usage.rb +46 -0
  28. data/app/components/alchemy/admin/dashboard/widgets/picture_counts.rb +17 -0
  29. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +41 -0
  30. data/app/components/alchemy/admin/dashboard/widgets/recent_pages.rb +16 -0
  31. data/app/components/alchemy/admin/dashboard/widgets/sites.html.erb +29 -0
  32. data/app/components/alchemy/admin/dashboard/widgets/sites.rb +15 -0
  33. data/app/components/alchemy/admin/dashboard/widgets/stat_widget.html.erb +23 -0
  34. data/app/components/alchemy/admin/dashboard/widgets/stat_widget.rb +19 -0
  35. data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +32 -0
  36. data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +37 -0
  37. data/app/components/alchemy/admin/dashboard/widgets/usage_widget.html.erb +42 -0
  38. data/app/components/alchemy/admin/dashboard/widgets/usage_widget.rb +66 -0
  39. data/app/components/alchemy/admin/dashboard/widgets/user_counts.rb +25 -0
  40. data/app/components/alchemy/admin/element_editor.html.erb +27 -20
  41. data/app/components/alchemy/admin/element_schedule_timestamps.rb +33 -0
  42. data/app/components/alchemy/admin/element_select.rb +4 -3
  43. data/app/components/alchemy/admin/page_node.html.erb +1 -20
  44. data/app/components/alchemy/admin/page_publication_fields.html.erb +30 -0
  45. data/app/components/alchemy/admin/page_publication_fields.rb +18 -0
  46. data/app/components/alchemy/admin/page_status_indicators.html.erb +29 -0
  47. data/app/components/alchemy/admin/page_status_indicators.rb +9 -0
  48. data/app/components/alchemy/admin/publish_element_button.html.erb +12 -4
  49. data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
  50. data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
  51. data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
  52. data/app/controllers/alchemy/pages_controller.rb +5 -4
  53. data/app/helpers/alchemy/elements_block_helper.rb +1 -0
  54. data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
  55. data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
  56. data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
  57. data/app/javascript/alchemy_admin/components/color_select.js +13 -4
  58. data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
  59. data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
  60. data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
  61. data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
  62. data/app/javascript/alchemy_admin/components/element_select.js +7 -4
  63. data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
  64. data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
  65. data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
  66. data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
  67. data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
  68. data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
  69. data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
  70. data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
  71. data/app/javascript/alchemy_admin/components/message.js +22 -15
  72. data/app/javascript/alchemy_admin/components/overlay.js +5 -7
  73. data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
  74. data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
  75. data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
  76. data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
  77. data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
  78. data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
  79. data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
  80. data/app/javascript/alchemy_admin/components/select.js +169 -26
  81. data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
  82. data/app/javascript/alchemy_admin/components/spinner.js +11 -11
  83. data/app/javascript/alchemy_admin/components/tags_autocomplete.js +9 -1
  84. data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
  85. data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
  86. data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
  87. data/app/javascript/alchemy_admin/components/uploader.js +71 -46
  88. data/app/javascript/alchemy_admin/dialog.js +3 -0
  89. data/app/javascript/alchemy_admin/hotkeys.js +0 -18
  90. data/app/javascript/alchemy_admin/image_cropper.js +7 -9
  91. data/app/javascript/alchemy_admin/initializer.js +21 -0
  92. data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
  93. data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
  94. data/app/jobs/alchemy/base_job.rb +2 -2
  95. data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
  96. data/app/models/alchemy/page/page_naming.rb +28 -5
  97. data/app/models/alchemy/page/page_natures.rb +7 -2
  98. data/app/models/alchemy/page/page_scopes.rb +2 -2
  99. data/app/models/alchemy/page/url_path.rb +7 -2
  100. data/app/models/alchemy/page.rb +2 -2
  101. data/app/models/alchemy/page_definition.rb +1 -0
  102. data/app/models/alchemy/permissions.rb +1 -1
  103. data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
  104. data/app/services/alchemy/page_finder.rb +88 -0
  105. data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
  106. data/app/stylesheets/alchemy/_mixins.scss +1 -7
  107. data/app/stylesheets/alchemy/_themes.scss +13 -1
  108. data/app/stylesheets/alchemy/admin/_tom-select.scss +240 -0
  109. data/app/stylesheets/alchemy/admin/archive.scss +0 -1
  110. data/app/stylesheets/alchemy/admin/base.scss +0 -19
  111. data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
  112. data/app/stylesheets/alchemy/admin/elements.scss +14 -17
  113. data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
  114. data/app/stylesheets/alchemy/admin/forms.scss +107 -93
  115. data/app/stylesheets/alchemy/admin/icons.scss +28 -0
  116. data/app/stylesheets/alchemy/admin/image_library.scss +20 -10
  117. data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
  118. data/app/stylesheets/alchemy/admin/popover.scss +3 -5
  119. data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
  120. data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
  121. data/app/stylesheets/alchemy/admin/sitemap.scss +5 -0
  122. data/app/stylesheets/alchemy/admin/tables.scss +32 -3
  123. data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
  124. data/app/stylesheets/alchemy/admin.scss +1 -0
  125. data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
  126. data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
  127. data/app/types/alchemy/wildcard_url_type.rb +48 -0
  128. data/app/views/alchemy/_menubar.html.erb +1 -5
  129. data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
  130. data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
  131. data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
  132. data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
  133. data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
  134. data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
  135. data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
  136. data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
  137. data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
  138. data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
  139. data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
  140. data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
  141. data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
  142. data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +10 -8
  143. data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
  144. data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
  145. data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
  146. data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
  147. data/app/views/alchemy/admin/pages/info.html.erb +6 -0
  148. data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
  149. data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
  150. data/app/views/alchemy/admin/resources/new.html.erb +3 -1
  151. data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
  152. data/app/views/alchemy/admin/translations/_en.js +4 -0
  153. data/app/views/layouts/alchemy/admin.html.erb +3 -3
  154. data/config/importmap.rb +2 -0
  155. data/config/locales/alchemy.en.yml +15 -0
  156. data/config/routes.rb +1 -0
  157. data/lib/alchemy/configuration/class_option.rb +46 -3
  158. data/lib/alchemy/configuration/collection_option.rb +4 -0
  159. data/lib/alchemy/configurations/dashboard.rb +79 -0
  160. data/lib/alchemy/configurations/main.rb +15 -0
  161. data/lib/alchemy/engine.rb +9 -3
  162. data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
  163. data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
  164. data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
  165. data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
  166. data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -31
  167. data/lib/alchemy/tinymce.rb +1 -1
  168. data/lib/alchemy/version.rb +17 -3
  169. data/vendor/javascript/cropperjs.min.js +1 -1
  170. data/vendor/javascript/flatpickr.min.js +1 -1
  171. data/vendor/javascript/floating-ui.min.js +1 -0
  172. data/vendor/javascript/keymaster.min.js +1 -1
  173. data/vendor/javascript/rails-ujs.min.js +1 -1
  174. data/vendor/javascript/shoelace.min.js +93 -93
  175. data/vendor/javascript/sortable.min.js +1 -1
  176. data/vendor/javascript/tinymce.min.js +5 -1
  177. data/vendor/javascript/tom-select.min.js +1 -0
  178. metadata +57 -18
  179. data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
  180. data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
  181. data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
  182. data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
  183. data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
  184. data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
  185. data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
  186. data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
  187. data/app/views/alchemy/admin/languages/new.html.erb +0 -1
  188. data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
  189. data/app/views/alchemy/admin/sites/new.html.erb +0 -1
@@ -1,22 +1,22 @@
1
1
  import IngredientAnchorLink from "alchemy_admin/ingredient_anchor_link"
2
2
  import { post } from "alchemy_admin/utils/ajax"
3
3
  import { createHtmlElement } from "alchemy_admin/utils/dom_helpers"
4
+ import { dispatchPageDirtyEvent } from "alchemy_admin/utils/dispatch_page_dirty_event"
4
5
  import { growl } from "alchemy_admin/growler"
5
6
 
6
7
  import "alchemy_admin/components/element_editor/publish_element_button"
7
8
  import "alchemy_admin/components/element_editor/delete_element_button"
8
9
 
9
- export function dispatchPageDirtyEvent(data) {
10
- document.dispatchEvent(
11
- new CustomEvent("alchemy:page-dirty", {
12
- detail: { tooltip: data.publishButtonTooltip }
13
- })
14
- )
15
- }
16
-
17
10
  export class ElementEditor extends HTMLElement {
18
- constructor() {
19
- super()
11
+ #form = null
12
+ #header = null
13
+ #toggleButton = null
14
+
15
+ connectedCallback() {
16
+ // The placeholder while be being dragged is empty.
17
+ if (this.classList.contains("ui-sortable-placeholder")) {
18
+ return
19
+ }
20
20
 
21
21
  // Add event listeners
22
22
  this.addEventListener("click", this)
@@ -27,24 +27,16 @@ export class ElementEditor extends HTMLElement {
27
27
 
28
28
  // Dirty observer still needs to be jQuery
29
29
  // in order to support select2.
30
- $(this.form).on("change", this.onChange)
30
+ this.#form = this.form
31
+ if (this.#form) {
32
+ $(this.#form).on("change", this.onChange)
33
+ }
31
34
 
32
- this.header?.addEventListener("dblclick", () => {
33
- this.toggle()
34
- })
35
- this.toggleButton?.addEventListener("click", (evt) => {
36
- const elementEditor = evt.target.closest("alchemy-element-editor")
37
- if (elementEditor === this) {
38
- this.toggle()
39
- }
40
- })
41
- }
35
+ this.#header = this.header
36
+ this.#header?.addEventListener("dblclick", this.#onHeaderDblclick)
42
37
 
43
- connectedCallback() {
44
- // The placeholder while be being dragged is empty.
45
- if (this.classList.contains("ui-sortable-placeholder")) {
46
- return
47
- }
38
+ this.#toggleButton = this.toggleButton
39
+ this.#toggleButton?.addEventListener("click", this.#onToggleClick)
48
40
 
49
41
  // When newly created, focus the element and refresh the preview
50
42
  if (this.hasAttribute("created")) {
@@ -56,6 +48,20 @@ export class ElementEditor extends HTMLElement {
56
48
  }
57
49
  }
58
50
 
51
+ disconnectedCallback() {
52
+ this.removeEventListener("click", this)
53
+ this.removeEventListener("alchemy:element-update-title", this)
54
+ this.removeEventListener("ajax:complete", this)
55
+ if (this.#form) {
56
+ $(this.#form).off("change", this.onChange)
57
+ this.#form = null
58
+ }
59
+ this.#header?.removeEventListener("dblclick", this.#onHeaderDblclick)
60
+ this.#header = null
61
+ this.#toggleButton?.removeEventListener("click", this.#onToggleClick)
62
+ this.#toggleButton = null
63
+ }
64
+
59
65
  handleEvent(event) {
60
66
  switch (event.type) {
61
67
  case "click":
@@ -79,7 +85,7 @@ export class ElementEditor extends HTMLElement {
79
85
  }
80
86
  }
81
87
 
82
- onChange(event) {
88
+ onChange = (event) => {
83
89
  const target = event.target
84
90
  // SortableJS fires a native change event :/
85
91
  // and we do not want to set the element editor dirty
@@ -87,7 +93,7 @@ export class ElementEditor extends HTMLElement {
87
93
  if (target.classList.contains("nested-elements")) {
88
94
  return
89
95
  }
90
- this.closest("alchemy-element-editor").setDirty(target)
96
+ this.setDirty(target)
91
97
  event.stopPropagation()
92
98
  return false
93
99
  }
@@ -579,6 +585,17 @@ export class ElementEditor extends HTMLElement {
579
585
  get previewWindow() {
580
586
  return document.getElementById("alchemy_preview_window")
581
587
  }
588
+
589
+ #onHeaderDblclick = () => {
590
+ this.toggle()
591
+ }
592
+
593
+ #onToggleClick = (evt) => {
594
+ const elementEditor = evt.target.closest("alchemy-element-editor")
595
+ if (elementEditor === this) {
596
+ this.toggle()
597
+ }
598
+ }
582
599
  }
583
600
 
584
601
  customElements.define("alchemy-element-editor", ElementEditor)
@@ -21,9 +21,7 @@ const formatItem = (icon, text, hint) => {
21
21
  }
22
22
 
23
23
  class ElementSelect extends HTMLElement {
24
- constructor() {
25
- super()
26
- }
24
+ #select2 = null
27
25
 
28
26
  connectedCallback() {
29
27
  const results = this.options
@@ -48,7 +46,12 @@ class ElementSelect extends HTMLElement {
48
46
  formatSelection,
49
47
  placeholder: this.placeholder
50
48
  }
51
- $(this.inputField).select2(options)
49
+ this.#select2 = $(this.inputField).select2(options)
50
+ }
51
+
52
+ disconnectedCallback() {
53
+ this.#select2?.select2("destroy")
54
+ this.#select2 = null
52
55
  }
53
56
 
54
57
  get options() {
@@ -4,22 +4,51 @@ class ElementsWindow extends HTMLElement {
4
4
  #visible = true
5
5
  #turboFrame = null
6
6
 
7
- constructor() {
8
- super()
9
- this.#attachEvents()
10
- }
11
-
12
7
  connectedCallback() {
13
- this.toggleButton?.addEventListener("click", (evt) => {
14
- evt.preventDefault()
15
- this.toggle()
16
- })
8
+ this.toggleButton?.addEventListener("click", this.#onToggleClick)
9
+ this.collapseButton?.addEventListener("click", this.#onCollapseClick)
10
+ window.addEventListener("message", this.#onWindowMessage)
11
+ document.body.addEventListener("click", this.#onBodyClick)
17
12
  if (window.location.hash) {
18
13
  this.focusElementEditor(window.location.hash)
19
14
  }
20
15
  this.resize()
21
16
  }
22
17
 
18
+ disconnectedCallback() {
19
+ this.toggleButton?.removeEventListener("click", this.#onToggleClick)
20
+ this.collapseButton?.removeEventListener("click", this.#onCollapseClick)
21
+ window.removeEventListener("message", this.#onWindowMessage)
22
+ document.body.removeEventListener("click", this.#onBodyClick)
23
+ }
24
+
25
+ #onToggleClick = (evt) => {
26
+ evt.preventDefault()
27
+ this.toggle()
28
+ }
29
+
30
+ #onCollapseClick = () => {
31
+ this.collapseAllElements()
32
+ }
33
+
34
+ #onWindowMessage = (event) => {
35
+ const data = event.data
36
+ if (data?.message == "Alchemy.focusElementEditor") {
37
+ const element = document.getElementById(`element_${data.element_id}`)
38
+ this.show()
39
+ element?.focusElement()
40
+ }
41
+ }
42
+
43
+ #onBodyClick = (evt) => {
44
+ if (!evt.target.closest("alchemy-element-editor")) {
45
+ this.querySelectorAll("alchemy-element-editor").forEach((editor) => {
46
+ editor.classList.remove("selected")
47
+ })
48
+ this.previewWindow?.postMessage({ message: "Alchemy.blurElements" })
49
+ }
50
+ }
51
+
23
52
  collapseAllElements() {
24
53
  this.querySelectorAll(
25
54
  "alchemy-element-editor:not([compact]):not([fixed])"
@@ -101,28 +130,6 @@ class ElementsWindow extends HTMLElement {
101
130
  this.turboFrame.style.transitionProperty = dragged ? "none" : null
102
131
  this.turboFrame.style.pointerEvents = dragged ? "none" : null
103
132
  }
104
-
105
- #attachEvents() {
106
- this.collapseButton?.addEventListener("click", () => {
107
- this.collapseAllElements()
108
- })
109
- window.addEventListener("message", (event) => {
110
- const data = event.data
111
- if (data?.message == "Alchemy.focusElementEditor") {
112
- const element = document.getElementById(`element_${data.element_id}`)
113
- this.show()
114
- element?.focusElement()
115
- }
116
- })
117
- document.body.addEventListener("click", (evt) => {
118
- if (!evt.target.closest("alchemy-element-editor")) {
119
- this.querySelectorAll("alchemy-element-editor").forEach((editor) => {
120
- editor.classList.remove("selected")
121
- })
122
- this.previewWindow?.postMessage({ message: "Alchemy.blurElements" })
123
- }
124
- })
125
- }
126
133
  }
127
134
 
128
135
  customElements.define("alchemy-elements-window", ElementsWindow)
@@ -7,14 +7,18 @@ class ElementsWindowHandle extends HTMLElement {
7
7
  #minWidth = MIN_WIDTH
8
8
  #maxWidth = MAX_WIDTH
9
9
 
10
- constructor() {
11
- super()
12
-
10
+ connectedCallback() {
13
11
  this.addEventListener("mousedown", this)
14
12
  window.addEventListener("mousemove", this)
15
13
  window.addEventListener("mouseup", this)
16
14
  }
17
15
 
16
+ disconnectedCallback() {
17
+ this.removeEventListener("mousedown", this)
18
+ window.removeEventListener("mousemove", this)
19
+ window.removeEventListener("mouseup", this)
20
+ }
21
+
18
22
  handleEvent(event) {
19
23
  switch (event.type) {
20
24
  case "mousedown":
@@ -1,6 +1,5 @@
1
1
  class FileEditor extends HTMLElement {
2
- constructor() {
3
- super()
2
+ connectedCallback() {
4
3
  this.deleteLink = this.querySelector(".remove_file_link")
5
4
  this.fileIcon = this.querySelector(".file_icon")
6
5
  this.fileName = this.querySelector(".file_name")
@@ -9,6 +8,10 @@ class FileEditor extends HTMLElement {
9
8
  this.deleteLink?.addEventListener("click", this)
10
9
  }
11
10
 
11
+ disconnectedCallback() {
12
+ this.deleteLink?.removeEventListener("click", this)
13
+ }
14
+
12
15
  handleEvent(event) {
13
16
  if (event.type === "click") this.removeFile()
14
17
  event.stopPropagation()
@@ -1,9 +1,7 @@
1
1
  export class IngredientGroup extends HTMLDetailsElement {
2
2
  #localStorageKey = "Alchemy.expanded_ingredient_groups"
3
3
 
4
- constructor() {
5
- super()
6
-
4
+ connectedCallback() {
7
5
  this.addEventListener("toggle", this)
8
6
 
9
7
  if (this.isInLocalStorage) {
@@ -11,6 +9,10 @@ export class IngredientGroup extends HTMLDetailsElement {
11
9
  }
12
10
  }
13
11
 
12
+ disconnectedCallback() {
13
+ this.removeEventListener("toggle", this)
14
+ }
15
+
14
16
  /**
15
17
  * Toggle visibility of the ingredient fields in this group
16
18
  */
@@ -21,7 +23,7 @@ export class IngredientGroup extends HTMLDetailsElement {
21
23
  this.toggleIcon.name = "arrow-down-s"
22
24
  if (!this.isInLocalStorage) expanded_ingredient_groups.push(this.id)
23
25
  } else {
24
- this.toggleIcon.name = "arrow-left-s"
26
+ this.toggleIcon.name = "arrow-right-s"
25
27
  expanded_ingredient_groups = expanded_ingredient_groups.filter(
26
28
  (value) => value !== this.id
27
29
  )
@@ -1,6 +1,5 @@
1
1
  class LinkButton extends HTMLButtonElement {
2
- constructor() {
3
- super()
2
+ connectedCallback() {
4
3
  this.addEventListener("click", this)
5
4
  this.classList.add("icon_button")
6
5
  // Prevent accidental form submits if this component is wrapped inside a form
@@ -1,6 +1,5 @@
1
1
  class UnlinkButton extends HTMLButtonElement {
2
- constructor() {
3
- super()
2
+ connectedCallback() {
4
3
  this.addEventListener("click", this)
5
4
  this.classList.add("icon_button")
6
5
  // Prevent accidental form submits if this component is wrapped inside a form
@@ -2,12 +2,16 @@ import "alchemy_admin/components/link_buttons/link_button"
2
2
  import "alchemy_admin/components/link_buttons/unlink_button"
3
3
 
4
4
  class LinkButtons extends HTMLElement {
5
- constructor() {
6
- super()
5
+ connectedCallback() {
7
6
  this.addEventListener("alchemy:link", this)
8
7
  this.addEventListener("alchemy:unlink", this)
9
8
  }
10
9
 
10
+ disconnectedCallback() {
11
+ this.removeEventListener("alchemy:link", this)
12
+ this.removeEventListener("alchemy:unlink", this)
13
+ }
14
+
11
15
  handleEvent(event) {
12
16
  switch (event.type) {
13
17
  case "alchemy:link":
@@ -2,41 +2,30 @@ const DEFAULT_DEBOUNCE_TIME = 150
2
2
 
3
3
  class ListFilter extends HTMLElement {
4
4
  #debounceTimer
5
+ #filterField = null
6
+ #clearButton = null
5
7
 
6
- constructor() {
7
- super()
8
- this.#attachEvents()
9
- }
10
-
11
- #attachEvents() {
8
+ connectedCallback() {
12
9
  if (this.hotkey) {
13
- key(this.hotkey, () => {
14
- this.filterField.focus()
15
- return false
16
- })
10
+ key(this.hotkey, this.#onHotkey)
17
11
  }
18
- this.filterField.addEventListener("keyup", () => {
19
- clearTimeout(this.#debounceTimer)
20
- this.#debounceTimer = setTimeout(() => {
21
- const term = this.filterField.value
22
- this.clearButton.style.visibility = term ? "visible" : "hidden"
23
- this.filter(term)
24
- }, this.debounceTime)
25
- })
26
- this.clearButton.addEventListener("click", (e) => {
27
- e.preventDefault()
28
- this.clear()
29
- })
30
- this.filterField.addEventListener("focus", () =>
31
- key.setScope("list_filter")
32
- )
33
- key("esc", "list_filter", () => {
34
- this.clear()
35
- this.filterField.blur()
36
- })
12
+ this.#filterField = this.filterField
13
+ this.#filterField.addEventListener("keyup", this.#onKeyup)
14
+ this.#filterField.addEventListener("focus", this.#onFocus)
15
+
16
+ this.#clearButton = this.clearButton
17
+ this.#clearButton.addEventListener("click", this.#onClearClick)
18
+
19
+ key("esc", "list_filter", this.#onEscape)
37
20
  }
38
21
 
39
22
  disconnectedCallback() {
23
+ clearTimeout(this.#debounceTimer)
24
+ this.#filterField?.removeEventListener("keyup", this.#onKeyup)
25
+ this.#filterField?.removeEventListener("focus", this.#onFocus)
26
+ this.#filterField = null
27
+ this.#clearButton?.removeEventListener("click", this.#onClearClick)
28
+ this.#clearButton = null
40
29
  if (this.hotkey) {
41
30
  key.unbind(this.hotkey)
42
31
  }
@@ -112,6 +101,32 @@ class ListFilter extends HTMLElement {
112
101
  get hotkey() {
113
102
  return this.getAttribute("hotkey")
114
103
  }
104
+
105
+ #onHotkey = () => {
106
+ this.filterField.focus()
107
+ return false
108
+ }
109
+
110
+ #onKeyup = () => {
111
+ clearTimeout(this.#debounceTimer)
112
+ this.#debounceTimer = setTimeout(() => {
113
+ const term = this.filterField.value
114
+ this.clearButton.style.visibility = term ? "visible" : "hidden"
115
+ this.filter(term)
116
+ }, this.debounceTime)
117
+ }
118
+
119
+ #onFocus = () => key.setScope("list_filter")
120
+
121
+ #onClearClick = (e) => {
122
+ e.preventDefault()
123
+ this.clear()
124
+ }
125
+
126
+ #onEscape = () => {
127
+ this.clear()
128
+ this.filterField.blur()
129
+ }
115
130
  }
116
131
 
117
132
  customElements.define("alchemy-list-filter", ListFilter)
@@ -1,15 +1,7 @@
1
1
  const DISMISS_DELAY = 5000
2
2
 
3
3
  class Message extends HTMLElement {
4
- #message
5
-
6
- constructor() {
7
- super()
8
- this.#message = this.innerHTML
9
- if (this.dismissable || this.type === "error") {
10
- this.addEventListener("click", this)
11
- }
12
- }
4
+ #dismissTimeoutId = null
13
5
 
14
6
  handleEvent(event) {
15
7
  if (event.type === "click") {
@@ -18,18 +10,33 @@ class Message extends HTMLElement {
18
10
  }
19
11
 
20
12
  connectedCallback() {
21
- this.innerHTML = `
22
- <alchemy-icon name="${this.iconName}"></alchemy-icon>
23
- ${this.dismissable && this.type === "error" ? '<alchemy-icon name="close"></alchemy-icon>' : ""}
24
- ${this.#message}
25
- `
13
+ if (!this.querySelector(":scope > alchemy-icon")) {
14
+ const closeIcon =
15
+ this.dismissable && this.type === "error"
16
+ ? '<alchemy-icon name="close"></alchemy-icon>'
17
+ : ""
18
+ this.insertAdjacentHTML(
19
+ "afterbegin",
20
+ `<alchemy-icon name="${this.iconName}"></alchemy-icon>${closeIcon}`
21
+ )
22
+ }
23
+ if (this.dismissable || this.type === "error") {
24
+ this.addEventListener("click", this)
25
+ }
26
26
  if (this.dismissable && this.type !== "error") {
27
- setTimeout(() => {
27
+ this.#dismissTimeoutId = setTimeout(() => {
28
28
  this.dismiss()
29
29
  }, this.dismissDelay)
30
30
  }
31
31
  }
32
32
 
33
+ disconnectedCallback() {
34
+ if (this.#dismissTimeoutId !== null) {
35
+ clearTimeout(this.#dismissTimeoutId)
36
+ this.#dismissTimeoutId = null
37
+ }
38
+ }
39
+
33
40
  dismiss() {
34
41
  this.addEventListener("transitionend", () => this.remove())
35
42
  this.classList.add("dismissed")
@@ -1,13 +1,11 @@
1
- import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
2
-
3
- class Overlay extends AlchemyHTMLElement {
4
- render() {
5
- return `
1
+ class Overlay extends HTMLElement {
2
+ connectedCallback() {
3
+ this.innerHTML = `
6
4
  <alchemy-spinner></alchemy-spinner>
7
5
  <div id="overlay_text_box">
8
- <span id="overlay_text">${this.getAttribute("text")}</span>
6
+ <span id="overlay_text">${this.getAttribute("text") ?? ""}</span>
9
7
  </div>
10
- `
8
+ `
11
9
  }
12
10
 
13
11
  set show(value) {
@@ -1,31 +1,44 @@
1
1
  // Handles the page publication date fields
2
2
  export class PagePublicationFields extends HTMLElement {
3
3
  connectedCallback() {
4
- const public_on_picker = this.querySelector("input#page_public_on")
5
- const public_until_picker = this.querySelector("input#page_public_until")
6
- const publication_date_fields = this.querySelector(
7
- ".page-publication-date-fields"
8
- )
9
- const public_field = this.querySelector("#page_public")
10
-
11
- if (!public_field) return
12
-
13
- public_field.addEventListener("click", function (evt) {
14
- const checkbox = evt.target
15
- const date = new Date()
16
- const now = new Date(
17
- date.getTime() - date.getTimezoneOffset() * 60000
18
- ).toISOString()
19
-
20
- if (checkbox.checked) {
21
- publication_date_fields.classList.remove("hidden")
22
- public_on_picker.value = now.substring(0, now.indexOf("T") + 6)
23
- } else {
24
- publication_date_fields.classList.add("hidden")
25
- public_on_picker.value = ""
26
- }
27
- public_until_picker.value = ""
28
- })
4
+ this.publicField?.addEventListener("click", this.#onClick)
5
+ }
6
+
7
+ disconnectedCallback() {
8
+ this.publicField?.removeEventListener("click", this.#onClick)
9
+ }
10
+
11
+ #onClick = (evt) => {
12
+ const checkbox = evt.target
13
+ const date = new Date()
14
+ const now = new Date(
15
+ date.getTime() - date.getTimezoneOffset() * 60000
16
+ ).toISOString()
17
+
18
+ if (checkbox.checked) {
19
+ this.publicationDateFields.classList.remove("hidden")
20
+ this.publicOnPicker.value = now.substring(0, now.indexOf("T") + 6)
21
+ } else {
22
+ this.publicationDateFields.classList.add("hidden")
23
+ this.publicOnPicker.value = ""
24
+ }
25
+ this.publicUntilPicker.value = ""
26
+ }
27
+
28
+ get publicField() {
29
+ return this.querySelector("#page_public")
30
+ }
31
+
32
+ get publicOnPicker() {
33
+ return this.querySelector("input#page_public_on")
34
+ }
35
+
36
+ get publicUntilPicker() {
37
+ return this.querySelector("input#page_public_until")
38
+ }
39
+
40
+ get publicationDateFields() {
41
+ return this.querySelector(".page-publication-date-fields")
29
42
  }
30
43
  }
31
44
 
@@ -1,9 +1,12 @@
1
1
  class PictureDescriptionSelect extends HTMLElement {
2
- constructor() {
3
- super()
2
+ connectedCallback() {
4
3
  this.addEventListener("change", this)
5
4
  }
6
5
 
6
+ disconnectedCallback() {
7
+ this.removeEventListener("change", this)
8
+ }
9
+
7
10
  handleEvent(event) {
8
11
  switch (event.type) {
9
12
  case "change":
@@ -8,9 +8,7 @@ const IMAGE_PLACEHOLDER = '<alchemy-icon name="image" size="xl"></alchemy-icon>'
8
8
  const THUMBNAIL_SIZE = "160x120"
9
9
 
10
10
  export class PictureEditor extends HTMLElement {
11
- constructor() {
12
- super()
13
-
11
+ connectedCallback() {
14
12
  this.cropFromField = this.querySelector("[data-crop-from]")
15
13
  this.cropSizeField = this.querySelector("[data-crop-size]")
16
14
  this.pictureIdField = this.querySelector("[data-picture-id]")
@@ -31,12 +29,9 @@ export class PictureEditor extends HTMLElement {
31
29
  this.updateCropLink()
32
30
  }, UPDATE_DELAY)
33
31
 
34
- this.deleteButton?.addEventListener("click", this.removeImage.bind(this))
35
- }
36
-
37
- connectedCallback() {
38
- this.observer = new MutationObserver(this.mutationCallback.bind(this))
32
+ this.deleteButton?.addEventListener("click", this.removeImage)
39
33
 
34
+ this.observer = new MutationObserver(this.mutationCallback)
40
35
  this.observer.observe(this.cropFromField, { attributes: true })
41
36
  this.observer.observe(this.cropSizeField, { attributes: true })
42
37
  this.observer.observe(this.pictureIdField, { attributes: true })
@@ -46,7 +41,7 @@ export class PictureEditor extends HTMLElement {
46
41
  this.observer.disconnect()
47
42
  }
48
43
 
49
- mutationCallback(mutationsList) {
44
+ mutationCallback = (mutationsList) => {
50
45
  for (const mutation of mutationsList) {
51
46
  if ("pictureId" in mutation.target.dataset) {
52
47
  this.cropFromField.value = ""
@@ -80,7 +75,7 @@ export class PictureEditor extends HTMLElement {
80
75
  })
81
76
  }
82
77
 
83
- removeImage() {
78
+ removeImage = () => {
84
79
  this.pictureThumbnail.innerHTML = IMAGE_PLACEHOLDER
85
80
  this.pictureIdField.value = ""
86
81
  this.image = null
@@ -7,12 +7,7 @@ export default class PictureThumbnail extends HTMLElement {
7
7
  constructor() {
8
8
  super()
9
9
 
10
- this.classList.add("thumbnail_background")
11
10
  this.spinner = new Spinner("small")
12
-
13
- if (this.src) {
14
- this.start()
15
- }
16
11
  }
17
12
 
18
13
  handleEvent(evt) {
@@ -29,6 +24,10 @@ export default class PictureThumbnail extends HTMLElement {
29
24
  }
30
25
 
31
26
  connectedCallback() {
27
+ this.classList.add("thumbnail_background")
28
+ if (this.src && !this.image) {
29
+ this.start()
30
+ }
32
31
  this.#setImage()
33
32
  }
34
33