alchemy_cms 8.0.0.a → 8.0.0.b
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.
Potentially problematic release.
This version of alchemy_cms might be problematic. Click here for more details.
- 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/default.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/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 +2 -0
- data/app/controllers/alchemy/admin/pictures_controller.rb +19 -33
- data/app/controllers/alchemy/pages_controller.rb +19 -2
- data/app/controllers/concerns/alchemy/admin/resource_filter.rb +1 -0
- data/app/helpers/alchemy/admin/attachments_helper.rb +5 -5
- 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 +30 -0
- data/app/models/alchemy/ingredient.rb +1 -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.rb +1 -6
- data/app/models/alchemy/page_definition.rb +1 -1
- data/app/models/alchemy/picture.rb +6 -17
- 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/dialogs.scss +17 -7
- data/app/stylesheets/alchemy/admin/element-select.scss +11 -0
- data/app/stylesheets/alchemy/admin/elements.scss +94 -33
- 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/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 +1 -1
- 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/elements/_form.html.erb +9 -9
- data/app/views/alchemy/admin/elements/_header.html.erb +4 -1
- data/app/views/alchemy/admin/ingredients/_picture_fields.html.erb +1 -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 +12 -22
- data/app/views/alchemy/admin/pictures/_archive_overlay.html.erb +1 -6
- data/app/views/alchemy/admin/pictures/_form.html.erb +1 -1
- 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 +14 -20
- 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 +10 -5
- 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/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 +22 -17
- 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/configurations/format_matchers.rb +1 -1
- data/lib/alchemy/configurations/main.rb +7 -0
- data/lib/alchemy/configurations/page_cache.rb +19 -0
- data/lib/alchemy/engine.rb +16 -7
- 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/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 +2 -11
- data/lib/generators/alchemy/install/install_generator.rb +21 -10
- data/lib/generators/alchemy/install/templates/alchemy.rb.tt +10 -6
- 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 +46 -32
- 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/upgrader/.keep +0 -0
- data/lib/alchemy/upgrader/tasks/.keep +0 -0
- data/rollup.config.mjs +0 -108
- data/vitest.config.js +0 -21
@@ -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,6 +29,10 @@ 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
|
|
33
38
|
class << self
|
@@ -151,8 +156,33 @@ module Alchemy
|
|
151
156
|
end
|
152
157
|
end
|
153
158
|
|
159
|
+
def icon_file
|
160
|
+
@_icon_file ||= File.read(icon_file_path).html_safe
|
161
|
+
end
|
162
|
+
|
163
|
+
def icon_file_name
|
164
|
+
"#{icon_name}.svg"
|
165
|
+
end
|
166
|
+
|
167
|
+
def icon_name
|
168
|
+
case icon
|
169
|
+
when TrueClass then name
|
170
|
+
when String then icon
|
171
|
+
else
|
172
|
+
"default"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
154
176
|
private
|
155
177
|
|
178
|
+
def icon_file_path
|
179
|
+
icons_root_path.join("app/assets/images/alchemy/element_icons", icon_file_name)
|
180
|
+
end
|
181
|
+
|
182
|
+
def icons_root_path
|
183
|
+
icon.nil? ? Alchemy::Engine.root : Rails.root
|
184
|
+
end
|
185
|
+
|
156
186
|
def hint_translation_scope
|
157
187
|
:element_hints
|
158
188
|
end
|
@@ -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
|
@@ -13,8 +13,21 @@ module Alchemy
|
|
13
13
|
language.public? && !!public_version&.public?
|
14
14
|
end
|
15
15
|
|
16
|
+
# Cache-Control max-age duration in seconds.
|
17
|
+
#
|
18
|
+
# You can set this via the `ALCHEMY_PAGE_CACHE_MAX_AGE` environment variable,
|
19
|
+
# in the `Alchemy.config.page_cache_max_age` configuration option,
|
20
|
+
# or in the pages definition in `config/alchemy/page_layouts.yml` file.
|
21
|
+
#
|
22
|
+
# Defaults to 600 seconds.
|
16
23
|
def expiration_time
|
17
|
-
|
24
|
+
return 0 unless cache_page?
|
25
|
+
|
26
|
+
if definition.cache.to_s.match?(/\d+/)
|
27
|
+
definition.cache.to_i
|
28
|
+
else
|
29
|
+
Alchemy.config.page_cache.max_age
|
30
|
+
end
|
18
31
|
end
|
19
32
|
|
20
33
|
def rootpage?
|
@@ -144,17 +157,9 @@ module Alchemy
|
|
144
157
|
# @returns Boolean
|
145
158
|
#
|
146
159
|
def cache_page?
|
147
|
-
return false
|
148
|
-
|
149
|
-
page_layout = PageDefinition.get(self.page_layout)
|
150
|
-
page_layout.cache != false && page_layout.searchresults != true
|
151
|
-
end
|
152
|
-
|
153
|
-
private
|
160
|
+
return false if !public? || restricted?
|
154
161
|
|
155
|
-
|
156
|
-
Alchemy.config.cache_pages &&
|
157
|
-
Rails.application.config.action_controller.perform_caching
|
162
|
+
definition.cache != false && definition.searchresults != true
|
158
163
|
end
|
159
164
|
end
|
160
165
|
end
|
data/app/models/alchemy/page.rb
CHANGED
@@ -115,7 +115,7 @@ module Alchemy
|
|
115
115
|
has_many :site_languages, through: :site, source: :languages
|
116
116
|
has_many :folded_pages, dependent: :destroy
|
117
117
|
has_many :legacy_urls, class_name: "Alchemy::LegacyPageUrl", dependent: :destroy
|
118
|
-
has_many :nodes, class_name: "Alchemy::Node", inverse_of: :page
|
118
|
+
has_many :nodes, class_name: "Alchemy::Node", inverse_of: :page, dependent: :restrict_with_error
|
119
119
|
has_many :versions, class_name: "Alchemy::PageVersion", inverse_of: :page, dependent: :destroy
|
120
120
|
has_one :draft_version, -> { drafts }, class_name: "Alchemy::PageVersion"
|
121
121
|
has_one :public_version, -> { published }, class_name: "Alchemy::PageVersion", autosave: -> { persisted? }
|
@@ -132,11 +132,6 @@ module Alchemy
|
|
132
132
|
before_create -> { versions.build },
|
133
133
|
if: -> { versions.none? }
|
134
134
|
|
135
|
-
before_destroy if: -> { nodes.any? } do
|
136
|
-
errors.add(:nodes, :still_present)
|
137
|
-
throw(:abort)
|
138
|
-
end
|
139
|
-
|
140
135
|
before_save :set_language_code,
|
141
136
|
if: -> { language.present? }
|
142
137
|
|
@@ -13,7 +13,7 @@ module Alchemy
|
|
13
13
|
attribute :autogenerate, default: []
|
14
14
|
attribute :layoutpage, :boolean, default: false
|
15
15
|
attribute :unique, :boolean, default: false
|
16
|
-
attribute :cache,
|
16
|
+
attribute :cache, default: true
|
17
17
|
attribute :insert_elements_at, :string, default: "bottom"
|
18
18
|
attribute :fixed_attributes, default: {}
|
19
19
|
attribute :searchable, :boolean, default: true
|
@@ -32,14 +32,8 @@ module Alchemy
|
|
32
32
|
include Alchemy::NameConversions
|
33
33
|
include Alchemy::Taggable
|
34
34
|
include Alchemy::TouchElements
|
35
|
+
include Alchemy::RelatableResource
|
35
36
|
|
36
|
-
has_many :picture_ingredients,
|
37
|
-
class_name: "Alchemy::Ingredients::Picture",
|
38
|
-
foreign_key: "related_object_id",
|
39
|
-
inverse_of: :related_object
|
40
|
-
|
41
|
-
has_many :elements, through: :picture_ingredients
|
42
|
-
has_many :pages, through: :elements
|
43
37
|
has_many :descriptions, class_name: "Alchemy::PictureDescription", dependent: :destroy
|
44
38
|
|
45
39
|
accepts_nested_attributes_for :descriptions, allow_destroy: true, reject_if: ->(attr) { attr[:text].blank? }
|
@@ -86,15 +80,16 @@ module Alchemy
|
|
86
80
|
|
87
81
|
scope :named, ->(name) { where("#{table_name}.name LIKE ?", "%#{name}%") }
|
88
82
|
scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
|
89
|
-
scope :deletable,
|
90
|
-
-> {
|
91
|
-
where("#{table_name}.id NOT IN (SELECT related_object_id FROM alchemy_ingredients WHERE related_object_type = 'Alchemy::Picture')")
|
92
|
-
}
|
93
83
|
scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }
|
94
84
|
scope :by_file_format, ->(file_format) do
|
95
85
|
Alchemy.storage_adapter.by_file_format_scope(file_format)
|
96
86
|
end
|
97
87
|
|
88
|
+
# Case insensitive Ransack searching and sorting for name attribute
|
89
|
+
ransacker :name, type: :string do
|
90
|
+
arel_table[:name].lower
|
91
|
+
end
|
92
|
+
|
98
93
|
# Class methods
|
99
94
|
|
100
95
|
class << self
|
@@ -251,12 +246,6 @@ module Alchemy
|
|
251
246
|
pages.any? && pages.not_restricted.blank?
|
252
247
|
end
|
253
248
|
|
254
|
-
# Returns true if picture is not assigned to any Picture ingredient.
|
255
|
-
#
|
256
|
-
def deletable?
|
257
|
-
picture_ingredients.empty?
|
258
|
-
end
|
259
|
-
|
260
249
|
def image_file_name
|
261
250
|
Alchemy.storage_adapter.image_file_name(self)
|
262
251
|
end
|