alchemy_cms 8.0.0.a → 8.0.0.c
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 +3 -0
- data/app/assets/builds/alchemy/admin/page-select.css +1 -1
- data/app/assets/builds/alchemy/admin.css +1 -1
- data/app/assets/builds/alchemy/dark-theme.css +1 -0
- data/app/assets/builds/alchemy/light-theme.css +1 -0
- data/app/assets/builds/alchemy/theme.css +1 -0
- 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 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy/skin.min.css +1 -1
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/content.min.css +1 -0
- data/app/assets/builds/tinymce/skins/ui/alchemy-dark/skin.min.css +1 -0
- data/app/assets/images/alchemy/element_icons/layout-bottom-2-line.svg +1 -0
- data/app/assets/images/alchemy/icons-sprite.svg +1 -1
- data/app/components/alchemy/admin/element_select.rb +39 -0
- data/app/components/alchemy/admin/link_dialog/tabs.rb +1 -1
- data/app/components/alchemy/admin/locale_select.rb +38 -0
- data/app/components/alchemy/ingredients/datetime_view.rb +4 -2
- data/app/controllers/alchemy/admin/attachments_controller.rb +2 -0
- data/app/controllers/alchemy/admin/elements_controller.rb +2 -0
- data/app/controllers/alchemy/admin/pages_controller.rb +3 -1
- data/app/controllers/alchemy/admin/pictures_controller.rb +26 -34
- data/app/controllers/alchemy/admin/resources_controller.rb +1 -1
- data/app/controllers/alchemy/pages_controller.rb +19 -2
- data/app/controllers/concerns/alchemy/admin/resource_filter.rb +1 -0
- data/app/decorators/alchemy/ingredient_editor.rb +9 -1
- data/app/helpers/alchemy/admin/attachments_helper.rb +5 -5
- data/app/helpers/alchemy/admin/base_helper.rb +0 -7
- data/app/helpers/alchemy/admin/form_helper.rb +2 -1
- data/app/helpers/alchemy/pages_helper.rb +1 -1
- data/app/javascript/alchemy_admin/components/auto_submit.js +20 -0
- data/app/javascript/alchemy_admin/components/datepicker.js +8 -5
- data/app/javascript/alchemy_admin/components/element_editor/delete_element_button.js +3 -2
- data/app/javascript/alchemy_admin/components/element_editor.js +25 -15
- data/app/javascript/alchemy_admin/components/element_select.js +43 -0
- data/app/javascript/alchemy_admin/components/index.js +5 -0
- data/app/javascript/alchemy_admin/components/link_buttons.js +6 -2
- data/app/javascript/alchemy_admin/components/remote_select.js +5 -1
- data/app/javascript/alchemy_admin/components/tinymce.js +93 -16
- data/app/javascript/alchemy_admin/dialog.js +1 -1
- data/app/javascript/alchemy_admin/file_editors.js +1 -1
- data/app/javascript/alchemy_admin/image_loader.js +4 -2
- data/app/javascript/alchemy_admin/picture_editors.js +7 -4
- data/app/javascript/alchemy_admin/picture_selector.js +4 -4
- data/app/jobs/alchemy/delete_picture_job.rb +12 -0
- data/app/models/alchemy/attachment.rb +2 -9
- data/app/models/alchemy/element.rb +1 -0
- data/app/models/alchemy/element_definition.rb +31 -0
- data/app/models/alchemy/ingredient.rb +1 -1
- data/app/models/alchemy/ingredients/boolean.rb +2 -1
- data/app/models/alchemy/language.rb +2 -7
- data/app/models/alchemy/page/page_naming.rb +4 -11
- data/app/models/alchemy/page/page_natures.rb +16 -11
- data/app/models/alchemy/page/publisher.rb +1 -1
- data/app/models/alchemy/page.rb +1 -6
- data/app/models/alchemy/page_definition.rb +1 -1
- data/app/models/alchemy/picture.rb +6 -17
- data/app/models/alchemy/resource.rb +15 -2
- data/app/models/alchemy/site/layout.rb +1 -0
- data/app/models/alchemy/site.rb +1 -6
- data/app/models/alchemy/storage_adapter/dragonfly/picture_url.rb +7 -2
- data/app/models/alchemy/storage_adapter/dragonfly.rb +24 -2
- data/app/models/concerns/alchemy/relatable_resource.rb +28 -0
- data/app/stylesheets/alchemy/_custom-properties.scss +162 -0
- data/app/stylesheets/alchemy/_mixins.scss +12 -24
- data/app/stylesheets/alchemy/_themes.scss +540 -0
- data/app/stylesheets/alchemy/admin/archive.scss +28 -8
- data/app/stylesheets/alchemy/admin/attachments.scss +10 -33
- data/app/stylesheets/alchemy/admin/base.scss +4 -1
- data/app/stylesheets/alchemy/admin/buttons.scss +7 -32
- data/app/stylesheets/alchemy/admin/dashboard.scss +13 -0
- data/app/stylesheets/alchemy/admin/dialogs.scss +17 -7
- data/app/stylesheets/alchemy/admin/element-select.scss +11 -0
- data/app/stylesheets/alchemy/admin/elements.scss +95 -34
- data/app/stylesheets/alchemy/admin/filters.scss +8 -9
- data/app/stylesheets/alchemy/admin/flatpickr.scss +12 -27
- data/app/stylesheets/alchemy/admin/form_fields.scss +0 -15
- data/app/stylesheets/alchemy/admin/forms.scss +3 -8
- data/app/stylesheets/alchemy/admin/frame.scss +5 -7
- data/app/stylesheets/alchemy/admin/icons.scss +0 -9
- data/app/stylesheets/alchemy/admin/image_library.scss +13 -55
- data/app/stylesheets/alchemy/admin/navigation.scss +1 -11
- data/app/stylesheets/alchemy/admin/node-select.scss +1 -10
- data/app/stylesheets/alchemy/admin/nodes.scss +6 -2
- data/app/stylesheets/alchemy/admin/notices.scss +5 -4
- data/app/stylesheets/alchemy/admin/page-select.scss +16 -0
- data/app/stylesheets/alchemy/admin/pagination.scss +1 -8
- data/app/stylesheets/alchemy/admin/preview_window.scss +12 -1
- data/app/stylesheets/alchemy/admin/resource_info.scss +106 -3
- data/app/stylesheets/alchemy/admin/search.scss +1 -1
- data/app/stylesheets/alchemy/admin/selects.scss +58 -31
- data/app/stylesheets/alchemy/admin/shoelace.scss +32 -62
- data/app/stylesheets/alchemy/admin/sitemap.scss +7 -18
- data/app/stylesheets/alchemy/admin/tables.scss +3 -3
- data/app/stylesheets/alchemy/admin/tags.scss +18 -35
- data/app/stylesheets/alchemy/admin/toolbar.scss +0 -6
- data/app/stylesheets/alchemy/admin/typography.scss +2 -5
- data/app/stylesheets/alchemy/admin.scss +1 -1
- data/app/stylesheets/alchemy/dark-theme.scss +5 -0
- data/app/stylesheets/alchemy/light-theme.scss +6 -0
- data/app/stylesheets/alchemy/theme.scss +13 -0
- data/app/stylesheets/tinymce/skins/content/alchemy/content.scss +8 -8
- data/app/stylesheets/tinymce/skins/content/alchemy-dark/content.scss +70 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy/skin.scss +28 -43
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/content.scss +1 -0
- data/app/stylesheets/tinymce/skins/ui/alchemy-dark/skin.scss +3784 -0
- data/app/views/alchemy/admin/attachments/_files_list.html.erb +20 -10
- data/app/views/alchemy/admin/attachments/assign.js.erb +4 -3
- data/app/views/alchemy/admin/attachments/show.html.erb +55 -43
- data/app/views/alchemy/admin/crop.html.erb +1 -1
- data/app/views/alchemy/admin/dashboard/index.html.erb +1 -1
- data/app/views/alchemy/admin/dashboard/info.html.erb +36 -6
- data/app/views/alchemy/admin/elements/_form.html.erb +9 -9
- data/app/views/alchemy/admin/elements/_header.html.erb +12 -10
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -1
- data/app/views/alchemy/admin/nodes/_form.html.erb +5 -1
- data/app/views/alchemy/admin/pages/info.html.erb +1 -1
- data/app/views/alchemy/admin/partials/_search_form.html.erb +1 -0
- data/app/views/alchemy/admin/pictures/_archive.html.erb +13 -23
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -6
- data/app/views/alchemy/admin/pictures/_form.html.erb +10 -5
- data/app/views/alchemy/admin/pictures/_infos.html.erb +21 -52
- data/app/views/alchemy/admin/pictures/_library_sidebar.html.erb +7 -0
- data/app/views/alchemy/admin/pictures/_picture.html.erb +15 -16
- data/app/views/alchemy/admin/pictures/_picture_to_assign.html.erb +20 -16
- data/app/views/alchemy/admin/pictures/_sorting_select.html.erb +13 -0
- data/app/views/alchemy/admin/pictures/_tag_list.html.erb +1 -1
- data/app/views/alchemy/admin/pictures/edit_multiple.html.erb +1 -6
- data/app/views/alchemy/admin/pictures/index.html.erb +3 -12
- data/app/views/alchemy/admin/pictures/show.html.erb +17 -14
- data/app/views/alchemy/admin/pictures/update.turbo_stream.erb +1 -1
- data/app/views/alchemy/admin/resources/_filter_bar.html.erb +5 -15
- data/app/views/alchemy/admin/resources/_resource_usage_info.html.erb +36 -0
- data/app/views/alchemy/admin/styleguide/index.html.erb +118 -66
- data/app/views/alchemy/admin/uploader/_button.html.erb +1 -1
- data/app/views/alchemy/base/error_notice.html.erb +1 -1
- data/app/views/alchemy/ingredients/_page_editor.html.erb +0 -1
- data/app/views/alchemy/ingredients/_richtext_editor.html.erb +0 -1
- data/app/views/alchemy/ingredients/_select_editor.html.erb +1 -2
- data/app/views/layouts/alchemy/admin.html.erb +25 -23
- data/config/locales/alchemy.en.yml +26 -8
- data/db/migrate/20250905140323_add_created_at_index_to_pictures_and_attachments.rb +14 -0
- data/lib/alchemy/configuration/base_option.rb +18 -5
- data/lib/alchemy/configuration/boolean_option.rb +2 -5
- data/lib/alchemy/configuration/collection_option.rb +69 -0
- data/lib/alchemy/configuration/configuration_option.rb +35 -0
- data/lib/alchemy/configuration/pathname_option.rb +12 -0
- data/lib/alchemy/configuration.rb +44 -6
- data/lib/alchemy/configurations/format_matchers.rb +1 -1
- data/lib/alchemy/configurations/importmap.rb +11 -0
- data/lib/alchemy/configurations/mailer.rb +2 -2
- data/lib/alchemy/configurations/main.rb +148 -3
- data/lib/alchemy/configurations/page_cache.rb +19 -0
- data/lib/alchemy/configurations/uploader.rb +2 -2
- data/lib/alchemy/deprecation.rb +1 -1
- data/lib/alchemy/engine.rb +43 -21
- data/lib/alchemy/install/tasks.rb +0 -12
- data/lib/alchemy/name_conversions.rb +6 -0
- data/lib/alchemy/tasks/tidy.rb +18 -0
- data/lib/alchemy/test_support/config_stubbing.rb +13 -4
- data/lib/alchemy/test_support/factories/language_factory.rb +8 -4
- data/lib/alchemy/test_support/factories/page_factory.rb +1 -0
- data/lib/alchemy/test_support/factories/picture_factory.rb +1 -0
- data/lib/alchemy/test_support/relatable_resource_examples.rb +58 -0
- data/lib/alchemy/tinymce.rb +0 -1
- data/lib/alchemy/version.rb +1 -1
- data/lib/alchemy.rb +18 -171
- data/lib/generators/alchemy/install/install_generator.rb +21 -10
- data/lib/generators/alchemy/install/templates/alchemy.rb.tt +88 -13
- data/lib/tasks/alchemy/assets.rake +1 -1
- data/lib/tasks/alchemy/tidy.rake +6 -0
- data/lib/tasks/alchemy/usage.rake +2 -0
- data/vendor/assets/stylesheets/tinymce/skins/content/dark/content.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/content/default/content.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/ui/oxide/skin.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/content.min.css +1 -0
- data/vendor/assets/stylesheets/tinymce/skins/ui/oxide-dark/skin.min.css +1 -0
- data/vendor/javascript/clipboard.min.js +1 -1
- data/vendor/javascript/cropperjs.min.js +1 -1
- data/vendor/javascript/handlebars.min.js +3 -3
- data/vendor/javascript/jquery.min.js +1 -1
- data/vendor/javascript/select2.min.js +3 -3
- data/vendor/javascript/shoelace.min.js +92 -76
- data/vendor/javascript/sortable.min.js +2 -2
- data/vendor/javascript/tinymce.min.js +1 -1
- data/vendor/javascript/ungap-custom-elements.min.js +2 -2
- metadata +51 -36
- data/CHANGELOG.md +0 -2100
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -73
- data/Gemfile +0 -78
- data/Rakefile +0 -102
- data/SECURITY.md +0 -13
- data/alchemy_cms.gemspec +0 -97
- data/app/assets/builds/alchemy/custom-properties.css +0 -1
- data/app/helpers/alchemy/admin/elements_helper.rb +0 -25
- data/app/stylesheets/alchemy/custom-properties.css +0 -244
- data/bin/importmap +0 -4
- data/bin/rails +0 -9
- data/bin/rspec +0 -3
- data/bin/setup +0 -30
- data/bin/start +0 -17
- data/bun.lockb +0 -0
- data/bundles/remixicon.mjs +0 -153
- data/bundles/shoelace.js +0 -12
- data/bundles/tinymce.js +0 -22
- data/eslint.config.js +0 -18
- data/lib/alchemy/configuration/class_set_option.rb +0 -46
- data/lib/alchemy/configuration/integer_list_option.rb +0 -13
- data/lib/alchemy/configuration/list_option.rb +0 -22
- data/lib/alchemy/configuration/string_list_option.rb +0 -13
- data/lib/alchemy/upgrader/.keep +0 -0
- data/lib/alchemy/upgrader/tasks/.keep +0 -0
- data/rollup.config.mjs +0 -108
- data/vitest.config.js +0 -21
|
@@ -21,10 +21,7 @@ class Datepicker extends AlchemyHTMLElement {
|
|
|
21
21
|
await import(`flatpickr/${locale}.js`)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
this.flatpickr = flatpickr(
|
|
25
|
-
this.getElementsByTagName("input")[0],
|
|
26
|
-
this.flatpickrOptions
|
|
27
|
-
)
|
|
24
|
+
this.flatpickr = flatpickr(this.inputField, this.flatpickrOptions)
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
disconnected() {
|
|
@@ -43,7 +40,9 @@ class Datepicker extends AlchemyHTMLElement {
|
|
|
43
40
|
noCalendar: this.inputType === "time",
|
|
44
41
|
time_24hr: translate("formats.time_24hr"),
|
|
45
42
|
onValueUpdate(_selectedDates, _dateStr, instance) {
|
|
46
|
-
instance.element
|
|
43
|
+
instance.element
|
|
44
|
+
.closest("alchemy-element-editor")
|
|
45
|
+
?.setDirty(this.inputField)
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
48
|
|
|
@@ -53,6 +52,10 @@ class Datepicker extends AlchemyHTMLElement {
|
|
|
53
52
|
|
|
54
53
|
return options
|
|
55
54
|
}
|
|
55
|
+
|
|
56
|
+
get inputField() {
|
|
57
|
+
return this.querySelector("input")
|
|
58
|
+
}
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
customElements.define("alchemy-datepicker", Datepicker)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import ajax from "alchemy_admin/utils/ajax"
|
|
1
2
|
import { removeTab } from "alchemy_admin/fixed_elements"
|
|
2
3
|
import { growl } from "alchemy_admin/growler"
|
|
3
4
|
import { reloadPreview } from "alchemy_admin/components/preview_window"
|
|
@@ -12,8 +13,8 @@ export class DeleteElementButton extends HTMLElement {
|
|
|
12
13
|
async handleEvent() {
|
|
13
14
|
const confirmed = await openConfirmDialog(this.message)
|
|
14
15
|
if (confirmed) {
|
|
15
|
-
const response = await
|
|
16
|
-
this.#removeElement(
|
|
16
|
+
const response = await ajax("DELETE", this.url)
|
|
17
|
+
this.#removeElement(response.data)
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -20,8 +20,10 @@ export class ElementEditor extends HTMLElement {
|
|
|
20
20
|
this.addEventListener("alchemy:element-update-title", this)
|
|
21
21
|
// We use of @rails/ujs for Rails remote forms
|
|
22
22
|
this.addEventListener("ajax:complete", this)
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
|
|
24
|
+
// Dirty observer still needs to be jQuery
|
|
25
|
+
// in order to support select2.
|
|
26
|
+
$(this).on("change", this.onChange)
|
|
25
27
|
|
|
26
28
|
this.header?.addEventListener("dblclick", () => {
|
|
27
29
|
this.toggle()
|
|
@@ -78,20 +80,22 @@ export class ElementEditor extends HTMLElement {
|
|
|
78
80
|
this.setTitle(event.detail.title)
|
|
79
81
|
}
|
|
80
82
|
break
|
|
81
|
-
case "change":
|
|
82
|
-
// SortableJS fires a native change event :/
|
|
83
|
-
// and we do not want to set the element editor dirty
|
|
84
|
-
// when this happens
|
|
85
|
-
if (event.target.classList.contains("nested-elements")) {
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
event.stopPropagation()
|
|
89
|
-
event.target.classList.add("dirty")
|
|
90
|
-
this.setDirty()
|
|
91
|
-
break
|
|
92
83
|
}
|
|
93
84
|
}
|
|
94
85
|
|
|
86
|
+
onChange(event) {
|
|
87
|
+
const target = event.target
|
|
88
|
+
// SortableJS fires a native change event :/
|
|
89
|
+
// and we do not want to set the element editor dirty
|
|
90
|
+
// when this happens
|
|
91
|
+
if (target.classList.contains("nested-elements")) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
this.setDirty(target)
|
|
95
|
+
event.stopPropagation()
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
/**
|
|
96
100
|
* Scrolls to and highlights element
|
|
97
101
|
* Expands if collapsed
|
|
@@ -227,11 +231,17 @@ export class ElementEditor extends HTMLElement {
|
|
|
227
231
|
|
|
228
232
|
/**
|
|
229
233
|
* Sets the element into dirty (unsafed) state
|
|
234
|
+
* @param {HTMLElement} editor
|
|
230
235
|
*/
|
|
231
|
-
setDirty() {
|
|
236
|
+
setDirty(editor) {
|
|
232
237
|
if (this.hasEditors) {
|
|
233
238
|
this.dirty = true
|
|
234
|
-
|
|
239
|
+
|
|
240
|
+
if (!window.onbeforeunload) {
|
|
241
|
+
window.onbeforeunload = (event) => event.preventDefault()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
editor?.closest(".ingredient-editor")?.classList.add("dirty")
|
|
235
245
|
}
|
|
236
246
|
}
|
|
237
247
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { hightlightTerm } from "alchemy_admin/components/remote_select"
|
|
2
|
+
|
|
3
|
+
const formatItem = (icon, text) => {
|
|
4
|
+
return `<div class="element-select-item">${icon} ${text}</div>`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
class ElementSelect extends HTMLInputElement {
|
|
8
|
+
constructor() {
|
|
9
|
+
super()
|
|
10
|
+
this.classList.add("alchemy_selectbox")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
connectedCallback() {
|
|
14
|
+
const el = this
|
|
15
|
+
const options = {
|
|
16
|
+
minimumResultsForSearch: 3,
|
|
17
|
+
dropdownAutoWidth: true,
|
|
18
|
+
data() {
|
|
19
|
+
return { results: JSON.parse(el.dataset.options) }
|
|
20
|
+
},
|
|
21
|
+
formatResult: (option, _el, search) => {
|
|
22
|
+
let text
|
|
23
|
+
|
|
24
|
+
if (option.id === "") return option.text
|
|
25
|
+
if (search.term !== "") {
|
|
26
|
+
text = hightlightTerm(option.text, search.term)
|
|
27
|
+
} else {
|
|
28
|
+
text = option.text
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return formatItem(option.icon, text)
|
|
32
|
+
},
|
|
33
|
+
formatSelection: (option) => {
|
|
34
|
+
return formatItem(option.icon, option.text)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
$(this).select2(options)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
customElements.define("alchemy-element-select", ElementSelect, {
|
|
42
|
+
extends: "input"
|
|
43
|
+
})
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
// ^ add support for top-level await in Terser
|
|
3
|
+
|
|
1
4
|
import "alchemy_admin/components/action"
|
|
2
5
|
import "alchemy_admin/components/attachment_select"
|
|
6
|
+
import "alchemy_admin/components/auto_submit"
|
|
3
7
|
import "alchemy_admin/components/button"
|
|
4
8
|
import "alchemy_admin/components/char_counter"
|
|
5
9
|
import "alchemy_admin/components/clipboard_button"
|
|
@@ -7,6 +11,7 @@ import "alchemy_admin/components/datepicker"
|
|
|
7
11
|
import "alchemy_admin/components/dialog_link"
|
|
8
12
|
import "alchemy_admin/components/dom_id_select"
|
|
9
13
|
import "alchemy_admin/components/element_editor"
|
|
14
|
+
import "alchemy_admin/components/element_select"
|
|
10
15
|
import "alchemy_admin/components/elements_window"
|
|
11
16
|
import "alchemy_admin/components/elements_window_handle"
|
|
12
17
|
import "alchemy_admin/components/list_filter"
|
|
@@ -27,7 +27,7 @@ class LinkButtons extends HTMLElement {
|
|
|
27
27
|
this.linkTargetField.value = data.target
|
|
28
28
|
|
|
29
29
|
this.unlinkButton.linked = true
|
|
30
|
-
this.
|
|
30
|
+
this.setElementDirty()
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
removeLink() {
|
|
@@ -40,7 +40,11 @@ class LinkButtons extends HTMLElement {
|
|
|
40
40
|
this.linkButton.classList.remove("linked")
|
|
41
41
|
this.unlinkButton.linked = false
|
|
42
42
|
|
|
43
|
-
this.
|
|
43
|
+
this.setElementDirty()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setElementDirty() {
|
|
47
|
+
this.elementEditor.setDirty(this)
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
get linkButton() {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
|
|
2
2
|
import { setupSelectLocale } from "alchemy_admin/i18n"
|
|
3
3
|
|
|
4
|
+
export function hightlightTerm(name, term) {
|
|
5
|
+
return name.replace(new RegExp(term, "gi"), (match) => `<em>${match}</em>`)
|
|
6
|
+
}
|
|
7
|
+
|
|
4
8
|
export class RemoteSelect extends AlchemyHTMLElement {
|
|
5
9
|
static properties = {
|
|
6
10
|
allowClear: { default: false },
|
|
@@ -148,6 +152,6 @@ export class RemoteSelect extends AlchemyHTMLElement {
|
|
|
148
152
|
* @private
|
|
149
153
|
*/
|
|
150
154
|
_hightlightTerm(name, term) {
|
|
151
|
-
return name
|
|
155
|
+
return hightlightTerm(name, term)
|
|
152
156
|
}
|
|
153
157
|
}
|
|
@@ -2,6 +2,9 @@ import "tinymce"
|
|
|
2
2
|
import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element"
|
|
3
3
|
import { currentLocale } from "alchemy_admin/i18n"
|
|
4
4
|
|
|
5
|
+
const DARK_THEME = "alchemy-dark"
|
|
6
|
+
const LIGHT_THEME = "alchemy"
|
|
7
|
+
|
|
5
8
|
class Tinymce extends AlchemyHTMLElement {
|
|
6
9
|
#min_height = null
|
|
7
10
|
|
|
@@ -32,6 +35,9 @@ class Tinymce extends AlchemyHTMLElement {
|
|
|
32
35
|
options
|
|
33
36
|
)
|
|
34
37
|
this.tinymceIntersectionObserver.observe(this)
|
|
38
|
+
|
|
39
|
+
// Set up theme change listener
|
|
40
|
+
this._setupThemeChangeListener()
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -42,6 +48,9 @@ class Tinymce extends AlchemyHTMLElement {
|
|
|
42
48
|
this.tinymceIntersectionObserver.disconnect()
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
// Remove theme change listener
|
|
52
|
+
this._removeThemeChangeListener()
|
|
53
|
+
|
|
45
54
|
tinymce.get(this.editorId)?.remove(this.editorId)
|
|
46
55
|
}
|
|
47
56
|
|
|
@@ -66,24 +75,84 @@ class Tinymce extends AlchemyHTMLElement {
|
|
|
66
75
|
*/
|
|
67
76
|
_initTinymceEditor() {
|
|
68
77
|
tinymce.init(this.configuration).then((editors) => {
|
|
69
|
-
editors.forEach((editor) =>
|
|
70
|
-
// mark the editor container as visible
|
|
71
|
-
// without these correction the editor remains hidden
|
|
72
|
-
// after a drag and drop action
|
|
73
|
-
editor.show()
|
|
74
|
-
|
|
75
|
-
// remove the spinner after the Tinymce initialized
|
|
76
|
-
this.getElementsByTagName("alchemy-spinner")[0].remove()
|
|
77
|
-
|
|
78
|
-
// event listener to mark the editor as dirty
|
|
79
|
-
if (this.elementEditor) {
|
|
80
|
-
editor.on("dirty", () => this.elementEditor.setDirty())
|
|
81
|
-
editor.on("click", () => this.elementEditor.onClickElement(false))
|
|
82
|
-
}
|
|
83
|
-
})
|
|
78
|
+
editors.forEach((editor) => this._setupEditor(editor))
|
|
84
79
|
})
|
|
85
80
|
}
|
|
86
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Setup editor after initialization
|
|
84
|
+
* @param {Object} editor - The TinyMCE editor instance
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
_setupEditor(editor) {
|
|
88
|
+
// mark the editor container as visible
|
|
89
|
+
// without these correction the editor remains hidden
|
|
90
|
+
// after a drag and drop action
|
|
91
|
+
editor.show()
|
|
92
|
+
|
|
93
|
+
// remove the spinner after the Tinymce initialized (only on first init)
|
|
94
|
+
const spinner = this.getElementsByTagName("alchemy-spinner")[0]
|
|
95
|
+
if (spinner) {
|
|
96
|
+
spinner.remove()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// event listener to mark the editor as dirty
|
|
100
|
+
if (this.elementEditor) {
|
|
101
|
+
editor.on("dirty", (evt) => {
|
|
102
|
+
this.elementEditor.setDirty(evt.target.editorContainer)
|
|
103
|
+
})
|
|
104
|
+
editor.on("click", () => this.elementEditor.onClickElement(false))
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Set up listener for OS theme changes
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
_setupThemeChangeListener() {
|
|
113
|
+
this.darkModeMediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
|
114
|
+
this.themeChangeHandler = (event) => this._handleThemeChange(event)
|
|
115
|
+
this.darkModeMediaQuery.addEventListener("change", this.themeChangeHandler)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Remove theme change listener
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
_removeThemeChangeListener() {
|
|
123
|
+
if (this.darkModeMediaQuery && this.themeChangeHandler) {
|
|
124
|
+
this.darkModeMediaQuery.removeEventListener(
|
|
125
|
+
"change",
|
|
126
|
+
this.themeChangeHandler
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Handle OS theme change and update TinyMCE skin
|
|
133
|
+
* @param {MediaQueryListEvent} event - The media query change event
|
|
134
|
+
* @private
|
|
135
|
+
*/
|
|
136
|
+
_handleThemeChange(event) {
|
|
137
|
+
const editor = tinymce.get(this.editorId)
|
|
138
|
+
if (editor) {
|
|
139
|
+
const skin = event.matches ? DARK_THEME : LIGHT_THEME
|
|
140
|
+
const content_css = event.matches ? DARK_THEME : LIGHT_THEME
|
|
141
|
+
|
|
142
|
+
// Update the skin by reinitializing the editor with new configuration
|
|
143
|
+
editor.remove()
|
|
144
|
+
tinymce
|
|
145
|
+
.init({
|
|
146
|
+
content_css,
|
|
147
|
+
...this.configuration,
|
|
148
|
+
skin
|
|
149
|
+
})
|
|
150
|
+
.then((editors) => {
|
|
151
|
+
editors.forEach((editor) => this._setupEditor(editor))
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
87
156
|
get configuration() {
|
|
88
157
|
const customConfig = {}
|
|
89
158
|
|
|
@@ -103,10 +172,12 @@ class Tinymce extends AlchemyHTMLElement {
|
|
|
103
172
|
})
|
|
104
173
|
|
|
105
174
|
const config = {
|
|
175
|
+
content_css: this.preferredTheme,
|
|
106
176
|
...Alchemy.TinymceDefaults,
|
|
107
177
|
...customConfig,
|
|
108
178
|
language: currentLocale(),
|
|
109
|
-
selector: `#${this.editorId}
|
|
179
|
+
selector: `#${this.editorId}`,
|
|
180
|
+
skin: this.preferredTheme
|
|
110
181
|
}
|
|
111
182
|
|
|
112
183
|
// Tinymce has a height of 400px by default
|
|
@@ -117,6 +188,12 @@ class Tinymce extends AlchemyHTMLElement {
|
|
|
117
188
|
return config
|
|
118
189
|
}
|
|
119
190
|
|
|
191
|
+
get preferredTheme() {
|
|
192
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
193
|
+
? DARK_THEME
|
|
194
|
+
: LIGHT_THEME
|
|
195
|
+
}
|
|
196
|
+
|
|
120
197
|
get editorId() {
|
|
121
198
|
return this.editor.id
|
|
122
199
|
}
|
|
@@ -233,7 +233,7 @@ export class Dialog {
|
|
|
233
233
|
this.dialog_header = $('<div class="alchemy-dialog-header" />')
|
|
234
234
|
this.dialog_title = $('<div class="alchemy-dialog-title" />')
|
|
235
235
|
this.close_button = $(
|
|
236
|
-
'<
|
|
236
|
+
'<button class="alchemy-dialog-close"><alchemy-icon name="close"></alchemy-icon></button>'
|
|
237
237
|
)
|
|
238
238
|
this.dialog_title.text(this.options.title)
|
|
239
239
|
this.dialog_header.append(this.dialog_title)
|
|
@@ -16,7 +16,7 @@ class FileEditor {
|
|
|
16
16
|
this.fileIcon.innerHTML = ""
|
|
17
17
|
this.fileName.innerHTML = ""
|
|
18
18
|
this.deleteLink.classList.add("hidden")
|
|
19
|
-
this.container.closest("alchemy-element-editor").setDirty()
|
|
19
|
+
this.container.closest("alchemy-element-editor").setDirty(this.formField)
|
|
20
20
|
return false
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Shows spinner while loading images and
|
|
2
2
|
// fades the image after its been loaded
|
|
3
3
|
|
|
4
|
+
import Spinner from "alchemy_admin/spinner"
|
|
5
|
+
|
|
4
6
|
export default class ImageLoader {
|
|
5
7
|
static init(scope = document) {
|
|
6
8
|
if (typeof scope === "string") {
|
|
@@ -15,7 +17,7 @@ export default class ImageLoader {
|
|
|
15
17
|
constructor(image) {
|
|
16
18
|
this.image = image
|
|
17
19
|
this.parent = image.parentNode
|
|
18
|
-
this.spinner = new
|
|
20
|
+
this.spinner = new Spinner("small")
|
|
19
21
|
this.bind()
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -28,7 +30,7 @@ export default class ImageLoader {
|
|
|
28
30
|
if (!force && this.image.complete) return
|
|
29
31
|
|
|
30
32
|
this.image.classList.add("loading")
|
|
31
|
-
this.spinner.spin(this.
|
|
33
|
+
this.spinner.spin(this.parent)
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
onLoaded() {
|
|
@@ -5,7 +5,7 @@ import { growl } from "alchemy_admin/growler"
|
|
|
5
5
|
import ImageLoader from "alchemy_admin/image_loader"
|
|
6
6
|
|
|
7
7
|
const UPDATE_DELAY = 125
|
|
8
|
-
const IMAGE_PLACEHOLDER = '<alchemy-icon name="image"></alchemy-icon>'
|
|
8
|
+
const IMAGE_PLACEHOLDER = '<alchemy-icon name="image" size="xl"></alchemy-icon>'
|
|
9
9
|
const THUMBNAIL_SIZE = "160x120"
|
|
10
10
|
|
|
11
11
|
export class PictureEditor {
|
|
@@ -75,6 +75,7 @@ export class PictureEditor {
|
|
|
75
75
|
this.image.src = data.url
|
|
76
76
|
this.image.alt = data.alt
|
|
77
77
|
this.image.title = data.title
|
|
78
|
+
this.setElementDirty()
|
|
78
79
|
})
|
|
79
80
|
.catch((error) => {
|
|
80
81
|
console.error(error.message || error)
|
|
@@ -83,8 +84,6 @@ export class PictureEditor {
|
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
ensureImage() {
|
|
86
|
-
if (this.image) return
|
|
87
|
-
|
|
88
87
|
const img = new Image()
|
|
89
88
|
this.thumbnailBackground.replaceChildren(img)
|
|
90
89
|
this.image = img
|
|
@@ -96,7 +95,11 @@ export class PictureEditor {
|
|
|
96
95
|
this.pictureIdField.value = ""
|
|
97
96
|
this.image = null
|
|
98
97
|
this.cropLink.classList.add("disabled")
|
|
99
|
-
this.
|
|
98
|
+
this.setElementDirty()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setElementDirty() {
|
|
102
|
+
this.container.closest(".element-editor").setDirty(this.container)
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
updateCropLink() {
|
|
@@ -15,13 +15,13 @@ function checkedInputs() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function editMultiplePicturesUrl(href) {
|
|
18
|
-
const
|
|
18
|
+
const url = new URL(href)
|
|
19
|
+
|
|
19
20
|
checkedInputs().forEach((entry) =>
|
|
20
|
-
|
|
21
|
+
url.searchParams.append(entry.name, entry.value)
|
|
21
22
|
)
|
|
22
|
-
const url = href + "?" + searchParameters.toString()
|
|
23
23
|
|
|
24
|
-
return url
|
|
24
|
+
return url.toString()
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -23,19 +23,12 @@ module Alchemy
|
|
|
23
23
|
include Alchemy::NameConversions
|
|
24
24
|
include Alchemy::Taggable
|
|
25
25
|
include Alchemy::TouchElements
|
|
26
|
+
include Alchemy::RelatableResource
|
|
26
27
|
|
|
27
28
|
include Alchemy.storage_adapter.attachment_class_methods
|
|
28
29
|
|
|
29
30
|
stampable stamper_class_name: Alchemy.user_class_name
|
|
30
31
|
|
|
31
|
-
has_many :file_ingredients,
|
|
32
|
-
class_name: "Alchemy::Ingredients::File",
|
|
33
|
-
foreign_key: "related_object_id",
|
|
34
|
-
inverse_of: :related_object
|
|
35
|
-
|
|
36
|
-
has_many :elements, through: :file_ingredients
|
|
37
|
-
has_many :pages, through: :elements
|
|
38
|
-
|
|
39
32
|
scope :by_file_type, ->(file_type) do
|
|
40
33
|
Alchemy.storage_adapter.by_file_type_scope(file_type)
|
|
41
34
|
end
|
|
@@ -86,7 +79,7 @@ module Alchemy
|
|
|
86
79
|
end
|
|
87
80
|
|
|
88
81
|
def ransackable_scopes(_auth_object = nil)
|
|
89
|
-
%i[by_file_type recent last_upload without_tag]
|
|
82
|
+
%i[by_file_type recent last_upload without_tag deletable]
|
|
90
83
|
end
|
|
91
84
|
end
|
|
92
85
|
|
|
@@ -21,6 +21,7 @@ module Alchemy
|
|
|
21
21
|
attribute :message
|
|
22
22
|
attribute :warning
|
|
23
23
|
attribute :hint
|
|
24
|
+
attribute :icon
|
|
24
25
|
|
|
25
26
|
validates :name,
|
|
26
27
|
presence: true,
|
|
@@ -28,8 +29,14 @@ module Alchemy
|
|
|
28
29
|
with: /\A[a-z_-]+\z/
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
validates :icon,
|
|
33
|
+
format: {with: /\A[\w-]+\z/i},
|
|
34
|
+
if: -> { icon.is_a?(String) }
|
|
35
|
+
|
|
31
36
|
delegate :blank?, to: :name
|
|
32
37
|
|
|
38
|
+
DEFAULT_ICON_NAME = "layout-bottom-2-line"
|
|
39
|
+
|
|
33
40
|
class << self
|
|
34
41
|
# Returns the definitions from elements.yml file.
|
|
35
42
|
#
|
|
@@ -151,8 +158,32 @@ module Alchemy
|
|
|
151
158
|
end
|
|
152
159
|
end
|
|
153
160
|
|
|
161
|
+
def icon_file
|
|
162
|
+
@_icon_file ||= File.read(icon_file_path).html_safe
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def icon_file_name
|
|
166
|
+
"#{icon_name}.svg"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def icon_name
|
|
170
|
+
case icon
|
|
171
|
+
when TrueClass then name
|
|
172
|
+
when String then icon
|
|
173
|
+
else DEFAULT_ICON_NAME
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
154
177
|
private
|
|
155
178
|
|
|
179
|
+
def icon_file_path
|
|
180
|
+
icons_root_path.join("app/assets/images/alchemy/element_icons", icon_file_name)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def icons_root_path
|
|
184
|
+
icon.nil? ? Alchemy::Engine.root : Rails.root
|
|
185
|
+
end
|
|
186
|
+
|
|
156
187
|
def hint_translation_scope
|
|
157
188
|
:element_hints
|
|
158
189
|
end
|
|
@@ -6,7 +6,8 @@ module Alchemy
|
|
|
6
6
|
#
|
|
7
7
|
class Boolean < Alchemy::Ingredient
|
|
8
8
|
def value
|
|
9
|
-
|
|
9
|
+
val = self[:value].nil? ? definition.default : self[:value]
|
|
10
|
+
ActiveRecord::Type::Boolean.new.cast(val)
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
# The localized value
|
|
@@ -25,8 +25,8 @@ require_dependency "alchemy/site"
|
|
|
25
25
|
module Alchemy
|
|
26
26
|
class Language < BaseRecord
|
|
27
27
|
belongs_to :site
|
|
28
|
-
has_many :pages, inverse_of: :language
|
|
29
|
-
has_many :nodes, inverse_of: :language
|
|
28
|
+
has_many :pages, inverse_of: :language, dependent: :restrict_with_error
|
|
29
|
+
has_many :nodes, inverse_of: :language, dependent: :restrict_with_error
|
|
30
30
|
|
|
31
31
|
before_validation :set_locale, if: -> { locale.blank? }
|
|
32
32
|
|
|
@@ -54,11 +54,6 @@ module Alchemy
|
|
|
54
54
|
after_update :set_pages_language,
|
|
55
55
|
if: :should_set_pages_language?
|
|
56
56
|
|
|
57
|
-
before_destroy if: -> { pages.any? } do
|
|
58
|
-
errors.add(:pages, :still_present)
|
|
59
|
-
throw(:abort)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
57
|
scope :published, -> { where(public: true) }
|
|
63
58
|
scope :with_root_page, -> { joins(:pages).where(Page.table_name => {language_root: true}) }
|
|
64
59
|
|
|
@@ -5,6 +5,7 @@ module Alchemy
|
|
|
5
5
|
module PageNaming
|
|
6
6
|
extend ActiveSupport::Concern
|
|
7
7
|
include NameConversions
|
|
8
|
+
|
|
8
9
|
RESERVED_URLNAMES = %w[admin messages new]
|
|
9
10
|
|
|
10
11
|
included do
|
|
@@ -16,8 +17,7 @@ module Alchemy
|
|
|
16
17
|
presence: true, uniqueness: {scope: [:parent_id], case_sensitive: false, unless: -> { parent_id.nil? }}
|
|
17
18
|
validates :urlname,
|
|
18
19
|
uniqueness: {scope: [:language_id, :layoutpage], if: -> { urlname.present? }, case_sensitive: false},
|
|
19
|
-
exclusion: {in: RESERVED_URLNAMES}
|
|
20
|
-
length: {minimum: 3, if: -> { urlname.present? }}
|
|
20
|
+
exclusion: {in: RESERVED_URLNAMES}
|
|
21
21
|
|
|
22
22
|
before_save :set_title,
|
|
23
23
|
if: -> { title.blank? }
|
|
@@ -73,17 +73,10 @@ module Alchemy
|
|
|
73
73
|
self[:title] = name
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# Names shorter than 3 will be filled up with dashes,
|
|
79
|
-
# so it does not collidate with the language code.
|
|
76
|
+
# Returns the full nested urlname.
|
|
80
77
|
#
|
|
81
|
-
def converted_url_name
|
|
82
|
-
url_name = convert_to_urlname(slug.blank? ? name : slug)
|
|
83
|
-
url_name.rjust(3, "-")
|
|
84
|
-
end
|
|
85
|
-
|
|
86
78
|
def nested_url_name
|
|
79
|
+
converted_url_name = convert_to_urlname(slug.blank? ? name : slug)
|
|
87
80
|
if parent&.language_root?
|
|
88
81
|
converted_url_name
|
|
89
82
|
else
|