maglevcms 3.0.0.beta3 → 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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +16 -53
  3. data/Rakefile +3 -1
  4. data/app/assets/builds/maglev/tailwind.css +424 -84
  5. data/app/assets/config/maglev_manifest.js +3 -2
  6. data/app/assets/javascripts/maglev/client/dom-operations.js +5 -5
  7. data/app/assets/javascripts/maglev/client/iframe-decorator.js +24 -0
  8. data/app/assets/javascripts/maglev/client/incoming-messages.js +13 -3
  9. data/app/assets/javascripts/maglev/client/index.js +3 -2
  10. data/app/assets/javascripts/maglev/client/utils.js +22 -0
  11. data/app/assets/javascripts/maglev/editor/controllers/app/forms/section_form_controller.js +4 -3
  12. data/app/assets/javascripts/maglev/editor/controllers/app/forms/style_form_controller.js +2 -1
  13. data/app/assets/javascripts/maglev/editor/controllers/app/page_preview_controller.js +96 -5
  14. data/app/assets/javascripts/maglev/editor/controllers/app/preview_notification_center_controller.js +105 -23
  15. data/app/assets/javascripts/maglev/editor/controllers/app/setting_controller.js +3 -2
  16. data/app/assets/javascripts/maglev/editor/controllers/shared/copy_to_clipboard_controller.js +2 -1
  17. data/app/assets/javascripts/maglev/editor/controllers/utils.js +22 -0
  18. data/app/assets/javascripts/maglev/editor/index.js +2 -1
  19. data/app/assets/javascripts/maglev/editor/patches/turbo_stream_patch.js +3 -3
  20. data/app/assets/stylesheets/maglev/tailwind.css.erb +8 -1
  21. data/app/components/maglev/content/link.rb +1 -1
  22. data/app/components/maglev/editor/settings/link/link_component.rb +7 -1
  23. data/app/components/maglev/section_component.rb +11 -1
  24. data/app/components/maglev/uikit/app_layout/sidebar/link_component.html.erb +1 -1
  25. data/app/components/maglev/uikit/app_layout/sidebar/link_component.rb +35 -5
  26. data/app/components/maglev/uikit/app_layout/sidebar_component.html.erb +3 -4
  27. data/app/components/maglev/uikit/app_layout/topbar_component.html.erb +1 -1
  28. data/app/components/maglev/uikit/app_layout/topbar_component.rb +1 -1
  29. data/app/components/maglev/uikit/button_group_component/button_group_component.html.erb +5 -0
  30. data/app/components/maglev/uikit/button_group_component.rb +70 -0
  31. data/app/components/maglev/uikit/device_toggler_component.rb +10 -1
  32. data/app/components/maglev/uikit/dropdown_component/dropdown_component.html.erb +2 -2
  33. data/app/components/maglev/uikit/dropdown_component.rb +6 -1
  34. data/app/components/maglev/uikit/form/color_field_component.html.erb +1 -1
  35. data/app/components/maglev/uikit/form/combobox_component.html.erb +1 -0
  36. data/app/components/maglev/uikit/form/link_component.rb +5 -1
  37. data/app/components/maglev/uikit/form/richtext_controller.js +5 -4
  38. data/app/components/maglev/uikit/form/search_form_component.html.erb +1 -0
  39. data/app/components/maglev/uikit/icon_component.rb +3 -0
  40. data/app/components/maglev/uikit/image_library/uploader_controller.js +3 -2
  41. data/app/components/maglev/uikit/list/list_item_component.html.erb +36 -19
  42. data/app/components/maglev/uikit/list/list_item_component.rb +19 -5
  43. data/app/components/maglev/uikit/locale_switcher_component/locale_switcher_component.html.erb +6 -10
  44. data/app/components/maglev/uikit/menu_dropdown_component/menu_dropdown_component.html.erb +6 -2
  45. data/app/components/maglev/uikit/menu_dropdown_component.rb +244 -7
  46. data/app/components/maglev/uikit/page_actions_dropdown_component/page_actions_dropdown_component.html.erb +39 -46
  47. data/app/components/maglev/uikit/pagination_component/pagination_component.html.erb +9 -12
  48. data/app/components/maglev/uikit/pagination_component.rb +6 -1
  49. data/app/components/maglev/uikit/section_toolbar/bottom_component.html.erb +1 -1
  50. data/app/components/maglev/uikit/tabs_component/tabs_component.html.erb +7 -4
  51. data/app/components/maglev/uikit/tabs_component.rb +23 -4
  52. data/app/components/maglev/uikit/well/simple_well_component.html.erb +15 -0
  53. data/app/components/maglev/uikit/well/simple_well_component.rb +13 -0
  54. data/app/controllers/concerns/maglev/editor/preview_urls_concern.rb +32 -0
  55. data/app/controllers/concerns/maglev/editor/turbo_concern.rb +29 -0
  56. data/app/controllers/concerns/maglev/flash_i18n_concern.rb +1 -0
  57. data/app/controllers/maglev/application_controller.rb +1 -1
  58. data/app/controllers/maglev/assets/active_storage_proxy_controller.rb +2 -1
  59. data/app/controllers/maglev/editor/assets_controller.rb +1 -1
  60. data/app/controllers/maglev/editor/base_controller.rb +6 -32
  61. data/app/controllers/maglev/editor/pages/clone_controller.rb +22 -0
  62. data/app/controllers/maglev/editor/pages/discard_draft_controller.rb +17 -0
  63. data/app/controllers/maglev/editor/pages_controller.rb +26 -7
  64. data/app/controllers/maglev/editor/section_blocks_controller.rb +13 -9
  65. data/app/controllers/maglev/editor/sections_controller.rb +26 -7
  66. data/app/controllers/maglev/published_page_preview_controller.rb +4 -0
  67. data/app/helpers/maglev/application_helper.rb +6 -6
  68. data/app/helpers/maglev/editor/section_blocks_helper.rb +2 -2
  69. data/app/models/maglev/page/publishable_concern.rb +69 -0
  70. data/app/models/maglev/page.rb +2 -13
  71. data/app/models/maglev/section/block.rb +4 -0
  72. data/app/models/maglev/section/content_concern.rb +3 -1
  73. data/app/models/maglev/section.rb +21 -1
  74. data/app/models/maglev/sections_content_store.rb +1 -3
  75. data/app/services/concerns/maglev/content/helpers_concern.rb +5 -8
  76. data/app/services/maglev/app_container.rb +4 -2
  77. data/app/services/maglev/discard_page_draft_service.rb +45 -0
  78. data/app/services/maglev/fetch_section_screenshot_url.rb +12 -1
  79. data/app/services/maglev/has_unpublished_changes.rb +21 -0
  80. data/app/services/maglev/publish_service.rb +15 -2
  81. data/app/views/layouts/maglev/editor/_sidebar.html.erb +6 -1
  82. data/app/views/layouts/maglev/editor/application.html.erb +5 -0
  83. data/app/views/layouts/maglev/editor/topbar/_page_info.html.erb +1 -1
  84. data/app/views/layouts/maglev/editor/topbar/_publish_button.html.erb +32 -10
  85. data/app/views/maglev/editor/assets/index.html.erb +1 -0
  86. data/app/views/maglev/editor/home/index.html.erb +1 -1
  87. data/app/views/maglev/editor/links/edit/_email.html.erb +1 -1
  88. data/app/views/maglev/editor/links/edit/_url.html.erb +1 -1
  89. data/app/views/maglev/editor/pages/_list.html.erb +25 -21
  90. data/app/views/maglev/editor/pages/_preview.html.erb +6 -6
  91. data/app/views/maglev/editor/pages/_preview_empty_message.html.erb +13 -10
  92. data/app/views/maglev/editor/pages/discard_draft/create.turbo_stream.erb +16 -0
  93. data/app/views/maglev/editor/pages/index.html.erb +8 -1
  94. data/app/views/maglev/editor/section_blocks/_form.html.erb +5 -13
  95. data/app/views/maglev/editor/section_blocks/_form_with_tabs.html.erb +15 -0
  96. data/app/views/maglev/editor/section_blocks/_new.html.erb +1 -1
  97. data/app/views/maglev/editor/section_blocks/edit.html.erb +2 -2
  98. data/app/views/maglev/editor/section_blocks/index/_list.html.erb +2 -2
  99. data/app/views/maglev/editor/section_blocks/index/_tree.html.erb +1 -1
  100. data/app/views/maglev/editor/section_blocks/update.turbo_stream.erb +1 -1
  101. data/app/views/maglev/editor/sections/_form.html.erb +6 -20
  102. data/app/views/maglev/editor/sections/_form_with_tabs.html.erb +21 -0
  103. data/app/views/maglev/editor/sections/_list.html.erb +1 -1
  104. data/app/views/maglev/editor/sections/edit.html.erb +3 -3
  105. data/app/views/maglev/editor/sections/index.html.erb +1 -1
  106. data/app/views/maglev/editor/sections/new.html.erb +18 -1
  107. data/app/views/maglev/editor/sections/theme/_empty_list.html.erb +3 -0
  108. data/app/views/maglev/editor/sections/theme/_list.html.erb +35 -0
  109. data/app/views/maglev/editor/sections/theme/_screenshot_placeholder.html.erb +6 -0
  110. data/app/views/maglev/editor/sections/theme/_search.html.erb +22 -0
  111. data/app/views/maglev/editor/sections/update.turbo_stream.erb +1 -1
  112. data/app/views/maglev/editor/shared/_button_label.html.erb +4 -4
  113. data/app/views/maglev/editor/style/edit.html.erb +1 -0
  114. data/config/editor_importmap.rb +13 -13
  115. data/config/locales/editor.ar.yml +12 -4
  116. data/config/locales/editor.en.yml +31 -23
  117. data/config/locales/editor.es.yml +12 -4
  118. data/config/locales/editor.fr.yml +12 -4
  119. data/config/locales/editor.pt-BR.yml +12 -4
  120. data/config/routes/maglev/assets.rb +4 -0
  121. data/config/routes/maglev/editor.rb +38 -0
  122. data/config/routes/maglev/preview.rb +8 -0
  123. data/config/routes/maglev/public_preview.rb +6 -0
  124. data/config/routes.rb +8 -47
  125. data/db/migrate/20211013210954_translate_section_content.rb +1 -0
  126. data/db/migrate/20260114112058_add_published_payload_to_pages.rb +14 -0
  127. data/exe/tailwind-cli +1 -1
  128. data/lib/generators/maglev/install_generator.rb +9 -7
  129. data/lib/generators/maglev/templates/install/config/initializers/maglev.rb +10 -3
  130. data/lib/maglev/active_storage/serving_blob.rb +29 -0
  131. data/lib/maglev/active_storage.rb +2 -0
  132. data/lib/maglev/config.rb +22 -3
  133. data/lib/maglev/engine.rb +14 -10
  134. data/lib/maglev/version.rb +1 -1
  135. data/lib/maglev.rb +18 -3
  136. data/lib/tasks/db_test_all.rake +290 -0
  137. metadata +46 -19
  138. data/app/controllers/maglev/editor/page_clone_controller.rb +0 -20
  139. data/app/views/maglev/editor/sections/_theme_list.html.erb +0 -32
  140. /data/vendor/javascript/{@floating-ui--core.js → maglev/@floating-ui--core.js} +0 -0
  141. /data/vendor/javascript/{@floating-ui--dom.js → maglev/@floating-ui--dom.js} +0 -0
  142. /data/vendor/javascript/{@floating-ui--utils--dom.js → maglev/@floating-ui--utils--dom.js} +0 -0
  143. /data/vendor/javascript/{@floating-ui--utils.js → maglev/@floating-ui--utils.js} +0 -0
  144. /data/vendor/javascript/{@hotwired--stimulus.js → maglev/@hotwired--stimulus.js} +0 -0
  145. /data/vendor/javascript/{@hotwired--turbo-rails.js → maglev/@hotwired--turbo-rails.js} +0 -0
  146. /data/vendor/javascript/{@hotwired--turbo.js → maglev/@hotwired--turbo.js} +0 -0
  147. /data/vendor/javascript/{@rails--actioncable--src.js → maglev/@rails--actioncable--src.js} +0 -0
  148. /data/vendor/javascript/{@rails--request.js.js → maglev/@rails--request.js.js} +0 -0
  149. /data/vendor/javascript/{@shopify--draggable.js → maglev/@shopify--draggable.js} +0 -0
  150. /data/vendor/javascript/{el-transition.js → maglev/el-transition.js} +0 -0
  151. /data/vendor/javascript/{stimulus-use.js → maglev/stimulus-use.js} +0 -0
  152. /data/vendor/javascript/{tiptap.bundle.js → maglev/tiptap.bundle.js} +0 -0
