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.
- checksums.yaml +4 -4
- data/README.md +4 -1
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js +1 -1
- data/app/assets/builds/alchemy/alchemy_admin.min.js.map +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -1
- data/app/assets/builds/alchemy/light-theme.css +1 -1
- data/app/assets/builds/alchemy/preview.min.js +1 -1
- data/app/assets/builds/alchemy/theme.css +1 -1
- data/app/assets/builds/alchemy/welcome.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/content/alchemy-dark/content.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -1
- data/app/assets/images/alchemy/admin/logo.svg +27 -0
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/dashboard/widget.rb +40 -0
- data/app/components/alchemy/admin/dashboard/widgets/attachment_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/element_usage.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.html.erb +13 -0
- data/app/components/alchemy/admin/dashboard/widgets/greeting.rb +21 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.html.erb +54 -0
- data/app/components/alchemy/admin/dashboard/widgets/locked_pages.rb +20 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.html.erb +22 -0
- data/app/components/alchemy/admin/dashboard/widgets/online_users.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_counts.rb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/page_usage.rb +46 -0
- data/app/components/alchemy/admin/dashboard/widgets/picture_counts.rb +17 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.html.erb +41 -0
- data/app/components/alchemy/admin/dashboard/widgets/recent_pages.rb +16 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.html.erb +29 -0
- data/app/components/alchemy/admin/dashboard/widgets/sites.rb +15 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.html.erb +23 -0
- data/app/components/alchemy/admin/dashboard/widgets/stat_widget.rb +19 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.html.erb +32 -0
- data/app/components/alchemy/admin/dashboard/widgets/system_info.rb +37 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.html.erb +42 -0
- data/app/components/alchemy/admin/dashboard/widgets/usage_widget.rb +66 -0
- data/app/components/alchemy/admin/dashboard/widgets/user_counts.rb +25 -0
- data/app/components/alchemy/admin/element_editor.html.erb +27 -20
- data/app/components/alchemy/admin/element_schedule_timestamps.rb +33 -0
- data/app/components/alchemy/admin/element_select.rb +4 -3
- data/app/components/alchemy/admin/page_node.html.erb +1 -20
- data/app/components/alchemy/admin/page_publication_fields.html.erb +30 -0
- data/app/components/alchemy/admin/page_publication_fields.rb +18 -0
- data/app/components/alchemy/admin/page_status_indicators.html.erb +29 -0
- data/app/components/alchemy/admin/page_status_indicators.rb +9 -0
- data/app/components/alchemy/admin/publish_element_button.html.erb +12 -4
- data/app/components/alchemy/ingredients/headline_editor.rb +1 -1
- data/app/controllers/alchemy/admin/dashboard/widgets_controller.rb +21 -0
- data/app/controllers/alchemy/admin/dashboard_controller.rb +3 -12
- data/app/controllers/alchemy/pages_controller.rb +5 -4
- data/app/helpers/alchemy/elements_block_helper.rb +1 -0
- data/app/javascript/alchemy_admin/components/auto_submit.js +15 -9
- data/app/javascript/alchemy_admin/components/char_counter.js +17 -7
- data/app/javascript/alchemy_admin/components/clipboard_button.js +2 -6
- data/app/javascript/alchemy_admin/components/color_select.js +13 -4
- data/app/javascript/alchemy_admin/components/datepicker.js +11 -14
- data/app/javascript/alchemy_admin/components/dialog_link.js +5 -2
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +6 -3
- data/app/javascript/alchemy_admin/components/element_editor.js +45 -28
- data/app/javascript/alchemy_admin/components/element_select.js +7 -4
- data/app/javascript/alchemy_admin/components/elements_window.js +38 -31
- data/app/javascript/alchemy_admin/components/elements_window_handle.js +7 -3
- data/app/javascript/alchemy_admin/components/file_editor.js +5 -2
- data/app/javascript/alchemy_admin/components/ingredient_group.js +6 -4
- data/app/javascript/alchemy_admin/components/link_buttons/link_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons/unlink_button.js +1 -2
- data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
- data/app/javascript/alchemy_admin/components/list_filter.js +44 -29
- data/app/javascript/alchemy_admin/components/message.js +22 -15
- data/app/javascript/alchemy_admin/components/overlay.js +5 -7
- data/app/javascript/alchemy_admin/components/page_publication_fields.js +38 -25
- data/app/javascript/alchemy_admin/components/picture_description_select.js +5 -2
- data/app/javascript/alchemy_admin/components/picture_editor.js +5 -10
- data/app/javascript/alchemy_admin/components/picture_thumbnail.js +4 -5
- data/app/javascript/alchemy_admin/components/preview_window.js +5 -10
- data/app/javascript/alchemy_admin/components/publish_page_button.js +2 -5
- data/app/javascript/alchemy_admin/components/remote_select.js +53 -23
- data/app/javascript/alchemy_admin/components/select.js +169 -26
- data/app/javascript/alchemy_admin/components/sortable_elements.js +1 -1
- data/app/javascript/alchemy_admin/components/spinner.js +11 -11
- data/app/javascript/alchemy_admin/components/tags_autocomplete.js +9 -1
- data/app/javascript/alchemy_admin/components/tinymce.js +16 -22
- data/app/javascript/alchemy_admin/components/uploader/file_upload.js +48 -45
- data/app/javascript/alchemy_admin/components/uploader/progress.js +70 -84
- data/app/javascript/alchemy_admin/components/uploader.js +71 -46
- data/app/javascript/alchemy_admin/dialog.js +3 -0
- data/app/javascript/alchemy_admin/hotkeys.js +0 -18
- data/app/javascript/alchemy_admin/image_cropper.js +7 -9
- data/app/javascript/alchemy_admin/initializer.js +21 -0
- data/app/javascript/alchemy_admin/utils/dispatch_page_dirty_event.js +7 -0
- data/app/javascript/tinymce/plugins/alchemy_link/index.js +9 -0
- data/app/jobs/alchemy/base_job.rb +2 -2
- data/app/jobs/alchemy/invalidate_elements_cache_job.rb +33 -0
- data/app/models/alchemy/page/page_naming.rb +28 -5
- data/app/models/alchemy/page/page_natures.rb +7 -2
- data/app/models/alchemy/page/page_scopes.rb +2 -2
- data/app/models/alchemy/page/url_path.rb +7 -2
- data/app/models/alchemy/page.rb +2 -2
- data/app/models/alchemy/page_definition.rb +1 -0
- data/app/models/alchemy/permissions.rb +1 -1
- data/app/models/concerns/alchemy/relatable_resource.rb +8 -0
- data/app/services/alchemy/page_finder.rb +88 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +6 -4
- data/app/stylesheets/alchemy/_mixins.scss +1 -7
- data/app/stylesheets/alchemy/_themes.scss +13 -1
- data/app/stylesheets/alchemy/admin/_tom-select.scss +240 -0
- data/app/stylesheets/alchemy/admin/archive.scss +0 -1
- data/app/stylesheets/alchemy/admin/base.scss +0 -19
- data/app/stylesheets/alchemy/admin/dashboard.scss +395 -28
- data/app/stylesheets/alchemy/admin/elements.scss +14 -17
- data/app/stylesheets/alchemy/admin/form_fields.scss +3 -3
- data/app/stylesheets/alchemy/admin/forms.scss +107 -93
- data/app/stylesheets/alchemy/admin/icons.scss +28 -0
- data/app/stylesheets/alchemy/admin/image_library.scss +20 -10
- data/app/stylesheets/alchemy/admin/navigation.scss +4 -1
- data/app/stylesheets/alchemy/admin/popover.scss +3 -5
- data/app/stylesheets/alchemy/admin/resource_info.scss +11 -17
- data/app/stylesheets/alchemy/admin/shoelace.scss +8 -0
- data/app/stylesheets/alchemy/admin/sitemap.scss +5 -0
- data/app/stylesheets/alchemy/admin/tables.scss +32 -3
- data/app/stylesheets/alchemy/admin/toolbar.scss +0 -1
- data/app/stylesheets/alchemy/admin.scss +1 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +0 -4
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +0 -4
- data/app/types/alchemy/wildcard_url_type.rb +48 -0
- data/app/views/alchemy/_menubar.html.erb +1 -5
- data/app/views/alchemy/admin/attachments/edit.html.erb +6 -3
- data/app/views/alchemy/admin/dashboard/_dashboard.html.erb +3 -2
- data/app/views/alchemy/admin/dashboard/_footer.html.erb +22 -0
- data/app/views/alchemy/admin/dashboard/_stats.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/_top.html.erb +4 -12
- data/app/views/alchemy/admin/dashboard/_widgets.html.erb +7 -0
- data/app/views/alchemy/admin/dashboard/index.html.erb +0 -17
- data/app/views/alchemy/admin/dashboard/info.html.erb +1 -62
- data/app/views/alchemy/admin/dashboard/widgets/show.html.erb +3 -0
- data/app/views/alchemy/admin/elements/_form.html.erb +2 -1
- data/app/views/alchemy/admin/elements/_schedule.html.erb +2 -15
- data/app/views/alchemy/admin/elements/_schedule_fields.html.erb +2 -0
- data/app/views/alchemy/admin/layoutpages/edit.html.erb +6 -3
- data/app/views/alchemy/admin/nodes/_page_nodes.html.erb +10 -8
- data/app/views/alchemy/admin/pages/_form.html.erb +25 -19
- data/app/views/alchemy/admin/pages/_publication_fields.html.erb +2 -32
- data/app/views/alchemy/admin/pages/_table.html.erb +1 -18
- data/app/views/alchemy/admin/pages/configure.html.erb +2 -2
- data/app/views/alchemy/admin/pages/info.html.erb +6 -0
- data/app/views/alchemy/admin/resources/_form.html.erb +7 -4
- data/app/views/alchemy/admin/resources/edit.html.erb +3 -1
- data/app/views/alchemy/admin/resources/new.html.erb +3 -1
- data/app/views/alchemy/admin/styleguide/index.html.erb +52 -30
- data/app/views/alchemy/admin/translations/_en.js +4 -0
- data/app/views/layouts/alchemy/admin.html.erb +3 -3
- data/config/importmap.rb +2 -0
- data/config/locales/alchemy.en.yml +15 -0
- data/config/routes.rb +1 -0
- data/lib/alchemy/configuration/class_option.rb +46 -3
- data/lib/alchemy/configuration/collection_option.rb +4 -0
- data/lib/alchemy/configurations/dashboard.rb +79 -0
- data/lib/alchemy/configurations/main.rb +15 -0
- data/lib/alchemy/engine.rb +9 -3
- data/lib/alchemy/sprockets/skip_builds_compression.rb +33 -0
- data/lib/alchemy/test_support/capybara_helpers.rb +17 -0
- data/lib/alchemy/test_support/relatable_resource_examples.rb +20 -0
- data/lib/alchemy/test_support/rspec_matchers.rb +8 -0
- data/lib/alchemy/test_support/shared_publishable_examples.rb +38 -31
- data/lib/alchemy/tinymce.rb +1 -1
- data/lib/alchemy/version.rb +17 -3
- data/vendor/javascript/cropperjs.min.js +1 -1
- data/vendor/javascript/flatpickr.min.js +1 -1
- data/vendor/javascript/floating-ui.min.js +1 -0
- data/vendor/javascript/keymaster.min.js +1 -1
- data/vendor/javascript/rails-ujs.min.js +1 -1
- data/vendor/javascript/shoelace.min.js +93 -93
- data/vendor/javascript/sortable.min.js +1 -1
- data/vendor/javascript/tinymce.min.js +5 -1
- data/vendor/javascript/tom-select.min.js +1 -0
- metadata +57 -18
- data/app/javascript/alchemy_admin/components/alchemy_html_element.js +0 -129
- data/app/views/alchemy/admin/dashboard/_left_column.html.erb +0 -4
- data/app/views/alchemy/admin/dashboard/_right_column.html.erb +0 -9
- data/app/views/alchemy/admin/dashboard/widgets/_locked_pages.html.erb +0 -52
- data/app/views/alchemy/admin/dashboard/widgets/_recent_pages.html.erb +0 -34
- data/app/views/alchemy/admin/dashboard/widgets/_sites.html.erb +0 -25
- data/app/views/alchemy/admin/dashboard/widgets/_users.html.erb +0 -21
- data/app/views/alchemy/admin/languages/edit.html.erb +0 -1
- data/app/views/alchemy/admin/languages/new.html.erb +0 -1
- data/app/views/alchemy/admin/sites/edit.html.erb +0 -1
- data/app/views/alchemy/admin/sites/new.html.erb +0 -1
|
@@ -5,13 +5,6 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
5
5
|
#afterLoad
|
|
6
6
|
#reloadIcon
|
|
7
7
|
#loadTimeout
|
|
8
|
-
#previewReadyHandler
|
|
9
|
-
|
|
10
|
-
constructor() {
|
|
11
|
-
super()
|
|
12
|
-
this.addEventListener("load", this)
|
|
13
|
-
this.#previewReadyHandler = this.#handlePreviewReadyMessage.bind(this)
|
|
14
|
-
}
|
|
15
8
|
|
|
16
9
|
handleEvent(evt) {
|
|
17
10
|
if (evt.type === "load") {
|
|
@@ -21,7 +14,7 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
21
14
|
}
|
|
22
15
|
}
|
|
23
16
|
|
|
24
|
-
#
|
|
17
|
+
#onPreviewReady = (event) => {
|
|
25
18
|
if (event.data.message === "Alchemy.previewReady") {
|
|
26
19
|
this.#clearLoadTimeout()
|
|
27
20
|
this.#stopSpinner()
|
|
@@ -32,8 +25,9 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
32
25
|
connectedCallback() {
|
|
33
26
|
let url = this.url
|
|
34
27
|
|
|
28
|
+
this.addEventListener("load", this)
|
|
35
29
|
this.#attachEvents()
|
|
36
|
-
window.addEventListener("message", this.#
|
|
30
|
+
window.addEventListener("message", this.#onPreviewReady)
|
|
37
31
|
|
|
38
32
|
if (window.localStorage.getItem("alchemy-preview-url")) {
|
|
39
33
|
url = window.localStorage.getItem("alchemy-preview-url")
|
|
@@ -44,8 +38,9 @@ class PreviewWindow extends HTMLIFrameElement {
|
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
disconnectedCallback() {
|
|
41
|
+
this.removeEventListener("load", this)
|
|
47
42
|
key.unbind("alt+r")
|
|
48
|
-
window.removeEventListener("message", this.#
|
|
43
|
+
window.removeEventListener("message", this.#onPreviewReady)
|
|
49
44
|
}
|
|
50
45
|
|
|
51
46
|
postMessage(data) {
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
class PublishPageButton extends HTMLElement {
|
|
2
|
-
constructor() {
|
|
3
|
-
super()
|
|
4
|
-
this.addEventListener("submit", this)
|
|
5
|
-
}
|
|
6
|
-
|
|
7
2
|
connectedCallback() {
|
|
3
|
+
this.addEventListener("submit", this)
|
|
8
4
|
document.addEventListener("alchemy:page-dirty", this)
|
|
9
5
|
}
|
|
10
6
|
|
|
11
7
|
disconnectedCallback() {
|
|
8
|
+
this.removeEventListener("submit", this)
|
|
12
9
|
document.removeEventListener("alchemy:page-dirty", this)
|
|
13
10
|
}
|
|
14
11
|
|
|
@@ -1,40 +1,38 @@
|
|
|
1
|
-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
|
|
2
1
|
import { setupSelectLocale } from "alchemy_admin/i18n"
|
|
3
2
|
|
|
4
3
|
export function hightlightTerm(name, term) {
|
|
5
4
|
return name.replace(new RegExp(term, "gi"), (match) => `<em>${match}</em>`)
|
|
6
5
|
}
|
|
7
6
|
|
|
8
|
-
export class RemoteSelect extends
|
|
9
|
-
|
|
10
|
-
allowClear: { default: false },
|
|
11
|
-
selection: { default: undefined },
|
|
12
|
-
placeholder: { default: "" },
|
|
13
|
-
queryParams: { default: "{}" },
|
|
14
|
-
url: { default: "" }
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Select2 manages its own DOM after initialization, so attribute changes
|
|
18
|
-
// must not trigger the default re-render which would destroy the widget.
|
|
19
|
-
static get observedAttributes() {
|
|
20
|
-
return []
|
|
21
|
-
}
|
|
7
|
+
export class RemoteSelect extends HTMLElement {
|
|
8
|
+
#select2 = null
|
|
22
9
|
|
|
23
|
-
async
|
|
10
|
+
async connectedCallback() {
|
|
24
11
|
await setupSelectLocale()
|
|
12
|
+
// Bail out if the element was disconnected while the locale was loading.
|
|
13
|
+
// Otherwise Select2 would leak onto a detached input.
|
|
14
|
+
if (!this.isConnected) return
|
|
25
15
|
|
|
26
16
|
this.input.classList.add("alchemy_selectbox")
|
|
27
17
|
|
|
28
|
-
$(this.input)
|
|
18
|
+
this.#select2 = $(this.input)
|
|
29
19
|
.select2(this.select2Config)
|
|
30
|
-
.on("select2-open",
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
20
|
+
.on("select2-open", this.#onOpen)
|
|
21
|
+
.on("change", this.#onChange)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
disconnectedCallback() {
|
|
25
|
+
if (this.#select2) {
|
|
26
|
+
this.#select2.off("select2-open", this.#onOpen)
|
|
27
|
+
this.#select2.off("change", this.#onChange)
|
|
28
|
+
this.#select2.select2("destroy")
|
|
29
|
+
this.#select2 = null
|
|
30
|
+
}
|
|
36
31
|
}
|
|
37
32
|
|
|
33
|
+
#onOpen = (evt) => this.onOpen(evt)
|
|
34
|
+
#onChange = (evt) => this.onChange(evt)
|
|
35
|
+
|
|
38
36
|
/**
|
|
39
37
|
* Optional on change handler called by Select2.
|
|
40
38
|
* @param {Event} event
|
|
@@ -65,6 +63,38 @@ export class RemoteSelect extends AlchemyHTMLElement {
|
|
|
65
63
|
}, 100)
|
|
66
64
|
}
|
|
67
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Dispatches a custom event with given name, namespaced under `Alchemy.`.
|
|
68
|
+
* Subclasses may call this to emit their own events.
|
|
69
|
+
* @param {string} name The name of the custom event
|
|
70
|
+
* @param {object} detail Optional event details
|
|
71
|
+
*/
|
|
72
|
+
dispatchCustomEvent(name, detail = {}) {
|
|
73
|
+
this.dispatchEvent(
|
|
74
|
+
new CustomEvent(`Alchemy.${name}`, { bubbles: true, detail })
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get allowClear() {
|
|
79
|
+
return this.hasAttribute("allow-clear")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
get selection() {
|
|
83
|
+
return this.getAttribute("selection")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get placeholder() {
|
|
87
|
+
return this.getAttribute("placeholder") ?? ""
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get queryParams() {
|
|
91
|
+
return this.getAttribute("query-params") ?? "{}"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get url() {
|
|
95
|
+
return this.getAttribute("url") ?? ""
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
get input() {
|
|
69
99
|
return this.getElementsByTagName("input")[0]
|
|
70
100
|
}
|
|
@@ -1,37 +1,44 @@
|
|
|
1
|
+
import TomSelect from "tom-select"
|
|
2
|
+
import { translate } from "alchemy_admin/i18n"
|
|
3
|
+
import {
|
|
4
|
+
autoUpdate,
|
|
5
|
+
computePosition,
|
|
6
|
+
flip,
|
|
7
|
+
offset,
|
|
8
|
+
size
|
|
9
|
+
} from "@floating-ui/dom"
|
|
10
|
+
|
|
11
|
+
const DROPDOWN_WINDOW_MARGIN = 16
|
|
12
|
+
const DROPDOWN_MIN_HEIGHT = 120
|
|
13
|
+
|
|
1
14
|
class Select extends HTMLSelectElement {
|
|
2
|
-
#
|
|
15
|
+
#tomSelect = null
|
|
3
16
|
|
|
4
17
|
connectedCallback() {
|
|
5
18
|
this.classList.add("alchemy_selectbox")
|
|
19
|
+
this.#initTomSelect()
|
|
20
|
+
}
|
|
6
21
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
dropdownAutoWidth: true,
|
|
10
|
-
allowClear: !!this.allowClear
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
// For single selects, remove the close button if allowClear is not set
|
|
14
|
-
// For multiple selects, always keep the close buttons
|
|
15
|
-
if (!this.allowClear && !this.multiple) {
|
|
16
|
-
this.#select2Element
|
|
17
|
-
.prev(".select2-container")
|
|
18
|
-
.find(".select2-search-choice-close")
|
|
19
|
-
.remove()
|
|
20
|
-
}
|
|
22
|
+
disconnectedCallback() {
|
|
23
|
+
this.#destroyTomSelect()
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
enable() {
|
|
24
27
|
this.removeAttribute("disabled")
|
|
25
|
-
this.#
|
|
28
|
+
this.#tomSelect?.enable()
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
disable() {
|
|
29
32
|
this.setAttribute("disabled", "disabled")
|
|
30
|
-
this.#
|
|
33
|
+
this.#tomSelect?.disable()
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
setOptions(data, prompt = undefined) {
|
|
34
|
-
|
|
37
|
+
const selectedValue = this.value
|
|
38
|
+
|
|
39
|
+
// Tom Select needs to be rebuilt from the new native options, so tear it
|
|
40
|
+
// down, replace the options and initialize it again.
|
|
41
|
+
this.#destroyTomSelect()
|
|
35
42
|
|
|
36
43
|
// reset the old options and insert the placeholder(s) first
|
|
37
44
|
this.innerHTML = ""
|
|
@@ -44,19 +51,155 @@ class Select extends HTMLSelectElement {
|
|
|
44
51
|
this.add(new Option(item.text, item.id, false, item.id === selectedValue))
|
|
45
52
|
})
|
|
46
53
|
|
|
47
|
-
this.#
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* inform Select2 to update
|
|
52
|
-
*/
|
|
53
|
-
#updateSelect2() {
|
|
54
|
-
this.#select2Element.trigger("change")
|
|
54
|
+
this.#initTomSelect()
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
get allowClear() {
|
|
58
58
|
return this.dataset.hasOwnProperty("allowClear") || this.multiple
|
|
59
59
|
}
|
|
60
|
+
|
|
61
|
+
get placeholder() {
|
|
62
|
+
return this.getAttribute("placeholder")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#initTomSelect() {
|
|
66
|
+
const plugins = {}
|
|
67
|
+
const hasPlaceholder = !!this.placeholder
|
|
68
|
+
// Capture this before Tom Select initializes, since it rewrites the
|
|
69
|
+
// select's selected option during setup.
|
|
70
|
+
const hasSelectedOption = !!this.querySelector("option[selected]")
|
|
71
|
+
const dropdownMask = document.createElement("div")
|
|
72
|
+
dropdownMask.className = "ts-dropdown-mask"
|
|
73
|
+
|
|
74
|
+
let removeAutoUpdater = () => {}
|
|
75
|
+
|
|
76
|
+
if (this.multiple) {
|
|
77
|
+
plugins.remove_button = {
|
|
78
|
+
title: translate("Remove")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (this.allowClear) {
|
|
83
|
+
plugins.clear_button = {
|
|
84
|
+
html() {
|
|
85
|
+
return `<button type="button" class="clear-button" aria-label="${translate(
|
|
86
|
+
"Clear selection"
|
|
87
|
+
)}">
|
|
88
|
+
<alchemy-icon name="close" size="1x"></alchemy-icon>
|
|
89
|
+
</button>`
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const settings = {
|
|
95
|
+
plugins,
|
|
96
|
+
closeAfterSelect: !this.multiple,
|
|
97
|
+
onInitialize: function () {
|
|
98
|
+
if (this.input.autofocus) {
|
|
99
|
+
this.focus()
|
|
100
|
+
}
|
|
101
|
+
// Tom Select auto-selects the first option when none is selected. With
|
|
102
|
+
// a placeholder we want it to start empty instead, but only clear when
|
|
103
|
+
// no option was explicitly marked selected, so a preselected value is
|
|
104
|
+
// preserved.
|
|
105
|
+
if (hasPlaceholder && !hasSelectedOption) {
|
|
106
|
+
this.clear()
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
onType(term) {
|
|
110
|
+
this.control_input.classList.toggle("has-value", term.length > 0)
|
|
111
|
+
},
|
|
112
|
+
// remove the transition after selection of option.
|
|
113
|
+
refreshThrottle: 0,
|
|
114
|
+
onDropdownOpen: async function () {
|
|
115
|
+
// Make the dropdown at least as wide as the control.
|
|
116
|
+
const styles = {
|
|
117
|
+
minWidth: `${this.control.offsetWidth}px`
|
|
118
|
+
}
|
|
119
|
+
// If the select is inside a dialog, we need to ensure the dropdown appears above it.
|
|
120
|
+
if (this.control.closest(".alchemy-dialog-body, .alchemy-popover")) {
|
|
121
|
+
styles.zIndex = "101"
|
|
122
|
+
}
|
|
123
|
+
Object.assign(this.dropdown.style, styles)
|
|
124
|
+
// Append the dropdown to the body to avoid overflow issues, especially in dialogs.
|
|
125
|
+
document.body.append(dropdownMask)
|
|
126
|
+
document.body.append(this.dropdown)
|
|
127
|
+
// Use Floating UI to position the dropdown relative to the control.
|
|
128
|
+
const updatePosition = async () => {
|
|
129
|
+
// Use Floating UI to calculate the dropdown position
|
|
130
|
+
const { x, y } = await computePosition(this.control, this.dropdown, {
|
|
131
|
+
middleware: [
|
|
132
|
+
// Flip to the opposite side if there’s not enough space
|
|
133
|
+
flip(),
|
|
134
|
+
// Make some space between the control and the dropdown to prevent overlap
|
|
135
|
+
offset(2),
|
|
136
|
+
// Ensure the dropdown fits within the viewport
|
|
137
|
+
size({
|
|
138
|
+
apply({ availableHeight, elements }) {
|
|
139
|
+
Object.assign(
|
|
140
|
+
elements.floating.querySelector(".ts-dropdown-content")
|
|
141
|
+
.style,
|
|
142
|
+
{
|
|
143
|
+
maxHeight: `${Math.max(DROPDOWN_MIN_HEIGHT, availableHeight - DROPDOWN_WINDOW_MARGIN)}px`
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
]
|
|
149
|
+
})
|
|
150
|
+
// Position the dropdown
|
|
151
|
+
Object.assign(this.dropdown.style, {
|
|
152
|
+
left: `${x}px`,
|
|
153
|
+
top: `${y}px`
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
// Update the dropdown position whenever the window resizes or scrolls.
|
|
157
|
+
removeAutoUpdater = autoUpdate(
|
|
158
|
+
this.control,
|
|
159
|
+
this.dropdown,
|
|
160
|
+
updatePosition
|
|
161
|
+
)
|
|
162
|
+
},
|
|
163
|
+
onDropdownClose: function () {
|
|
164
|
+
this.control_input.classList.remove("has-value")
|
|
165
|
+
// Remove the dropdown from DOM when closed.
|
|
166
|
+
this.dropdown.remove()
|
|
167
|
+
dropdownMask.remove()
|
|
168
|
+
// Cleanup the position auto-update when the dropdown is closed.
|
|
169
|
+
removeAutoUpdater()
|
|
170
|
+
},
|
|
171
|
+
allowEmptyOption: true,
|
|
172
|
+
openOnFocus: false,
|
|
173
|
+
// Keep options in their original order instead of sorting by value.
|
|
174
|
+
sortField: "$order",
|
|
175
|
+
// Show every option, not just the first 50 (e.g. the timezone select).
|
|
176
|
+
maxOptions: null,
|
|
177
|
+
// Customize the "create" and "no results" dropdown messages with i18n.
|
|
178
|
+
render: {
|
|
179
|
+
option_create(data, escape) {
|
|
180
|
+
return `<div class="create">
|
|
181
|
+
${translate("Add")}<strong>${escape(data.input)}</strong>…
|
|
182
|
+
</div>`
|
|
183
|
+
},
|
|
184
|
+
no_results() {
|
|
185
|
+
return `<div class="no-results">${translate("No results found")}</div>`
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.#tomSelect = new TomSelect(this, settings)
|
|
191
|
+
|
|
192
|
+
// Mimick the native select's click-to-open behavior.
|
|
193
|
+
this.#tomSelect.control.addEventListener(
|
|
194
|
+
"click",
|
|
195
|
+
this.#tomSelect.open.bind(this.#tomSelect)
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#destroyTomSelect() {
|
|
200
|
+
this.#tomSelect?.destroy()
|
|
201
|
+
this.#tomSelect = null
|
|
202
|
+
}
|
|
60
203
|
}
|
|
61
204
|
|
|
62
205
|
customElements.define("alchemy-select", Select, { extends: "select" })
|
|
@@ -2,7 +2,7 @@ import Sortable from "sortablejs"
|
|
|
2
2
|
import { growl } from "alchemy_admin/growler"
|
|
3
3
|
import { post } from "alchemy_admin/utils/ajax"
|
|
4
4
|
import { reloadPreview } from "alchemy_admin/components/preview_window"
|
|
5
|
-
import { dispatchPageDirtyEvent } from "alchemy_admin/
|
|
5
|
+
import { dispatchPageDirtyEvent } from "alchemy_admin/utils/dispatch_page_dirty_event"
|
|
6
6
|
|
|
7
7
|
const SORTABLE_OPTIONS = {
|
|
8
8
|
draggable: ".element-editor",
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
class Spinner extends AlchemyHTMLElement {
|
|
4
|
-
static properties = {
|
|
5
|
-
size: { default: "medium" },
|
|
6
|
-
color: { default: "currentColor" }
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
render() {
|
|
1
|
+
class Spinner extends HTMLElement {
|
|
2
|
+
connectedCallback() {
|
|
10
3
|
this.className = `spinner spinner--${this.size}`
|
|
11
|
-
|
|
12
|
-
return `
|
|
4
|
+
this.innerHTML = `
|
|
13
5
|
<svg width="100%" viewBox="0 0 28 28" style="--spinner-color: ${this.color}">
|
|
14
6
|
<path
|
|
15
7
|
class="hex1"
|
|
@@ -26,6 +18,14 @@ class Spinner extends AlchemyHTMLElement {
|
|
|
26
18
|
</svg>
|
|
27
19
|
`
|
|
28
20
|
}
|
|
21
|
+
|
|
22
|
+
get size() {
|
|
23
|
+
return this.getAttribute("size") || "medium"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get color() {
|
|
27
|
+
return this.getAttribute("color") || "currentColor"
|
|
28
|
+
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
customElements.define("alchemy-spinner", Spinner)
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { setupSelectLocale } from "alchemy_admin/i18n"
|
|
2
2
|
|
|
3
3
|
class TagsAutocomplete extends HTMLElement {
|
|
4
|
+
#select2 = null
|
|
5
|
+
|
|
4
6
|
async connectedCallback() {
|
|
5
7
|
await setupSelectLocale()
|
|
8
|
+
if (!this.isConnected) return
|
|
6
9
|
|
|
7
10
|
this.classList.add("autocomplete_tag_list")
|
|
8
|
-
$(this.input).select2(this.select2Config)
|
|
11
|
+
this.#select2 = $(this.input).select2(this.select2Config)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
disconnectedCallback() {
|
|
15
|
+
this.#select2?.select2("destroy")
|
|
16
|
+
this.#select2 = null
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
get input() {
|
|
@@ -1,19 +1,30 @@
|
|
|
1
1
|
import "tinymce"
|
|
2
|
-
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
|
|
3
2
|
import { currentLocale } from "alchemy_admin/i18n"
|
|
4
3
|
|
|
5
4
|
const DARK_THEME = "alchemy-dark"
|
|
6
5
|
const LIGHT_THEME = "alchemy"
|
|
7
6
|
|
|
8
|
-
class Tinymce extends
|
|
7
|
+
class Tinymce extends HTMLElement {
|
|
9
8
|
#min_height = null
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* the observer will initialize Tinymce if the textarea becomes visible
|
|
13
12
|
*/
|
|
14
|
-
|
|
13
|
+
connectedCallback() {
|
|
15
14
|
this.className = "tinymce_container"
|
|
16
15
|
|
|
16
|
+
// Append the spinner if not already present (idempotent on reconnect/clone)
|
|
17
|
+
if (!this.querySelector(":scope > alchemy-spinner")) {
|
|
18
|
+
this.insertAdjacentHTML(
|
|
19
|
+
"beforeend",
|
|
20
|
+
`<alchemy-spinner size="small"></alchemy-spinner>`
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// hide the textarea until TinyMCE is ready to show the editor
|
|
25
|
+
this.style.minHeight = `${this.minHeight}px`
|
|
26
|
+
this.editor.style.display = "none"
|
|
27
|
+
|
|
17
28
|
const observerCallback = (entries, observer) => {
|
|
18
29
|
entries.forEach((entry) => {
|
|
19
30
|
if (entry.intersectionRatio > 0) {
|
|
@@ -43,10 +54,8 @@ class Tinymce extends AlchemyHTMLElement {
|
|
|
43
54
|
/**
|
|
44
55
|
* disconnect intersection observer and remove Tinymce editor if the web components get destroyed
|
|
45
56
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.tinymceIntersectionObserver.disconnect()
|
|
49
|
-
}
|
|
57
|
+
disconnectedCallback() {
|
|
58
|
+
this.tinymceIntersectionObserver?.disconnect()
|
|
50
59
|
|
|
51
60
|
// Remove theme change listener
|
|
52
61
|
this._removeThemeChangeListener()
|
|
@@ -54,21 +63,6 @@ class Tinymce extends AlchemyHTMLElement {
|
|
|
54
63
|
tinymce.get(this.editorId)?.remove(this.editorId)
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
render() {
|
|
58
|
-
return `
|
|
59
|
-
${this.initialContent}
|
|
60
|
-
<alchemy-spinner size="small"></alchemy-spinner>
|
|
61
|
-
`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* hide the textarea until TinyMCE is ready to show the editor
|
|
66
|
-
*/
|
|
67
|
-
afterRender() {
|
|
68
|
-
this.style.minHeight = `${this.minHeight}px`
|
|
69
|
-
this.editor.style.display = "none"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
66
|
/**
|
|
73
67
|
* initialize Richtext area after the Intersection observer triggered
|
|
74
68
|
* @private
|