maglevcms 3.0.0.beta2 → 3.0.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 +16 -53
- data/Rakefile +3 -1
- data/app/assets/builds/maglev/tailwind.css +681 -11670
- data/app/assets/config/maglev_manifest.js +3 -2
- data/app/assets/javascripts/maglev/client/dom-operations.js +5 -5
- data/app/assets/javascripts/maglev/client/iframe-decorator.js +24 -0
- data/app/assets/javascripts/maglev/client/incoming-messages.js +13 -3
- data/app/assets/javascripts/maglev/client/index.js +3 -2
- data/app/assets/javascripts/maglev/client/utils.js +22 -0
- data/app/assets/javascripts/maglev/editor/controllers/app/forms/section_form_controller.js +4 -3
- data/app/assets/javascripts/maglev/editor/controllers/app/forms/style_form_controller.js +2 -1
- data/app/assets/javascripts/maglev/editor/controllers/app/page_preview_controller.js +96 -5
- data/app/assets/javascripts/maglev/editor/controllers/app/preview_notification_center_controller.js +105 -23
- data/app/assets/javascripts/maglev/editor/controllers/app/setting_controller.js +3 -2
- data/app/assets/javascripts/maglev/editor/controllers/shared/copy_to_clipboard_controller.js +2 -1
- data/app/assets/javascripts/maglev/editor/controllers/shared/submit_button_controller.js +29 -5
- data/app/assets/javascripts/maglev/editor/controllers/utils.js +38 -0
- data/app/assets/javascripts/maglev/editor/index.js +5 -44
- data/app/assets/javascripts/maglev/editor/patches/page_renderer_patch.js +47 -0
- data/app/assets/javascripts/maglev/editor/patches/turbo_delayed_streams.js +35 -0
- data/app/assets/javascripts/maglev/editor/patches/turbo_stream_patch.js +50 -0
- data/app/assets/stylesheets/maglev/application.css +0 -2
- data/app/assets/stylesheets/maglev/tailwind.css.erb +11 -2
- data/app/components/maglev/content/link.rb +1 -1
- data/app/components/maglev/editor/settings/link/link_component.rb +7 -1
- data/app/components/maglev/section_component.rb +11 -1
- data/app/components/maglev/uikit/app_layout/sidebar/link_component.html.erb +1 -1
- data/app/components/maglev/uikit/app_layout/sidebar/link_component.rb +35 -5
- data/app/components/maglev/uikit/app_layout/sidebar_component.html.erb +3 -4
- data/app/components/maglev/uikit/app_layout/topbar/logo_component.html.erb +1 -1
- data/app/components/maglev/uikit/app_layout/topbar/page_info_component.html.erb +1 -1
- data/app/components/maglev/uikit/app_layout/topbar_component.html.erb +1 -1
- data/app/components/maglev/uikit/app_layout/topbar_component.rb +1 -1
- data/app/components/maglev/uikit/button_group_component/button_group_component.html.erb +5 -0
- data/app/components/maglev/uikit/button_group_component.rb +70 -0
- data/app/components/maglev/uikit/device_toggler_component.rb +10 -1
- data/app/components/maglev/uikit/dropdown_component/dropdown_component.html.erb +2 -2
- data/app/components/maglev/uikit/dropdown_component/dropdown_controller.js +6 -1
- data/app/components/maglev/uikit/dropdown_component.rb +6 -1
- data/app/components/maglev/uikit/form/color_field_component.html.erb +1 -1
- data/app/components/maglev/uikit/form/combobox_component.html.erb +1 -0
- data/app/components/maglev/uikit/form/link_component.rb +5 -1
- data/app/components/maglev/uikit/form/richtext_controller.js +5 -4
- data/app/components/maglev/uikit/form/search_form_component.html.erb +1 -0
- data/app/components/maglev/uikit/icon_component.rb +3 -0
- data/app/components/maglev/uikit/image_library/uploader_controller.js +3 -2
- data/app/components/maglev/uikit/list/list_item_component.html.erb +36 -19
- data/app/components/maglev/uikit/list/list_item_component.rb +19 -5
- data/app/components/maglev/uikit/locale_switcher_component/locale_switcher_component.html.erb +6 -10
- data/app/components/maglev/uikit/menu_dropdown_component/menu_dropdown_component.html.erb +6 -2
- data/app/components/maglev/uikit/menu_dropdown_component.rb +244 -7
- data/app/components/maglev/uikit/page_actions_dropdown_component/page_actions_dropdown_component.html.erb +39 -46
- data/app/components/maglev/uikit/pagination_component/pagination_component.html.erb +9 -12
- data/app/components/maglev/uikit/pagination_component.rb +6 -1
- data/app/components/maglev/uikit/section_toolbar/bottom_component.html.erb +1 -1
- data/app/components/maglev/uikit/tabs_component/tabs_component.html.erb +7 -4
- data/app/components/maglev/uikit/tabs_component.rb +23 -4
- data/app/components/maglev/uikit/well/simple_well_component.html.erb +15 -0
- data/app/components/maglev/uikit/well/simple_well_component.rb +13 -0
- data/app/controllers/concerns/maglev/editor/errors_concern.rb +9 -9
- data/app/controllers/concerns/maglev/editor/preview_urls_concern.rb +32 -0
- data/app/controllers/concerns/maglev/editor/turbo_concern.rb +29 -0
- data/app/controllers/concerns/maglev/errors_concern.rb +17 -0
- data/app/controllers/concerns/maglev/flash_i18n_concern.rb +1 -0
- data/app/controllers/maglev/application_controller.rb +2 -1
- data/app/controllers/maglev/assets/active_storage_proxy_controller.rb +2 -1
- data/app/controllers/maglev/editor/assets_controller.rb +1 -1
- data/app/controllers/maglev/editor/base_controller.rb +6 -32
- data/app/controllers/maglev/editor/pages/clone_controller.rb +22 -0
- data/app/controllers/maglev/editor/pages/discard_draft_controller.rb +17 -0
- data/app/controllers/maglev/editor/pages_controller.rb +26 -7
- data/app/controllers/maglev/editor/section_blocks_controller.rb +13 -9
- data/app/controllers/maglev/editor/sections_controller.rb +26 -7
- data/app/controllers/maglev/published_page_preview_controller.rb +4 -0
- data/app/controllers/maglev/site_controller.rb +15 -0
- data/app/helpers/maglev/application_helper.rb +26 -8
- data/app/helpers/maglev/editor/section_blocks_helper.rb +2 -2
- data/app/models/maglev/page/publishable_concern.rb +69 -0
- data/app/models/maglev/page.rb +3 -0
- data/app/models/maglev/section/block.rb +4 -0
- data/app/models/maglev/section/content_concern.rb +3 -1
- data/app/models/maglev/section.rb +21 -1
- data/app/models/maglev/sections_content_store.rb +1 -3
- data/app/models/maglev/site.rb +5 -0
- data/app/services/concerns/maglev/content/helpers_concern.rb +5 -8
- data/app/services/maglev/app_container.rb +4 -2
- data/app/services/maglev/discard_page_draft_service.rb +45 -0
- data/app/services/maglev/fetch_section_screenshot_url.rb +12 -1
- data/app/services/maglev/fetch_site.rb +3 -1
- data/app/services/maglev/get_published_page_sections_service.rb +1 -1
- data/app/services/maglev/has_unpublished_changes.rb +21 -0
- data/app/services/maglev/publish_service.rb +18 -2
- data/app/views/layouts/maglev/editor/_sidebar.html.erb +8 -2
- data/app/views/layouts/maglev/editor/_topbar.html.erb +6 -23
- data/app/views/layouts/maglev/editor/application.html.erb +6 -0
- data/app/views/layouts/maglev/editor/topbar/_page_info.html.erb +11 -0
- data/app/views/layouts/maglev/editor/topbar/_publish_button.html.erb +35 -0
- data/app/views/maglev/editor/assets/index.html.erb +1 -0
- data/app/views/maglev/editor/home/index.html.erb +1 -1
- data/app/views/maglev/editor/links/edit/_email.html.erb +1 -1
- data/app/views/maglev/editor/links/edit/_url.html.erb +1 -1
- data/app/views/maglev/editor/pages/_list.html.erb +25 -21
- data/app/views/maglev/editor/pages/_preview.html.erb +6 -6
- data/app/views/maglev/editor/pages/_preview_empty_message.html.erb +13 -10
- data/app/views/maglev/editor/pages/discard_draft/create.turbo_stream.erb +16 -0
- data/app/views/maglev/editor/pages/index.html.erb +8 -1
- data/app/views/maglev/editor/publication/create.turbo_stream.erb +3 -1
- data/app/views/maglev/editor/section_blocks/_form.html.erb +5 -13
- data/app/views/maglev/editor/section_blocks/_form_with_tabs.html.erb +15 -0
- data/app/views/maglev/editor/section_blocks/_new.html.erb +1 -1
- data/app/views/maglev/editor/section_blocks/edit.html.erb +2 -2
- data/app/views/maglev/editor/section_blocks/index/_list.html.erb +2 -2
- data/app/views/maglev/editor/section_blocks/index/_tree.html.erb +1 -1
- data/app/views/maglev/editor/section_blocks/update.turbo_stream.erb +9 -1
- data/app/views/maglev/editor/sections/_form.html.erb +6 -20
- data/app/views/maglev/editor/sections/_form_with_tabs.html.erb +21 -0
- data/app/views/maglev/editor/sections/_list.html.erb +1 -1
- data/app/views/maglev/editor/sections/edit.html.erb +3 -3
- data/app/views/maglev/editor/sections/index.html.erb +1 -1
- data/app/views/maglev/editor/sections/new.html.erb +18 -1
- data/app/views/maglev/editor/sections/theme/_empty_list.html.erb +3 -0
- data/app/views/maglev/editor/sections/theme/_list.html.erb +35 -0
- data/app/views/maglev/editor/sections/theme/_screenshot_placeholder.html.erb +6 -0
- data/app/views/maglev/editor/sections/theme/_search.html.erb +22 -0
- data/app/views/maglev/editor/sections/update.turbo_stream.erb +9 -1
- data/app/views/maglev/editor/shared/_button_label.html.erb +4 -4
- data/app/views/maglev/editor/style/edit.html.erb +1 -0
- data/app/views/maglev/errors/site_not_found.html.erb +33 -0
- data/config/editor_importmap.rb +16 -13
- data/config/locales/editor.ar.yml +12 -4
- data/config/locales/editor.en.yml +31 -23
- data/config/locales/editor.es.yml +12 -4
- data/config/locales/editor.fr.yml +12 -4
- data/config/locales/editor.pt-BR.yml +12 -4
- data/config/routes/maglev/assets.rb +4 -0
- data/config/routes/maglev/editor.rb +38 -0
- data/config/routes/maglev/preview.rb +8 -0
- data/config/routes/maglev/public_preview.rb +6 -0
- data/config/routes.rb +8 -44
- data/db/migrate/20211013210954_translate_section_content.rb +1 -0
- data/db/migrate/20251116171603_add_published_at_to_sites_and_pages.rb +6 -0
- data/db/migrate/20260114112058_add_published_payload_to_pages.rb +14 -0
- data/exe/tailwind-cli +1 -1
- data/lib/generators/maglev/install_generator.rb +9 -7
- data/lib/generators/maglev/templates/install/config/initializers/maglev.rb +10 -3
- data/lib/maglev/active_storage/serving_blob.rb +29 -0
- data/lib/maglev/active_storage.rb +2 -0
- data/lib/maglev/config.rb +22 -3
- data/lib/maglev/engine.rb +15 -10
- data/lib/maglev/errors.rb +1 -0
- data/lib/maglev/version.rb +1 -1
- data/lib/maglev.rb +18 -3
- data/lib/tasks/db_test_all.rake +290 -0
- data/lib/tasks/maglev/tailwindcss.rake +1 -0
- metadata +55 -19
- data/app/controllers/maglev/editor/page_clone_controller.rb +0 -20
- data/app/views/maglev/editor/sections/_theme_list.html.erb +0 -32
- /data/vendor/javascript/{@floating-ui--core.js → maglev/@floating-ui--core.js} +0 -0
- /data/vendor/javascript/{@floating-ui--dom.js → maglev/@floating-ui--dom.js} +0 -0
- /data/vendor/javascript/{@floating-ui--utils--dom.js → maglev/@floating-ui--utils--dom.js} +0 -0
- /data/vendor/javascript/{@floating-ui--utils.js → maglev/@floating-ui--utils.js} +0 -0
- /data/vendor/javascript/{@hotwired--stimulus.js → maglev/@hotwired--stimulus.js} +0 -0
- /data/vendor/javascript/{@hotwired--turbo-rails.js → maglev/@hotwired--turbo-rails.js} +0 -0
- /data/vendor/javascript/{@hotwired--turbo.js → maglev/@hotwired--turbo.js} +0 -0
- /data/vendor/javascript/{@rails--actioncable--src.js → maglev/@rails--actioncable--src.js} +0 -0
- /data/vendor/javascript/{@rails--request.js.js → maglev/@rails--request.js.js} +0 -0
- /data/vendor/javascript/{@shopify--draggable.js → maglev/@shopify--draggable.js} +0 -0
- /data/vendor/javascript/{el-transition.js → maglev/el-transition.js} +0 -0
- /data/vendor/javascript/{stimulus-use.js → maglev/stimulus-use.js} +0 -0
- /data/vendor/javascript/{tiptap.bundle.js → maglev/tiptap.bundle.js} +0 -0
|
@@ -1,25 +1,41 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
import { sleep } from"maglev-controllers/utils"
|
|
3
|
+
import TurboDelayedStreams from 'maglev-patches/turbo_delayed_streams'
|
|
3
4
|
|
|
4
5
|
export default class extends Controller {
|
|
5
6
|
|
|
6
7
|
initialize() {
|
|
7
8
|
this._start = this.start.bind(this)
|
|
8
9
|
this._end = this.end.bind(this)
|
|
10
|
+
this._cancel = this.cancel.bind(this)
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
connect() {
|
|
14
|
+
this.canceled = false
|
|
12
15
|
this.formElement = this.element.closest('form')
|
|
13
16
|
this.formElement.addEventListener('turbo:submit-start', this._start)
|
|
14
17
|
this.formElement.addEventListener('turbo:submit-end', this._end)
|
|
18
|
+
document.addEventListener("turbo:before-cache", this._cancel)
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
disconnect() {
|
|
22
|
+
this.canceled = true
|
|
23
|
+
this.requestId = null
|
|
18
24
|
this.formElement.removeEventListener('turbo:submit-start', this._start)
|
|
19
|
-
this.formElement.removeEventListener('turbo:submit-end', this._end)
|
|
25
|
+
this.formElement.removeEventListener('turbo:submit-end', this._end)
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
cancel() {
|
|
29
|
+
this.canceled = true
|
|
30
|
+
this.element.disabled = false
|
|
31
|
+
this.formElement.classList.remove('is-pending', 'is-success', 'is-error')
|
|
32
|
+
this.formElement.classList.add('is-default')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
start(event) {
|
|
36
|
+
// the requestId is set by the turbo:before-fetch-request listener. We need it if there are delayed streams to render
|
|
37
|
+
this.requestId = event.detail.formSubmission.fetchRequest.fetchOptions.turboRequestId
|
|
38
|
+
|
|
23
39
|
this.formElement.classList.add('is-pending')
|
|
24
40
|
this.startedAt = Date.now()
|
|
25
41
|
}
|
|
@@ -30,16 +46,24 @@ export default class extends Controller {
|
|
|
30
46
|
|
|
31
47
|
// on an UX standpoint, we want to show the pending state for a short time to avoid flickering
|
|
32
48
|
// if the submit (call to the server) took less than 800ms, we wait for 800ms to show the pending state
|
|
33
|
-
if (Date.now() - this.startedAt <
|
|
49
|
+
if (Date.now() - this.startedAt < 600) await sleep(600)
|
|
50
|
+
|
|
51
|
+
if (this.canceled) return
|
|
34
52
|
|
|
35
53
|
this.formElement.classList.remove('is-pending')
|
|
36
|
-
this.formElement.classList.add(event.detail.success ? 'is-success' : 'is-error')
|
|
54
|
+
this.formElement.classList.add(event.detail.success ? 'is-success' : 'is-error')
|
|
37
55
|
|
|
38
56
|
// wait for 2 seconds and then remove the success or error class and add the default class
|
|
39
|
-
await sleep(
|
|
57
|
+
await sleep(1400)
|
|
58
|
+
|
|
59
|
+
if (this.canceled) return
|
|
40
60
|
|
|
41
61
|
this.formElement.classList.remove(event.detail.success ? 'is-success' : 'is-error')
|
|
42
62
|
this.formElement.classList.add('is-default')
|
|
43
63
|
this.element.disabled = false
|
|
64
|
+
|
|
65
|
+
// render the delayed turbo stream messages from the request
|
|
66
|
+
// use the requestId to identify the request and render the turbo stream messages
|
|
67
|
+
TurboDelayedStreams.render(this.requestId)
|
|
44
68
|
}
|
|
45
69
|
}
|
|
@@ -5,6 +5,44 @@ export const isSamePath = (targetPath) => {
|
|
|
5
5
|
return current.pathname === target.pathname
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
const getParentEditorConfig = () => {
|
|
9
|
+
try {
|
|
10
|
+
return window.parent?.maglevEditorConfig
|
|
11
|
+
} catch (_) {
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const getEditorConfig = () => {
|
|
17
|
+
return window.maglevEditorConfig || getParentEditorConfig() || {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const isEditorJsLogsEnabled = () => {
|
|
21
|
+
return getEditorConfig().jsLogsEnabled === true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const log = (...args) => {
|
|
25
|
+
if (isEditorJsLogsEnabled()) {
|
|
26
|
+
console.log(...args)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
export const sleep = (ms) => {
|
|
9
31
|
return new Promise(resolve => setTimeout(resolve, ms))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function uuidv4() {
|
|
35
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
|
|
36
|
+
const r = Math.random() * 16 | 0
|
|
37
|
+
const v = c === "x" ? r : (r & 0x3 | 0x8)
|
|
38
|
+
return v.toString(16)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function generateRequestId() {
|
|
43
|
+
// use the native randomUUID function if available (Browsers don't expose it for non-secure domains)
|
|
44
|
+
if (window.crypto?.randomUUID) {
|
|
45
|
+
return window.crypto.randomUUID()
|
|
46
|
+
}
|
|
47
|
+
return uuidv4()
|
|
10
48
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import "@hotwired/turbo-rails"
|
|
2
2
|
import "maglev-controllers"
|
|
3
|
-
import
|
|
3
|
+
import "maglev-patches/page_renderer_patch"
|
|
4
|
+
import "maglev-patches/turbo_stream_patch"
|
|
5
|
+
import { log } from "maglev-controllers/utils"
|
|
4
6
|
|
|
5
|
-
console.log('Maglev Editor
|
|
7
|
+
console.log('Maglev Editor v3 ⚡️')
|
|
6
8
|
|
|
7
9
|
// We need to set the content locale in the headers for each Turbo request
|
|
8
10
|
document.addEventListener("turbo:before-fetch-request", (event) => {
|
|
@@ -22,45 +24,4 @@ document.addEventListener("click", (event) => {
|
|
|
22
24
|
if (current.pathname === target.pathname && current.search === target.search) {
|
|
23
25
|
link.dataset.turboAction = "replace"
|
|
24
26
|
}
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
// iFrames will always be reloaded even if we mark them as turbo-permanent.
|
|
28
|
-
// In the context of the Editor, the workaround is to replace the content of another DIV (#root)
|
|
29
|
-
// instead of the body and put the preview iframe as a sibling of the root DIV.
|
|
30
|
-
PageRenderer.prototype.assignNewBody = function() {
|
|
31
|
-
const body = document.querySelector("#root")
|
|
32
|
-
const el = this.newElement.querySelector("#root")
|
|
33
|
-
|
|
34
|
-
// replace all the data attributes of the BODY tag with the data attributes of the newElement
|
|
35
|
-
// This is because the BODY tag carries the current page id in one of the controller values
|
|
36
|
-
const bodyDataAttributes = document.body.dataset
|
|
37
|
-
const newElementDataAttributes = this.newElement.dataset
|
|
38
|
-
Object.keys(newElementDataAttributes).forEach(key => {
|
|
39
|
-
bodyDataAttributes[key] = newElementDataAttributes[key]
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// now replace the "body" with the newElement
|
|
43
|
-
if (body && el) {
|
|
44
|
-
body.replaceWith(el)
|
|
45
|
-
return
|
|
46
|
-
} else if (document.body && this.newElement instanceof HTMLBodyElement) {
|
|
47
|
-
document.body.replaceWith(this.newElement)
|
|
48
|
-
} else {
|
|
49
|
-
document.documentElement.appendChild(this.newElement)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Custom stream actions
|
|
54
|
-
|
|
55
|
-
StreamActions.console_log = function() {
|
|
56
|
-
const message = this.getAttribute("message")
|
|
57
|
-
console.log(message)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
StreamActions.dispatch_event = function() {
|
|
61
|
-
const type = this.getAttribute("type")
|
|
62
|
-
const payload = this.getAttribute("payload")
|
|
63
|
-
console.log('dispatchEvent', type, payload, `dispatcher:${type}`)
|
|
64
|
-
const event = new CustomEvent(`dispatcher:${type}`, { detail: JSON.parse(payload) })
|
|
65
|
-
window.dispatchEvent(event)
|
|
66
|
-
}
|
|
27
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { PageRenderer } from '@hotwired/turbo'
|
|
2
|
+
|
|
3
|
+
// iFrames will always be reloaded even if we mark them as turbo-permanent.
|
|
4
|
+
// In the context of the Editor, the workaround is to replace the content of another DIV (#root)
|
|
5
|
+
// instead of the body and put the preview iframe as a sibling of the root DIV.
|
|
6
|
+
// assignNewBody is the right function to patch because it's called by both the PageRenderer and the SnapshotRenderer.
|
|
7
|
+
|
|
8
|
+
// Inspirations:
|
|
9
|
+
// - https://github.com/hotwired/turbo/pull/711/files
|
|
10
|
+
// - https://github.com/Challenge-Guy/turbo-cfm1/commit/2a3b0acfe0367f32c9d1635ff7c6fd7d87d2a2cd
|
|
11
|
+
PageRenderer.prototype.assignNewBody = function() {
|
|
12
|
+
const body = document.querySelector("#root")
|
|
13
|
+
const el = this.newElement.querySelector("#root")
|
|
14
|
+
|
|
15
|
+
// Only update sync elements during snapshot preview render
|
|
16
|
+
// those elements are permanent during the snapshot preview render and updated when we've got the new snapshot
|
|
17
|
+
if (this.isPreview) {
|
|
18
|
+
const syncElements = document.querySelectorAll('[data-turbo-sync]')
|
|
19
|
+
syncElements.forEach(element => {
|
|
20
|
+
const currentElement = document.querySelector(`#${element.id}`)
|
|
21
|
+
const newElement = this.newElement.querySelector(`#${element.id}`)
|
|
22
|
+
|
|
23
|
+
if (currentElement && newElement) {
|
|
24
|
+
// sync the innerHTML of the newElement with the currentElement
|
|
25
|
+
newElement.innerHTML = currentElement.innerHTML
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// replace all the data attributes of the BODY tag with the data attributes of the newElement
|
|
31
|
+
// This is because the BODY tag carries the current page id in one of the controller values
|
|
32
|
+
const bodyDataAttributes = document.body.dataset
|
|
33
|
+
const newElementDataAttributes = this.newElement.dataset
|
|
34
|
+
Object.keys(newElementDataAttributes).forEach(key => {
|
|
35
|
+
bodyDataAttributes[key] = newElementDataAttributes[key]
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// now replace the "body" with the newElement
|
|
39
|
+
if (body && el) {
|
|
40
|
+
body.replaceWith(el)
|
|
41
|
+
return
|
|
42
|
+
} else if (document.body && this.newElement instanceof HTMLBodyElement) {
|
|
43
|
+
document.body.replaceWith(this.newElement)
|
|
44
|
+
} else {
|
|
45
|
+
document.documentElement.appendChild(this.newElement)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Delayed Turbo streams are streams that are not rendered immediately, but are rendered later manually by the application
|
|
2
|
+
// It's used by the SubmitButton controller for instance
|
|
3
|
+
|
|
4
|
+
class TurboDelayedStream {
|
|
5
|
+
constructor(maxElements = 10) {
|
|
6
|
+
this.maxElements = maxElements
|
|
7
|
+
this.elements = new Map()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
add(requestId, render) {
|
|
11
|
+
if (this.elements.has(requestId)) {
|
|
12
|
+
this.elements.get(requestId).push(render)
|
|
13
|
+
} else {
|
|
14
|
+
this.elements.set(requestId, [render])
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (this.elements.size > this.maxElements) {
|
|
18
|
+
this.elements.delete(this.elements.keys().next().value)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
render(requestId) {
|
|
23
|
+
if (!this.elements.has(requestId)) return
|
|
24
|
+
|
|
25
|
+
const renderFns = this.elements.get(requestId)
|
|
26
|
+
|
|
27
|
+
// render all the turbo streams for this request
|
|
28
|
+
renderFns.forEach(render => render())
|
|
29
|
+
|
|
30
|
+
// remove the element from the list
|
|
31
|
+
this.elements.delete(requestId)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default new TurboDelayedStream()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
import { StreamActions } from '@hotwired/turbo'
|
|
3
|
+
import TurboDelayedStreams from 'maglev-patches/turbo_delayed_streams'
|
|
4
|
+
import { generateRequestId, log } from 'maglev-controllers/utils'
|
|
5
|
+
|
|
6
|
+
// Custom stream actions
|
|
7
|
+
StreamActions.console_log = function() {
|
|
8
|
+
const message = this.getAttribute("message")
|
|
9
|
+
log(message)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
StreamActions.dispatch_event = function() {
|
|
13
|
+
const type = this.getAttribute("type")
|
|
14
|
+
const payload = this.getAttribute("payload")
|
|
15
|
+
log('dispatchEvent', type, payload, `dispatcher:${type}`)
|
|
16
|
+
const event = new CustomEvent(`dispatcher:${type}`, { detail: JSON.parse(payload) })
|
|
17
|
+
window.dispatchEvent(event)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Handle delayed streams through Turbo events
|
|
21
|
+
|
|
22
|
+
document.addEventListener("turbo:before-stream-render", (event) => {
|
|
23
|
+
const delayedStream = (
|
|
24
|
+
event.detail.newStream.templateContent.querySelector('meta[name="turbo-delayed-stream"]')?.content ?? event.detail.newStream.getAttribute('delayed')
|
|
25
|
+
) === 'true'
|
|
26
|
+
const requestId = (
|
|
27
|
+
event.detail.newStream.templateContent.querySelector('meta[name="turbo-request-id"]')?.content ?? event.detail.newStream.getAttribute('request-id')
|
|
28
|
+
)?.split(',')?.[0]
|
|
29
|
+
|
|
30
|
+
if (delayedStream) {
|
|
31
|
+
// Keep the stream in the queue to be rendered later
|
|
32
|
+
TurboDelayedStreams.add(requestId, () => {
|
|
33
|
+
// console.log('rendering delayed stream', requestId, event.detail.newStream)
|
|
34
|
+
event.detail.render(event.detail.newStream)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Cancel Turbo's rendering for this stream
|
|
38
|
+
event.preventDefault()
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
document.addEventListener('turbo:before-fetch-request', (event) => {
|
|
43
|
+
const requestId = generateRequestId()
|
|
44
|
+
|
|
45
|
+
// Attach to headers (for server → client stream correlation)
|
|
46
|
+
event.detail.fetchOptions.headers["X-Turbo-Request-ID"] = requestId
|
|
47
|
+
|
|
48
|
+
// Attach to the fetchRequest instance
|
|
49
|
+
event.detail.fetchOptions.turboRequestId = requestId
|
|
50
|
+
})
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
--color-editor-primary: var(--editor-color-primary);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
/* Empty-state overlay in #root: visible when the preview Stimulus shell has .is-empty */
|
|
12
|
+
@custom-variant editor-preview-empty (body:has([data-controller~="editor-page-preview"].is-empty) &);
|
|
13
|
+
|
|
14
|
+
@source "<%= Maglev::Engine.root.join('app') %>";
|
|
15
|
+
@source "<%= Maglev::Engine.root.join('lib') %>";
|
|
16
|
+
@source "<%= Maglev::Engine.root.join('spec/components/previews/maglev/uikit') %>";
|
|
17
|
+
|
|
12
18
|
<% Maglev.config.tailwindcss_folders.each do |folder| %>
|
|
13
19
|
@source "<%= folder %>";
|
|
14
20
|
<% end %>
|
|
@@ -26,6 +32,10 @@
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
@layer base {
|
|
35
|
+
.turbo-progress-bar {
|
|
36
|
+
background-color: var(--editor-color-primary) !important;
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
/* Tiptap / ProseMirror */
|
|
30
40
|
.ProseMirror {
|
|
31
41
|
@apply block py-2 px-3 rounded bg-gray-100 text-gray-800 focus:outline-none focus:ring focus:ring-inset focus:ring-2 focus:ring-editor-primary/50 placeholder-gray-500;
|
|
@@ -141,7 +151,6 @@
|
|
|
141
151
|
td,
|
|
142
152
|
th {
|
|
143
153
|
min-width: 1em;
|
|
144
|
-
// border: 2px solid $color-grey;
|
|
145
154
|
@apply border-2;
|
|
146
155
|
@apply border-solid;
|
|
147
156
|
padding: 3px 5px;
|
|
@@ -6,12 +6,18 @@ module Maglev
|
|
|
6
6
|
module Link
|
|
7
7
|
class LinkComponent < Maglev::Editor::Settings::BaseComponent
|
|
8
8
|
def edit_link_path
|
|
9
|
-
fetch_path(:edit_link_path, { value: value, input_name: input_name })
|
|
9
|
+
fetch_path(:edit_link_path, { value: value || default_value, input_name: input_name })
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def after_initialize
|
|
13
13
|
@value = value&.with_indifferent_access
|
|
14
14
|
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def default_value
|
|
19
|
+
{ link_type: 'url' }
|
|
20
|
+
end
|
|
15
21
|
end
|
|
16
22
|
end
|
|
17
23
|
end
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# NOTE: this class is too long (but not too complex),
|
|
4
|
+
# we need to move it along with the PageComponent and BlockComponent
|
|
5
|
+
# to a separate folder (content?)
|
|
6
|
+
|
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
|
3
8
|
module Maglev
|
|
4
9
|
class SectionComponent < BaseComponent
|
|
5
10
|
include TagHelper
|
|
@@ -31,7 +36,7 @@ module Maglev
|
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
def lock_version
|
|
34
|
-
@lock_version ||=
|
|
39
|
+
@lock_version ||= lock_source&.lock_version || '0'
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def dom_data
|
|
@@ -63,6 +68,10 @@ module Maglev
|
|
|
63
68
|
|
|
64
69
|
private
|
|
65
70
|
|
|
71
|
+
def lock_source
|
|
72
|
+
definition&.site_scoped? ? site : page
|
|
73
|
+
end
|
|
74
|
+
|
|
66
75
|
def build_block_list
|
|
67
76
|
build_blocks(attributes[:blocks])
|
|
68
77
|
end
|
|
@@ -131,3 +140,4 @@ module Maglev
|
|
|
131
140
|
end
|
|
132
141
|
end
|
|
133
142
|
end
|
|
143
|
+
# rubocop:enable Metrics/ClassLength
|
|
@@ -5,12 +5,24 @@ module Maglev
|
|
|
5
5
|
module AppLayout
|
|
6
6
|
module Sidebar
|
|
7
7
|
class LinkComponent < Maglev::Uikit::BaseComponent
|
|
8
|
-
|
|
8
|
+
LINK_BASE_CLASSES = [
|
|
9
|
+
'relative flex w-full min-h-11 items-center justify-center rounded py-3',
|
|
10
|
+
'outline-none transition-colors duration-200',
|
|
11
|
+
'focus-visible:ring-2 focus-visible:ring-editor-primary/50',
|
|
12
|
+
'focus-visible:ring-offset-2 focus-visible:ring-offset-white'
|
|
13
|
+
].join(' ')
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
LINK_ACTIVE_CLASSES = 'bg-gray-100 text-editor-primary hover:bg-gray-100'
|
|
16
|
+
|
|
17
|
+
LINK_INACTIVE_CLASSES = 'text-black hover:bg-gray-100'
|
|
18
|
+
|
|
19
|
+
attr_reader :path, :icon, :icon_size, :active, :data, :label
|
|
20
|
+
|
|
21
|
+
def initialize(path:, icon:, active: false, label: nil, options: {})
|
|
11
22
|
@path = path
|
|
12
23
|
@active = active
|
|
13
24
|
@icon = icon
|
|
25
|
+
@label = label
|
|
14
26
|
@icon_size = options[:icon_size] || '1.5rem'
|
|
15
27
|
@position = options[:position] || :top
|
|
16
28
|
@data = options[:data]
|
|
@@ -24,15 +36,33 @@ module Maglev
|
|
|
24
36
|
@position == :top
|
|
25
37
|
end
|
|
26
38
|
|
|
39
|
+
def link_html_options
|
|
40
|
+
{
|
|
41
|
+
class: link_classes(active: active?),
|
|
42
|
+
data: data,
|
|
43
|
+
title: label.presence,
|
|
44
|
+
aria: link_aria_attributes
|
|
45
|
+
}.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
27
48
|
def link_classes(...)
|
|
28
49
|
class_variants(
|
|
29
|
-
base:
|
|
50
|
+
base: LINK_BASE_CLASSES,
|
|
30
51
|
variants: {
|
|
31
|
-
active:
|
|
32
|
-
'!active':
|
|
52
|
+
active: LINK_ACTIVE_CLASSES,
|
|
53
|
+
'!active': LINK_INACTIVE_CLASSES
|
|
33
54
|
}
|
|
34
55
|
).render(...)
|
|
35
56
|
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def link_aria_attributes
|
|
61
|
+
aria = {}
|
|
62
|
+
aria[:label] = label.presence
|
|
63
|
+
aria[:current] = 'page' if active?
|
|
64
|
+
aria.compact.presence
|
|
65
|
+
end
|
|
36
66
|
end
|
|
37
67
|
end
|
|
38
68
|
end
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
id="app-layout-sidebar"
|
|
3
3
|
class="w-16 flex-shrink-0 flex content-center h-full border-r border-gray-200 relative z-20 bg-white"
|
|
4
4
|
>
|
|
5
|
-
<nav class="w-16 flex
|
|
6
|
-
<ol class="
|
|
5
|
+
<nav class="flex h-full w-16 flex-col justify-between py-2">
|
|
6
|
+
<ol class="flex flex-col gap-1 px-2">
|
|
7
7
|
<% links.each do |link| %>
|
|
8
8
|
<%= link if link.top? %>
|
|
9
9
|
<% end %>
|
|
10
10
|
</ol>
|
|
11
11
|
|
|
12
|
-
<ol class="
|
|
13
|
-
<li></li>
|
|
12
|
+
<ol class="flex flex-col gap-1 px-2">
|
|
14
13
|
<% links.each do |link| %>
|
|
15
14
|
<%= link unless link.top? %>
|
|
16
15
|
<% end %>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<%= link_to root_path, class: "w-16 h-full flex justify-center items-center border-r border-gray-200 shrink-0" do %>
|
|
1
|
+
<%= link_to root_path, class: "w-16 h-full flex justify-center items-center border-r border-gray-200 shrink-0", data: { controller: 'prevent-same-path' } do %>
|
|
2
2
|
<%= image_tag logo_url, class: "w-2/4", id: 'editor-logo-image', data: { turbo_permanent: true } %>
|
|
3
3
|
<% end %>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="flex flex-row items-center h-full w-full px-4 overflow-hidden">
|
|
5
5
|
<div class="flex flex-col leading-none overflow-hidden">
|
|
6
6
|
<div class="flex items-center">
|
|
7
|
-
<%=
|
|
7
|
+
<%= helpers.maglev_page_icon(page, size: '1.1rem', wrapper_class_names: 'text-gray-900') %>
|
|
8
8
|
<div class="text-base font-semibold truncate ml-1 mr-3">
|
|
9
9
|
<%= page.title.presence || page.default_title %>
|
|
10
10
|
</div>
|
|
@@ -5,7 +5,7 @@ module Maglev
|
|
|
5
5
|
module AppLayout
|
|
6
6
|
class TopbarComponent < ViewComponent::Base
|
|
7
7
|
renders_one :logo, 'Maglev::Uikit::AppLayout::Topbar::LogoComponent'
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
renders_one :page_info
|
|
10
10
|
renders_one :actions
|
|
11
11
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Maglev
|
|
4
|
+
module Uikit
|
|
5
|
+
class ButtonGroupComponent < Maglev::Uikit::BaseComponent
|
|
6
|
+
renders_many :buttons
|
|
7
|
+
|
|
8
|
+
attr_reader :color, :size
|
|
9
|
+
|
|
10
|
+
def initialize(color: nil, size: nil)
|
|
11
|
+
@color = color || :primary
|
|
12
|
+
@size = size || :medium
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# rubocop:disable Metrics/MethodLength
|
|
16
|
+
def button_classes
|
|
17
|
+
class_variants(
|
|
18
|
+
base: %(
|
|
19
|
+
inline-flex items-center justify-center cursor-pointer
|
|
20
|
+
group-[.is-success]/form:bg-green-500/95 group-[.is-success]/form:hover:bg-green-500/100
|
|
21
|
+
group-[.is-success]/form:disabled:bg-green-500/75
|
|
22
|
+
group-[.is-error]/form:bg-red-500/95 group-[.is-error]/form:hover:bg-red-500/100
|
|
23
|
+
group-[.is-error]/form:disabled:bg-red-500/75
|
|
24
|
+
),
|
|
25
|
+
variants: {
|
|
26
|
+
size: {
|
|
27
|
+
medium: 'inline-flex items-center justify-center px-4 h-10'
|
|
28
|
+
},
|
|
29
|
+
color: {
|
|
30
|
+
primary: 'text-white bg-editor-primary hover:bg-editor-primary/90 disabled:bg-editor-primary/75',
|
|
31
|
+
secondary: 'text-gray-800 hover:bg-gray-100'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
).render(color: color, size: size)
|
|
35
|
+
end
|
|
36
|
+
# rubocop:enable Metrics/MethodLength
|
|
37
|
+
|
|
38
|
+
# rubocop:disable Metrics/MethodLength
|
|
39
|
+
def wrapper_classes(**args)
|
|
40
|
+
class_variants(
|
|
41
|
+
base: %(
|
|
42
|
+
inline-flex items-center transition-colors duration-200 h-full
|
|
43
|
+
[.is-success]:bg-green-500/95 [.is-success]:hover:bg-green-500/100
|
|
44
|
+
[.is-success]:disabled:bg-green-500/75
|
|
45
|
+
[.is-error]:bg-red-500/95 [.is-error]:hover:bg-red-500/100
|
|
46
|
+
[.is-error]:disabled:bg-red-500/75
|
|
47
|
+
),
|
|
48
|
+
variants: {
|
|
49
|
+
color: {
|
|
50
|
+
primary: 'text-white bg-editor-primary hover:bg-editor-primary/90 has-[:disabled]:bg-editor-primary/75',
|
|
51
|
+
secondary: 'text-gray-800 hover:bg-gray-100'
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
).render(color: color, **args)
|
|
55
|
+
end
|
|
56
|
+
# rubocop:enable Metrics/MethodLength
|
|
57
|
+
|
|
58
|
+
def wrapped_button_classes(**args)
|
|
59
|
+
class_variants(
|
|
60
|
+
base: 'cursor-pointer',
|
|
61
|
+
variants: {
|
|
62
|
+
size: {
|
|
63
|
+
medium: 'inline-flex items-center justify-center px-4 h-10'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
).render(size: size, **args)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -5,13 +5,22 @@ module Maglev
|
|
|
5
5
|
class DeviceTogglerComponent < Maglev::Uikit::BaseComponent
|
|
6
6
|
def toggler_classes(...)
|
|
7
7
|
class_variants(
|
|
8
|
-
base:
|
|
8
|
+
base: base_classes,
|
|
9
9
|
variants: {
|
|
10
10
|
active: active_classes
|
|
11
11
|
}
|
|
12
12
|
).render(...)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
+
def base_classes
|
|
16
|
+
[
|
|
17
|
+
'cursor-pointer hover:bg-gray-100 h-10 w-10 flex items-center justify-center rounded',
|
|
18
|
+
'outline-none transition-colors duration-200',
|
|
19
|
+
'focus-visible:ring-2 focus-visible:ring-editor-primary/50',
|
|
20
|
+
'focus-visible:ring-offset-2 focus-visible:ring-offset-white'
|
|
21
|
+
].join(' ').freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
15
24
|
def active_classes
|
|
16
25
|
'bg-gray-100'
|
|
17
26
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%= tag.div(
|
|
2
|
-
class:
|
|
2
|
+
class: wrapper_classes,
|
|
3
3
|
data: {
|
|
4
4
|
controller: 'uikit-dropdown',
|
|
5
5
|
'uikit-dropdown-placement-value': placement
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<%= trigger %>
|
|
9
9
|
|
|
10
10
|
<div
|
|
11
|
-
class="hidden z-50 opacity-0 scale-95 shadow-lg rounded
|
|
11
|
+
class="hidden z-50 opacity-0 scale-95 shadow-lg rounded text-sm bg-white w-max fixed"
|
|
12
12
|
data-aria-orientation="vertical"
|
|
13
13
|
data-aria-labelledby="dropdown-button"
|
|
14
14
|
data-uikit-dropdown-target="content"
|