@@ -1,6 +1,7 @@
1
- //= link_directory ../stylesheets/maglev .css
1
+ //= link_directory ../builds/maglev .css
2
+ //= link maglev/application.css
2
3
 
3
- //= link_tree ../../../vendor/javascript .js
4
+ //= link_tree ../../../vendor/javascript/maglev .js
4
5
  //= link_tree ../../components .js
5
6
  //= link_tree ../javascripts/maglev .js
6
7
 
@@ -1,4 +1,4 @@
1
- import { debounce, postMessageToEditor } from 'maglev-client/utils'
1
+ import { debounce, log, postMessageToEditor } from 'maglev-client/utils'
2
2
  import runScripts from 'maglev-client/run-scripts'
3
3
 
4
4
  const parentDocument = window.parent.document
@@ -79,7 +79,7 @@ const removeSection = (event) => {
79
79
 
80
80
  const checkSectionLockVersion = (event) => {
81
81
  const { sectionId, lockVersion } = event.detail
82
- console.log('[DOM Operations] checkSectionLockVersion', event)
82
+ log('[DOM Operations] checkSectionLockVersion', event)
83
83
  const section = previewDocument.querySelector(`[data-maglev-section-id='${sectionId}']`)
84
84
  const localLockVersion = section?.getAttribute('data-maglev-section-lock-version')
85
85
  if (lockVersion !== localLockVersion && lockVersion !== '') {
@@ -180,7 +180,7 @@ const updateStyle = async (event) => {
180
180
  // }
181
181
 
182
182
  const updateTextSetting = (sourceId, change) => {
183
- console.log('[DOM Operations] updateTextSetting', sourceId, change)
183
+ log('[DOM Operations] updateTextSetting', sourceId, change)
184
184
  const selector = `[data-maglev-id='${sourceId}.${change.settingId}']`
185
185
  const settings = previewDocument.querySelectorAll(selector)
186
186
  settings.forEach(($el) => ($el.innerHTML = change.value))
@@ -188,11 +188,11 @@ const updateTextSetting = (sourceId, change) => {
188
188
  }
189
189
 
190
190
  const updateLinkSetting = (sourceId, change) => {
191
- console.log('updateLinkSetting', sourceId, change)
191
+ log('updateLinkSetting', sourceId, change)
192
192
  }
193
193
 
194
194
  const updatePreviewDocument = async ({ sectionId, insertAt = undefined, scrollToSection = false }) => {
195
- console.log('[DOM Operations] updatePreviewDocument', sectionId, insertAt, scrollToSection)
195
+ log('[DOM Operations] updatePreviewDocument', sectionId, insertAt, scrollToSection)
196
196
 
197
197
  const doc = await getUpdatedDoc({ section_id: sectionId })
198
198
 
@@ -3,6 +3,7 @@ import { isBlank, postMessageToEditor } from 'maglev-client/utils'
3
3
  // keep track of the current hovered section
4
4
  let listeners = []
5
5
  let hoveredSectionId = null
6
+ let hoveredSettingKey = null
6
7
  let lastCursorPosition = { x: 0, y: 0 }
7
8
 
8
9
  export const start = (config) => {
@@ -172,6 +173,7 @@ const getMinTop = (previewDocument, currentSectionId, stickySectionIds) => {
172
173
  const onSectionLeft = () => {
173
174
  postMessageToEditor('section:leave')
174
175
  hoveredSectionId = null
176
+ hoveredSettingKey = null
175
177
  }
176
178
 
177
179
  const onBlockHovered = (el) => {
@@ -200,10 +202,32 @@ const onSettingHovered = (el) => {
200
202
  window.getComputedStyle(el).borderRadius === '0px'
201
203
  )
202
204
  el.style.borderRadius = '2px'
205
+
206
+ const fragments = el.dataset.maglevId.split('.')
207
+ const section = el.closest('[data-maglev-section-id]')
208
+ const sectionId = section?.dataset?.maglevSectionId
209
+ const sectionBlock = el.closest('[data-maglev-block-id]')
210
+ const sectionBlockId = sectionBlock?.dataset?.maglevBlockId
211
+ const settingId = fragments[1]
212
+
213
+ if (!sectionId || !settingId) return
214
+
215
+ const key = `${sectionId}:${sectionBlockId || 'none'}:${settingId}`
216
+ if (key === hoveredSettingKey) return
217
+
218
+ hoveredSettingKey = key
219
+
220
+ const prefix = sectionBlockId ? 'sectionBlock' : 'section'
221
+ postMessageToEditor(`${prefix}:setting:hovered`, {
222
+ sectionId,
223
+ sectionBlockId,
224
+ settingId,
225
+ })
203
226
  }
204
227
 
205
228
  const onSettingLeft = (el) => {
206
229
  el.style.boxShadow = 'none'
230
+ hoveredSettingKey = null
207
231
  }
208
232
 
209
233
  const onSettingClicked = (el, event) => {
@@ -1,12 +1,14 @@
1
1
  import { start as decorateIframe } from 'maglev-client/iframe-decorator'
2
- import { postMessageToEditor } from 'maglev-client/utils'
2
+ import { log, postMessageToEditor } from 'maglev-client/utils'
3
+
4
+ const PAGE_UPDATED_EVENTS = ['section:add', 'section:move', 'section:update', 'section:remove', 'block:add', 'block:move', 'block:remove', 'setting:update', 'style:update']
3
5
 
4
6
  export const start = () => {
5
7
  window.addEventListener('message', ({ data: { type, ...data } }) => {
6
8
  // a message MUST have a type
7
9
  if (!type) return
8
10
 
9
- const internalType = type.replace('maglev:', '')
11
+ const internalType = getInternalType(type)
10
12
 
11
13
  switch (internalType) {
12
14
  case 'config':
@@ -36,7 +38,7 @@ export const start = () => {
36
38
  triggerEvent(type, data)
37
39
  break
38
40
  default:
39
- console.log('[maglev][iframe] unknown message type', type)
41
+ log('[maglev][iframe] unknown message type', type)
40
42
  break
41
43
  }
42
44
  })
@@ -46,4 +48,12 @@ export const start = () => {
46
48
  const triggerEvent = (type, data) => {
47
49
  const event = new CustomEvent(type, { detail: data })
48
50
  window.dispatchEvent(event)
51
+
52
+ if (PAGE_UPDATED_EVENTS.includes(getInternalType(type))) {
53
+ window.dispatchEvent(new CustomEvent('maglev:page-updated', { detail: { type, data } }))
54
+ }
49
55
  }
56
+
57
+ function getInternalType(type) {
58
+ return type.replace('maglev:', '')
59
+ }
@@ -1,8 +1,9 @@
1
1
  import { start as startListeningEvents } from 'maglev-client/dom-operations'
2
2
  import { start as startListeningMessages } from 'maglev-client/incoming-messages'
3
+ import { log } from 'maglev-client/utils'
3
4
 
4
5
  const initializeClient = () => {
5
- console.log('Maglev Client v2 🚆')
6
+ log('Maglev Client v3 🚆')
6
7
 
7
8
  // no need to start the client when the site is being visited outside the editor
8
9
  // (shouldn't happen, but just in case)
@@ -14,7 +15,7 @@ const initializeClient = () => {
14
15
  // listen local events (converted from messages) and process them
15
16
  startListeningEvents()
16
17
 
17
- console.log('Maglev Client v2 ✅')
18
+ console.log('Maglev Client v3 ✅')
18
19
  }
19
20
 
20
21
  // Check if document is already ready
@@ -7,6 +7,28 @@ export const isBlank = (object) => {
7
7
  )
8
8
  }
9
9
 
10
+ const getParentEditorConfig = () => {
11
+ try {
12
+ return window.parent?.maglevEditorConfig
13
+ } catch (_) {
14
+ return undefined
15
+ }
16
+ }
17
+
18
+ const getEditorConfig = () => {
19
+ return window.maglevEditorConfig || getParentEditorConfig() || {}
20
+ }
21
+
22
+ export const isEditorJsLogsEnabled = () => {
23
+ return getEditorConfig().jsLogsEnabled === true
24
+ }
25
+
26
+ export const log = (...args) => {
27
+ if (isEditorJsLogsEnabled()) {
28
+ console.log(...args)
29
+ }
30
+ }
31
+
10
32
  export const debounce = (fn, time) => {
11
33
  let timeoutId
12
34
  function wrapper(...args) {
@@ -1,5 +1,6 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
2
  import { useDebounce } from 'stimulus-use'
3
+ import { log } from 'maglev-controllers/utils'
3
4
 
4
5
  export default class extends Controller {
5
6
  static targets = ['lockVersion']
@@ -20,7 +21,7 @@ export default class extends Controller {
20
21
  }
21
22
 
22
23
  onSettingChange(event) {
23
- console.log('[SectionForm::onSettingChange]', this.sourceId , event.detail)
24
+ log('[SectionForm::onSettingChange]', this.sourceId , event.detail)
24
25
  const { detail: { settingType, settingId, value } } = event
25
26
  this.dispatch('updateSetting', { detail: {
26
27
  sourceId: this.sourceId,
@@ -30,12 +31,12 @@ export default class extends Controller {
30
31
  }
31
32
 
32
33
  onPersist(event) {
33
- console.log('[SectionForm::onPersist]', event.detail, typeof event.detail)
34
+ log('[SectionForm::onPersist]', event.detail, typeof event.detail)
34
35
  this.lockVersionTarget.value = event.detail.lockVersion
35
36
  }
36
37
 
37
38
  async afterSettingUpdate(event) {
38
- console.log('[SectionForm::afterSettingUpdate] 🙌🙌', event.detail)
39
+ log('[SectionForm::afterSettingUpdate] 🙌🙌', event.detail)
39
40
  await this.element.requestSubmit()
40
41
 
41
42
  if (!event.detail.updated) {
@@ -1,10 +1,11 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
+ import { log } from 'maglev-controllers/utils'
2
3
 
3
4
  export default class extends Controller {
4
5
  static values = { style: Array }
5
6
 
6
7
  update(event) {
7
- console.log('[StyleForm] update', event.detail, this.styleValue)
8
+ log('[StyleForm] update', event.detail, this.styleValue)
8
9
 
9
10
  const newStyle = this.styleValue
10
11
  newStyle.forEach(style => {
@@ -6,9 +6,12 @@ export default class extends Controller {
6
6
  connect() {
7
7
  this.setupTransformations()
8
8
  this.numberOfSections = null
9
+ this.syncPreviewTimer = null
9
10
  }
10
11
 
11
12
  disconnect() {
13
+ clearTimeout(this.syncPreviewTimer)
14
+ this.syncPreviewTimer = null
12
15
  this.teardownTransformations()
13
16
  }
14
17
 
@@ -33,6 +36,14 @@ export default class extends Controller {
33
36
  this.element.classList.remove('is-empty')
34
37
  }
35
38
 
39
+ // force reload the iframe
40
+ reload() {
41
+ this.startLoading()
42
+ if (!this.reloadIframeInPlace()) {
43
+ this.iframeTarget.src = this.iframeTarget.src
44
+ }
45
+ }
46
+
36
47
  // called when the Maglev client JS lib has been fully loaded on the iframe
37
48
  clientReady(event) {
38
49
  const { numberOfSections } = event.detail
@@ -47,20 +58,100 @@ export default class extends Controller {
47
58
  this.updateEmptyMessageState()
48
59
  }
49
60
 
50
- // called when the user navigates to a new page in the editor (another Maglev page OR in a different locale)
61
+ // Preview markup lives outside the Turbo morph root (#root), so the iframe persists while <head>
62
+ // meta updates. Reading meta on turbo:load can race; we debounce. After Back, compare the live
63
+ // iframe document URL (same-origin) to meta, not only the src attribute.
51
64
  detectUrlChange() {
52
- const currentPath = new URL(this.iframeTarget.src).pathname
53
- const newPath = document.querySelector('meta[name=page-preview-url]').content
65
+ clearTimeout(this.syncPreviewTimer)
66
+ this.syncPreviewTimer = setTimeout(() => {
67
+ this.syncPreviewTimer = null
68
+ this.syncPreview()
69
+ }, 0)
70
+ }
71
+
72
+ syncPreview() {
73
+ const meta = document.querySelector('meta[name=page-preview-url]')
74
+ if (!meta?.content) return
75
+
76
+ let targetHref
77
+ try {
78
+ targetHref = new URL(meta.content, document.baseURI).href
79
+ } catch {
80
+ return
81
+ }
54
82
 
55
- if (currentPath !== newPath) {
83
+ const currentHref = this.livePreviewHref()
84
+ const targetKey = this.previewUrlKey(targetHref)
85
+ const currentKey = this.previewUrlKey(currentHref)
86
+
87
+ if (currentKey !== targetKey) {
56
88
  this.startLoading()
57
- this.iframeTarget.src = newPath
89
+ this.assignIframeSrcWithoutHistory(targetHref)
58
90
  } else {
59
91
  this.element.classList.add('is-loaded')
60
92
  this.updateEmptyMessageState()
61
93
  }
62
94
  }
63
95
 
96
+ livePreviewHref() {
97
+ try {
98
+ const win = this.iframeTarget.contentWindow
99
+ const href = win?.location?.href
100
+ if (href && !href.startsWith("about:")) {
101
+ return new URL(href, document.baseURI).href
102
+ }
103
+ } catch {
104
+ // cross-origin preview document
105
+ }
106
+ try {
107
+ return new URL(this.iframeTarget.src, document.baseURI).href
108
+ } catch {
109
+ return this.iframeTarget.src
110
+ }
111
+ }
112
+
113
+ previewUrlKey(href) {
114
+ try {
115
+ const u = new URL(href, document.baseURI)
116
+ u.hash = ""
117
+ let path = u.pathname
118
+ if (path.length > 1 && path.endsWith("/")) {
119
+ path = path.slice(0, -1)
120
+ }
121
+ return `${u.origin}${path}${u.search}`
122
+ } catch {
123
+ return href
124
+ }
125
+ }
126
+
127
+ // Assigning iframe.src pushes a joint session-history entry in most browsers, so the first Back
128
+ // pops the iframe instead of the parent Turbo visit. replace/reload avoid that when same-origin.
129
+ assignIframeSrcWithoutHistory(resolvedHref) {
130
+ try {
131
+ const win = this.iframeTarget.contentWindow
132
+ if (win?.location?.replace) {
133
+ win.location.replace(resolvedHref)
134
+ return
135
+ }
136
+ } catch {
137
+ // cross-origin iframe document: fall back to src
138
+ }
139
+ this.iframeTarget.src = resolvedHref
140
+ }
141
+
142
+ reloadIframeInPlace() {
143
+ try {
144
+ const win = this.iframeTarget.contentWindow
145
+ if (win?.location?.reload) {
146
+ win.location.reload()
147
+ return true
148
+ }
149
+ } catch {
150
+ // cross-origin: caller will fall back to reassigning src
151
+ }
152
+ return false
153
+ }
154
+
64
155
  updateEmptyMessageState() {
65
156
  if (this.numberOfSections === 0) {
66
157
  this.element.classList.add('is-empty')
@@ -1,25 +1,42 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
- import { isSamePath } from "maglev-controllers/utils"
2
+ import { isSamePath, log } from "maglev-controllers/utils"
3
3
 
4
4
  export default class extends Controller {
5
5
  static targets = ["iframe"]
6
6
  static values = {
7
7
  sectionPath: String,
8
- sectionBlockPath: String
8
+ sectionBlockPath: String,
9
+ primaryColor: String,
10
+ stickySectionIds: Array,
9
11
  }
10
12
 
11
13
  connect() {
12
14
  this.isClientReady = false
13
- this.clientReadyCallbacks = []
15
+ this.clientReadyCallbacks = []
16
+ this.prefetchedPaths = new Set()
17
+ this.lastConfiguredDocument = null
18
+
19
+ // The iframe `load` event can fire before Stimulus wires `load->sendConfig`.
20
+ // If that happens, no config is sent and the client stays undecorated.
21
+ this.sendConfigIfIframeAlreadyLoaded()
14
22
  }
15
23
 
16
24
  // called when the iframe DOM is loaded
17
- sendConfig(event) {
18
- const { primaryColor, stickySectionIds } = event.params
25
+ sendConfig() {
26
+ const { primaryColorValue, stickySectionIdsValue } = this
27
+ const iframeDocument = this.iframeTarget?.contentDocument
28
+ if (!iframeDocument) return
29
+
30
+ // Prevent duplicate `config` for the same document (load + fallback path).
31
+ if (this.lastConfiguredDocument === iframeDocument) return
32
+
19
33
  this.postMessage('config', {
20
- primaryColor,
21
- stickySectionIds,
34
+ primaryColor: primaryColorValue,
35
+ stickySectionIds: stickySectionIdsValue,
22
36
  })
37
+
38
+ // Prevent duplicate `config` for the same document (load + fallback path).
39
+ this.lastConfiguredDocument = iframeDocument
23
40
  }
24
41
 
25
42
  // called when the Maglev client JS lib has been fully loaded on the iframe
@@ -30,61 +47,70 @@ export default class extends Controller {
30
47
 
31
48
  // called by the iframe when the user clicks on a setting of a section or a section block
32
49
  editSection(event) {
33
- console.log('[PreviewNotificationCenter][editSection]', event.detail)
50
+ log('[PreviewNotificationCenter][editSection]', event.detail)
34
51
  const { sectionId, sectionBlockId, settingId } = event.detail
35
- const pathTemplate = sectionBlockId ? this.sectionBlockPathValue : this.sectionPathValue
36
- const path = `${pathTemplate}#${settingId}`.replace(':section_id', sectionId).replace(':section_block_id', sectionBlockId)
52
+ const path = this.buildSettingPath({ sectionId, sectionBlockId, settingId })
37
53
 
38
54
  if (isSamePath(path)) {
39
55
  window.location.hash = settingId
40
56
  } else {
41
- Turbo.visit(path)
57
+ this.visitPath(path)
42
58
  }
43
59
  }
44
60
 
45
61
  editSectionBlock(event) {
46
- console.log('[PreviewNotificationCenter][editSectionBlock]', event.detail)
62
+ log('[PreviewNotificationCenter][editSectionBlock]', event.detail)
47
63
  const { sectionId, sectionBlockId } = event.detail
48
64
  const pathTemplate = this.sectionBlockPathValue
49
65
  const path = `${pathTemplate}#${sectionBlockId}`.replace(':section_id', sectionId).replace(':section_block_id', sectionBlockId)
50
- Turbo.visit(path)
66
+ this.visitPath(path)
67
+ }
68
+
69
+ prefetchEditSectionOrBlock(event) {
70
+ const { sectionId, sectionBlockId, settingId } = event.detail
71
+ const path = this.buildSettingPath({ sectionId, sectionBlockId, settingId })
72
+ const prefetchPath = this.stripHash(path)
73
+
74
+ if (isSamePath(prefetchPath) || this.prefetchedPaths.has(prefetchPath)) return
75
+
76
+ this.enqueuePrefetch(prefetchPath)
51
77
  }
52
78
 
53
79
  // === SECTIONS ===
54
80
 
55
81
  addSection(event) {
56
- console.log('addSection', event.detail.fetchResponse.response.headers.get('X-Section-Id'), event.detail.fetchResponse.response.headers.get('X-Section-Position'))
82
+ log('addSection', event.detail.fetchResponse.response.headers.get('X-Section-Id'), event.detail.fetchResponse.response.headers.get('X-Section-Position'))
57
83
  const sectionId = event.detail.fetchResponse.response.headers.get('X-Section-Id')
58
84
  const position = event.detail.fetchResponse.response.headers.get('X-Section-Position')
59
85
  this.postMessage('section:add', { sectionId, insertAt: parseInt(position) })
60
86
  }
61
87
 
62
88
  deleteSection(event) {
63
- console.log('deleteSection', event.params)
89
+ log('deleteSection', event.params)
64
90
  const { sectionId } = event.params
65
91
  this.postMessage('section:remove', { sectionId })
66
92
  }
67
93
 
68
94
  moveSection(event) {
69
- console.log('moveSection 💨💨💨', event.detail)
95
+ log('moveSection 💨💨💨', event.detail)
70
96
  const { oldIndex, newIndex } = event.detail
71
97
  this.postMessage('section:move', { oldIndex, newIndex })
72
98
  }
73
99
 
74
100
  updateSection(event) {
75
- console.log('updateSection 🧼🧼🧼', event.detail)
101
+ log('updateSection 🧼🧼🧼', event.detail)
76
102
  const { sectionId } = event.detail
77
103
  this.postMessage('section:update', { sectionId })
78
104
  }
79
105
 
80
106
  checkSectionLockVersion(event) {
81
- console.log('checkSectionLockVersion 🕵🏻‍♂️🕵🏻‍♂️🕵🏻‍♂️', event)
107
+ log('checkSectionLockVersion 🕵🏻‍♂️🕵🏻‍♂️🕵🏻‍♂️', event)
82
108
  const { sectionId, lockVersion } = event.detail
83
109
  this.postMessage('section:checkLockVersion', { sectionId, lockVersion })
84
110
  }
85
111
 
86
112
  pingSection(event) {
87
- console.log('pingSection 🏓🏓🏓', event.detail, this.isClientReady)
113
+ log('pingSection 🏓🏓🏓', event.detail, this.isClientReady)
88
114
  const { sectionId } = event.detail
89
115
  this.postMessageWhenClientReady('section:ping', { sectionId })
90
116
  }
@@ -92,26 +118,26 @@ export default class extends Controller {
92
118
  // === SECTION BLOCKS ===
93
119
 
94
120
  addSectionBlock(event) {
95
- console.log('addSectionBlock ➕➕➕', event.params)
121
+ log('addSectionBlock ➕➕➕', event.params)
96
122
  const { sectionId } = event.params
97
123
  this.postMessage('block:add', { sectionId })
98
124
  }
99
125
 
100
126
  deleteSectionBlock(event) {
101
- console.log('deleteSectionBlock 🗑️🗑️🗑️', event.params)
127
+ log('deleteSectionBlock 🗑️🗑️🗑️', event.params)
102
128
  const { sectionId, sectionBlockId } = event.params
103
129
  this.postMessage('block:remove', { sectionId, sectionBlockId })
104
130
  }
105
131
 
106
132
  moveSectionBlocks(event) {
107
- console.log('moveSectionBlocks 💨💨💨', event.params)
133
+ log('moveSectionBlocks 💨💨💨', event.params)
108
134
  const sectionId = event.params.sectionId
109
135
  const { oldItemId: sectionBlockId, newItemId: targetSectionBlockId, direction } = event.detail
110
136
  this.postMessage('block:move', { sectionId, sectionBlockId, targetSectionBlockId, direction })
111
137
  }
112
138
 
113
139
  pingSectionBlock(event) {
114
- console.log('pingSectionBlock 🏓🏓🏓', event.detail)
140
+ log('pingSectionBlock 🏓🏓🏓', event.detail)
115
141
  const { sectionBlockId } = event.detail
116
142
  this.postMessageWhenClientReady('block:ping', { sectionBlockId })
117
143
  }
@@ -132,6 +158,62 @@ export default class extends Controller {
132
158
 
133
159
  // === UTILS ===
134
160
 
161
+ buildSettingPath({ sectionId, sectionBlockId, settingId }) {
162
+ const pathTemplate = sectionBlockId ? this.sectionBlockPathValue : this.sectionPathValue
163
+ return `${pathTemplate}#${settingId}`.replace(':section_id', sectionId).replace(':section_block_id', sectionBlockId)
164
+ }
165
+
166
+ stripHash(path) {
167
+ const url = new URL(path, window.location.origin)
168
+ url.hash = ''
169
+ return `${url.pathname}${url.search}`
170
+ }
171
+
172
+ enqueuePrefetch(path) {
173
+ if (this.prefetchedPaths.has(path)) return
174
+
175
+ this.prefetchedPaths.add(path)
176
+
177
+ // Use Turbo's own prefetch pipeline so the visit cache is warmed,
178
+ // which avoids the full loading-bar behavior on subsequent click.
179
+ const link = document.createElement('a')
180
+ link.href = path
181
+ link.dataset.maglevPrefetch = 'true'
182
+ link.dataset.turboPrefetch = 'true'
183
+ link.hidden = true
184
+ document.body.appendChild(link)
185
+
186
+ link.dispatchEvent(new MouseEvent('mouseenter', {
187
+ bubbles: true,
188
+ cancelable: true,
189
+ view: window,
190
+ }))
191
+
192
+ setTimeout(() => link.remove(), 500)
193
+ }
194
+
195
+ visitPath(path) {
196
+ const url = new URL(path, window.location.origin)
197
+ const visitPath = `${url.pathname}${url.search}`
198
+ const hash = url.hash
199
+
200
+ if (hash) this.applyHashAfterVisit(hash)
201
+ Turbo.visit(visitPath)
202
+ }
203
+
204
+ applyHashAfterVisit(hash) {
205
+ window.addEventListener('turbo:load', () => {
206
+ window.location.hash = hash
207
+ }, { once: true })
208
+ }
209
+
210
+ sendConfigIfIframeAlreadyLoaded() {
211
+ const iframeDocument = this.iframeTarget?.contentDocument
212
+ if (!iframeDocument || iframeDocument.readyState !== 'complete') return
213
+
214
+ this.sendConfig()
215
+ }
216
+
135
217
  postMessage(type, data) {
136
218
  this.iframeTarget.contentWindow.postMessage({ type: `maglev:${type}`, ...(data || {}) }, '*')
137
219
  }
@@ -1,4 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
+ import { log } from 'maglev-controllers/utils'
2
3
 
3
4
  export default class extends Controller {
4
5
  static values = {
@@ -8,7 +9,7 @@ export default class extends Controller {
8
9
  }
9
10
 
10
11
  connect() {
11
- console.log('[Editor::Setting] connect', this.settingIdValue)
12
+ log('[Editor::Setting] connect', this.settingIdValue)
12
13
 
13
14
  if (window.location.hash) {
14
15
  // dirty way to wait for the underlying controller to be connected
@@ -17,7 +18,7 @@ export default class extends Controller {
17
18
  }
18
19
 
19
20
  change(event) {
20
- console.log('[Editor::Setting] change', event.detail)
21
+ log('[Editor::Setting] change', event.detail)
21
22
  this.dispatch('change', {
22
23
  detail: {
23
24
  settingId: this.settingIdValue,
@@ -1,4 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
+ import { log } from 'maglev-controllers/utils'
2
3
 
3
4
  export default class extends Controller {
4
5
  static targets = ['text', 'success']
@@ -15,7 +16,7 @@ export default class extends Controller {
15
16
  if (navigator.clipboard) {
16
17
  navigator.clipboard.writeText(this.sourceValue)
17
18
  } else {
18
- console.log('Clipboard API not supported or unavailable.')
19
+ log('Clipboard API not supported or unavailable.')
19
20
  }
20
21
 
21
22
  this.timeout = setTimeout(() => {
@@ -5,6 +5,28 @@ 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))
10
32
  }
@@ -2,8 +2,9 @@ import "@hotwired/turbo-rails"
2
2
  import "maglev-controllers"
3
3
  import "maglev-patches/page_renderer_patch"
4
4
  import "maglev-patches/turbo_stream_patch"
5
+ import { log } from "maglev-controllers/utils"
5
6
 
6
- console.log('Maglev Editor v2 ⚡️')
7
+ console.log('Maglev Editor v3 ⚡️')
7
8
 
8
9
  // We need to set the content locale in the headers for each Turbo request
9
10
  document.addEventListener("turbo:before-fetch-request", (event) => {