panda-cms 0.7.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 (233) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +73 -0
  3. data/Rakefile +7 -0
  4. data/app/assets/builds/panda.cms.css +2808 -0
  5. data/app/assets/config/panda_cms_manifest.js +4 -0
  6. data/app/assets/stylesheets/panda/cms/application.tailwind.css +162 -0
  7. data/app/assets/stylesheets/panda/cms/editor.css +120 -0
  8. data/app/builders/panda/cms/form_builder.rb +234 -0
  9. data/app/components/panda/cms/admin/button_component.rb +70 -0
  10. data/app/components/panda/cms/admin/container_component.html.erb +13 -0
  11. data/app/components/panda/cms/admin/container_component.rb +13 -0
  12. data/app/components/panda/cms/admin/flash_message_component.html.erb +31 -0
  13. data/app/components/panda/cms/admin/flash_message_component.rb +47 -0
  14. data/app/components/panda/cms/admin/heading_component.rb +45 -0
  15. data/app/components/panda/cms/admin/panel_component.html.erb +7 -0
  16. data/app/components/panda/cms/admin/panel_component.rb +13 -0
  17. data/app/components/panda/cms/admin/slideover_component.html.erb +9 -0
  18. data/app/components/panda/cms/admin/slideover_component.rb +15 -0
  19. data/app/components/panda/cms/admin/statistics_component.html.erb +4 -0
  20. data/app/components/panda/cms/admin/statistics_component.rb +17 -0
  21. data/app/components/panda/cms/admin/tab_bar_component.html.erb +35 -0
  22. data/app/components/panda/cms/admin/tab_bar_component.rb +15 -0
  23. data/app/components/panda/cms/admin/table_component.html.erb +29 -0
  24. data/app/components/panda/cms/admin/table_component.rb +46 -0
  25. data/app/components/panda/cms/admin/tag_component.rb +35 -0
  26. data/app/components/panda/cms/admin/user_activity_component.html.erb +5 -0
  27. data/app/components/panda/cms/admin/user_activity_component.rb +33 -0
  28. data/app/components/panda/cms/admin/user_display_component.html.erb +17 -0
  29. data/app/components/panda/cms/admin/user_display_component.rb +21 -0
  30. data/app/components/panda/cms/code_component.rb +64 -0
  31. data/app/components/panda/cms/grid_component.html.erb +6 -0
  32. data/app/components/panda/cms/grid_component.rb +15 -0
  33. data/app/components/panda/cms/menu_component.html.erb +6 -0
  34. data/app/components/panda/cms/menu_component.rb +58 -0
  35. data/app/components/panda/cms/page_menu_component.html.erb +21 -0
  36. data/app/components/panda/cms/page_menu_component.rb +38 -0
  37. data/app/components/panda/cms/rich_text_component.html.erb +6 -0
  38. data/app/components/panda/cms/rich_text_component.rb +84 -0
  39. data/app/components/panda/cms/text_component.rb +72 -0
  40. data/app/constraints/panda/cms/admin_constraint.rb +18 -0
  41. data/app/controllers/panda/cms/admin/block_contents_controller.rb +52 -0
  42. data/app/controllers/panda/cms/admin/dashboard_controller.rb +20 -0
  43. data/app/controllers/panda/cms/admin/files_controller.rb +21 -0
  44. data/app/controllers/panda/cms/admin/forms_controller.rb +53 -0
  45. data/app/controllers/panda/cms/admin/menus_controller.rb +30 -0
  46. data/app/controllers/panda/cms/admin/pages_controller.rb +91 -0
  47. data/app/controllers/panda/cms/admin/posts_controller.rb +146 -0
  48. data/app/controllers/panda/cms/admin/sessions_controller.rb +94 -0
  49. data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +37 -0
  50. data/app/controllers/panda/cms/admin/settings_controller.rb +20 -0
  51. data/app/controllers/panda/cms/application_controller.rb +57 -0
  52. data/app/controllers/panda/cms/errors_controller.rb +33 -0
  53. data/app/controllers/panda/cms/form_submissions_controller.rb +23 -0
  54. data/app/controllers/panda/cms/pages_controller.rb +72 -0
  55. data/app/controllers/panda/cms/posts_controller.rb +13 -0
  56. data/app/helpers/panda/cms/admin/files_helper.rb +6 -0
  57. data/app/helpers/panda/cms/admin/pages_helper.rb +6 -0
  58. data/app/helpers/panda/cms/admin/posts_helper.rb +48 -0
  59. data/app/helpers/panda/cms/application_helper.rb +120 -0
  60. data/app/helpers/panda/cms/pages_helper.rb +6 -0
  61. data/app/helpers/panda/cms/theme_helper.rb +18 -0
  62. data/app/javascript/panda/cms/@editorjs--editorjs.js +2577 -0
  63. data/app/javascript/panda/cms/@hotwired--stimulus.js +4 -0
  64. data/app/javascript/panda/cms/@hotwired--turbo.js +160 -0
  65. data/app/javascript/panda/cms/@rails--actioncable--src.js +4 -0
  66. data/app/javascript/panda/cms/application_panda_cms.js +39 -0
  67. data/app/javascript/panda/cms/controllers/dashboard_controller.js +7 -0
  68. data/app/javascript/panda/cms/controllers/editor_form_controller.js +77 -0
  69. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +320 -0
  70. data/app/javascript/panda/cms/controllers/index.js +48 -0
  71. data/app/javascript/panda/cms/controllers/slug_controller.js +87 -0
  72. data/app/javascript/panda/cms/editor/css_extractor.js +80 -0
  73. data/app/javascript/panda/cms/editor/editor_js_config.js +177 -0
  74. data/app/javascript/panda/cms/editor/editor_js_initializer.js +285 -0
  75. data/app/javascript/panda/cms/editor/plain_text_editor.js +110 -0
  76. data/app/javascript/panda/cms/editor/resource_loader.js +115 -0
  77. data/app/javascript/panda/cms/tailwindcss-stimulus-components.js +4 -0
  78. data/app/jobs/panda/cms/application_job.rb +6 -0
  79. data/app/jobs/panda/cms/record_visit_job.rb +31 -0
  80. data/app/mailers/panda/cms/application_mailer.rb +8 -0
  81. data/app/mailers/panda/cms/form_mailer.rb +21 -0
  82. data/app/models/action_text/rich_text_version.rb +6 -0
  83. data/app/models/panda/cms/application_record.rb +7 -0
  84. data/app/models/panda/cms/block.rb +34 -0
  85. data/app/models/panda/cms/block_content.rb +18 -0
  86. data/app/models/panda/cms/block_content_version.rb +8 -0
  87. data/app/models/panda/cms/breadcrumb.rb +12 -0
  88. data/app/models/panda/cms/current.rb +17 -0
  89. data/app/models/panda/cms/form.rb +9 -0
  90. data/app/models/panda/cms/form_submission.rb +7 -0
  91. data/app/models/panda/cms/menu.rb +52 -0
  92. data/app/models/panda/cms/menu_item.rb +58 -0
  93. data/app/models/panda/cms/page.rb +96 -0
  94. data/app/models/panda/cms/page_version.rb +8 -0
  95. data/app/models/panda/cms/post.rb +60 -0
  96. data/app/models/panda/cms/post_version.rb +8 -0
  97. data/app/models/panda/cms/redirect.rb +11 -0
  98. data/app/models/panda/cms/template.rb +124 -0
  99. data/app/models/panda/cms/template_version.rb +8 -0
  100. data/app/models/panda/cms/user.rb +31 -0
  101. data/app/models/panda/cms/version.rb +8 -0
  102. data/app/models/panda/cms/visit.rb +9 -0
  103. data/app/services/panda/cms/html_to_editor_js_converter.rb +200 -0
  104. data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
  105. data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
  106. data/app/views/layouts/panda/cms/application.html.erb +41 -0
  107. data/app/views/layouts/panda/cms/public.html.erb +3 -0
  108. data/app/views/panda/cms/admin/dashboard/show.html.erb +12 -0
  109. data/app/views/panda/cms/admin/files/index.html.erb +124 -0
  110. data/app/views/panda/cms/admin/files/show.html.erb +2 -0
  111. data/app/views/panda/cms/admin/forms/edit.html.erb +0 -0
  112. data/app/views/panda/cms/admin/forms/index.html.erb +13 -0
  113. data/app/views/panda/cms/admin/forms/new.html.erb +15 -0
  114. data/app/views/panda/cms/admin/forms/show.html.erb +35 -0
  115. data/app/views/panda/cms/admin/menus/index.html.erb +8 -0
  116. data/app/views/panda/cms/admin/pages/edit.html.erb +36 -0
  117. data/app/views/panda/cms/admin/pages/index.html.erb +22 -0
  118. data/app/views/panda/cms/admin/pages/new.html.erb +15 -0
  119. data/app/views/panda/cms/admin/pages/show.html.erb +1 -0
  120. data/app/views/panda/cms/admin/posts/_form.html.erb +29 -0
  121. data/app/views/panda/cms/admin/posts/edit.html.erb +6 -0
  122. data/app/views/panda/cms/admin/posts/index.html.erb +18 -0
  123. data/app/views/panda/cms/admin/posts/new.html.erb +6 -0
  124. data/app/views/panda/cms/admin/sessions/new.html.erb +17 -0
  125. data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +68 -0
  126. data/app/views/panda/cms/admin/settings/index.html.erb +21 -0
  127. data/app/views/panda/cms/admin/settings/insta.html +4 -0
  128. data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +28 -0
  129. data/app/views/panda/cms/admin/shared/_flash.html.erb +5 -0
  130. data/app/views/panda/cms/admin/shared/_sidebar.html.erb +41 -0
  131. data/app/views/panda/cms/form_mailer/notification_email.html.erb +11 -0
  132. data/app/views/panda/cms/shared/_editor.html.erb +0 -0
  133. data/app/views/panda/cms/shared/_favicons.html.erb +9 -0
  134. data/app/views/panda/cms/shared/_footer.html.erb +2 -0
  135. data/app/views/panda/cms/shared/_header.html.erb +15 -0
  136. data/app/views/panda/cms/shared/_importmap.html.erb +33 -0
  137. data/config/importmap.rb +13 -0
  138. data/config/initializers/inflections.rb +3 -0
  139. data/config/initializers/panda/cms/form_errors.rb +38 -0
  140. data/config/initializers/panda/cms/healthcheck_log_silencer.rb +11 -0
  141. data/config/initializers/panda/cms/paper_trail.rb +7 -0
  142. data/config/initializers/panda/cms.rb +10 -0
  143. data/config/initializers/zeitwork.rb +3 -0
  144. data/config/locales/en.yml +49 -0
  145. data/config/puma/test.rb +9 -0
  146. data/config/routes.rb +48 -0
  147. data/config/tailwind.config.js +37 -0
  148. data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
  149. data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
  150. data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
  151. data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
  152. data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
  153. data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
  154. data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
  155. data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
  156. data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
  157. data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
  158. data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +82 -0
  159. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
  160. data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
  161. data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
  162. data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
  163. data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
  164. data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
  165. data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
  166. data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
  167. data/db/migrate/20240317214827_create_panda_cms_redirects.rb +14 -0
  168. data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
  169. data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
  170. data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
  171. data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
  172. data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +28 -0
  173. data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
  174. data/db/migrate/20240804235210_create_panda_cms_forms.rb +11 -0
  175. data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +9 -0
  176. data/db/migrate/20240805121123_create_panda_cms_posts.rb +27 -0
  177. data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +14 -0
  178. data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +13 -0
  179. data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +5 -0
  180. data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +5 -0
  181. data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
  182. data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +11 -0
  183. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
  184. data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +35 -0
  185. data/db/migrate/20241119214549_remove_action_text_from_posts.rb +9 -0
  186. data/db/migrate/20241120000419_remove_post_tag_references.rb +19 -0
  187. data/db/migrate/20241120110943_add_editor_js_to_posts.rb +27 -0
  188. data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +5 -0
  189. data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +5 -0
  190. data/db/migrate/migrate +1 -0
  191. data/db/seeds.rb +5 -0
  192. data/lib/generators/panda/cms/install_generator.rb +29 -0
  193. data/lib/panda/cms/bulk_editor.rb +171 -0
  194. data/lib/panda/cms/demo_site_generator.rb +67 -0
  195. data/lib/panda/cms/editor_js/blocks/alert.rb +34 -0
  196. data/lib/panda/cms/editor_js/blocks/base.rb +33 -0
  197. data/lib/panda/cms/editor_js/blocks/header.rb +15 -0
  198. data/lib/panda/cms/editor_js/blocks/image.rb +36 -0
  199. data/lib/panda/cms/editor_js/blocks/list.rb +32 -0
  200. data/lib/panda/cms/editor_js/blocks/paragraph.rb +15 -0
  201. data/lib/panda/cms/editor_js/blocks/quote.rb +41 -0
  202. data/lib/panda/cms/editor_js/blocks/table.rb +50 -0
  203. data/lib/panda/cms/editor_js/renderer.rb +124 -0
  204. data/lib/panda/cms/editor_js.rb +16 -0
  205. data/lib/panda/cms/editor_js_content.rb +21 -0
  206. data/lib/panda/cms/engine.rb +257 -0
  207. data/lib/panda/cms/exceptions_app.rb +26 -0
  208. data/lib/panda/cms/railtie.rb +11 -0
  209. data/lib/panda/cms/slug.rb +24 -0
  210. data/lib/panda/cms.rb +0 -0
  211. data/lib/panda-cms/version.rb +5 -0
  212. data/lib/panda-cms.rb +81 -0
  213. data/lib/tasks/panda_cms.rake +54 -0
  214. data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
  215. data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
  216. data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
  217. data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
  218. data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
  219. data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
  220. data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
  221. data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
  222. data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
  223. data/public/panda-cms-assets/favicons/browserconfig.xml +9 -0
  224. data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
  225. data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
  226. data/public/panda-cms-assets/favicons/favicon.ico +0 -0
  227. data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
  228. data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +61 -0
  229. data/public/panda-cms-assets/favicons/site.webmanifest +14 -0
  230. data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
  231. data/public/panda-cms-assets/panda-nav.png +0 -0
  232. data/public/panda-cms-assets/rich_text_editor.css +568 -0
  233. metadata +654 -0
@@ -0,0 +1,77 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = ["editorContainer", "hiddenField"];
5
+ static values = {
6
+ editorId: String,
7
+ };
8
+
9
+ connect() {
10
+ this.initializeEditor();
11
+ }
12
+
13
+ async initializeEditor() {
14
+ if (this.editor) return;
15
+
16
+ try {
17
+ const holderId =
18
+ this.editorIdValue + "_holder" ||
19
+ `editor-${Math.random().toString(36).substring(2, 9)}`;
20
+ let holderDiv = document.createElement("div");
21
+ holderDiv.id = holderId;
22
+ holderDiv.className = "codex-editor";
23
+ this.editorContainerTarget.innerHTML = "";
24
+ this.editorContainerTarget.appendChild(holderDiv);
25
+
26
+ const { getEditorConfig } = await import(
27
+ "panda/cms/editor/editor_js_config"
28
+ );
29
+ const config = getEditorConfig(holderId, this.getInitialContent());
30
+
31
+ editor_content_post;
32
+
33
+ this.editor = new EditorJS({
34
+ ...config,
35
+ holder: holderId,
36
+ autofocus: false,
37
+ minHeight: 1,
38
+ logLevel: "ERROR",
39
+ onChange: () => {
40
+ if (!this.editor) return;
41
+ this.editor.save().then((outputData) => {
42
+ outputData.source = "editorJS";
43
+ this.hiddenFieldTarget.value = JSON.stringify(outputData);
44
+ });
45
+ },
46
+ });
47
+ } catch (error) {
48
+ console.error("[Panda CMS] Editor setup failed:", error);
49
+ }
50
+ }
51
+
52
+ getInitialContent() {
53
+ try {
54
+ const value = this.hiddenFieldTarget.value;
55
+ if (value && value !== "{}") {
56
+ const data = JSON.parse(value);
57
+ if (data.blocks) return data;
58
+ }
59
+ } catch (e) {
60
+ console.warn("[Panda CMS] Could not parse initial content:", e);
61
+ }
62
+
63
+ return {
64
+ time: Date.now(),
65
+ blocks: [{ type: "paragraph", data: { text: "" } }],
66
+ version: "2.28.2",
67
+ source: "editorJS",
68
+ };
69
+ }
70
+
71
+ disconnect() {
72
+ if (this.editor) {
73
+ this.editor.destroy();
74
+ this.editor = null;
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,320 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { PlainTextEditor } from "panda/cms/editor/plain_text_editor"
3
+ import { EditorJSInitializer } from "panda/cms/editor/editor_js_initializer"
4
+
5
+ export default class extends Controller {
6
+ static values = {
7
+ pageId: Number,
8
+ adminPath: String,
9
+ autosave: Boolean
10
+ }
11
+
12
+ connect() {
13
+ console.debug("[Panda CMS] EditorIframe controller connected")
14
+ this.frame = this.element
15
+ this.setupControls()
16
+ this.setupFrame()
17
+ this.editors = []
18
+ this.editorsInitialized = {
19
+ plain: false,
20
+ rich: false
21
+ }
22
+ }
23
+
24
+ setupControls() {
25
+ // Create editor controls if they don't exist
26
+ if (!parent.document.querySelector('.editor-controls')) {
27
+ const controls = parent.document.createElement('div')
28
+ controls.className = 'editor-controls'
29
+ parent.document.body.appendChild(controls)
30
+ }
31
+
32
+ // Create save button if it doesn't exist
33
+ if (!parent.document.getElementById('saveEditableButton')) {
34
+ const saveButton = parent.document.createElement('a')
35
+ saveButton.id = 'saveEditableButton'
36
+ saveButton.href = '#'
37
+ saveButton.textContent = 'Save Changes'
38
+ saveButton.className = 'btn btn-primary'
39
+ parent.document.querySelector('.editor-controls').appendChild(saveButton)
40
+ }
41
+ }
42
+
43
+ setupFrame() {
44
+ // Always show the frame initially to ensure it's visible for tests
45
+ this.frame.style.display = ""
46
+ this.frame.style.width = "100%"
47
+ this.frame.style.height = "100%"
48
+ this.frame.style.minHeight = "500px"
49
+
50
+ // Get CSRF token
51
+ this.csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ""
52
+
53
+ // Setup frame load handler
54
+ this.frame.addEventListener("load", async () => {
55
+ console.debug("[Panda CMS] Frame loaded")
56
+ this.frameDocument = this.frame.contentDocument || this.frame.contentWindow.document
57
+ this.body = this.frameDocument.body
58
+ this.head = this.frameDocument.head
59
+
60
+ // Set up error handling for the iframe
61
+ this.frameDocument.defaultView.onerror = (message, source, lineno, colno, error) => {
62
+ // Relay the error to the parent window
63
+ const fullMessage = `iFrame Error: ${message} (${source}:${lineno}:${colno})`
64
+ console.error(fullMessage, error)
65
+
66
+ // Throw the error in the parent context for Cuprite to catch
67
+ setTimeout(() => {
68
+ throw new Error(fullMessage)
69
+ }, 0)
70
+
71
+ return false // Let the error propagate
72
+ }
73
+
74
+ // Set up unhandled rejection handling for the iframe
75
+ this.frameDocument.defaultView.onunhandledrejection = (event) => {
76
+ const fullMessage = `iFrame Unhandled Promise Rejection: ${event.reason}`
77
+ console.error(fullMessage)
78
+
79
+ // Throw the error in the parent context for Cuprite to catch
80
+ setTimeout(() => {
81
+ throw event.reason
82
+ }, 0)
83
+ }
84
+
85
+ // Ensure frame is visible after load
86
+ this.frame.style.display = ""
87
+ this.ensureFrameVisibility()
88
+
89
+ // Wait for document to be ready
90
+ if (this.frameDocument.readyState !== 'complete') {
91
+ await new Promise(resolve => {
92
+ this.frameDocument.addEventListener('DOMContentLoaded', resolve)
93
+ })
94
+ }
95
+
96
+ // Load Editor.js resources in the iframe context
97
+ try {
98
+ const { EDITOR_JS_RESOURCES, EDITOR_JS_CSS } = await import("panda/cms/editor/editor_js_config")
99
+ const { ResourceLoader } = await import("panda/cms/editor/resource_loader")
100
+
101
+ // First load EditorJS core
102
+ const editorCore = EDITOR_JS_RESOURCES[0]
103
+ await ResourceLoader.loadScript(this.frameDocument, this.head, editorCore)
104
+
105
+ // Then load all tools in parallel
106
+ const toolLoads = EDITOR_JS_RESOURCES.slice(1).map(async (resource) => {
107
+ await ResourceLoader.loadScript(this.frameDocument, this.head, resource)
108
+ })
109
+
110
+ // Load CSS directly
111
+ await ResourceLoader.embedCSS(this.frameDocument, this.head, EDITOR_JS_CSS)
112
+
113
+ // Wait for all resources to load
114
+ await Promise.all(toolLoads)
115
+ console.debug("[Panda CMS] Editor resources loaded in iframe")
116
+
117
+ // Wait a small amount of time for scripts to initialize
118
+ await new Promise(resolve => setTimeout(resolve, 100))
119
+
120
+ // Initialize editors only if we have the body and editable elements
121
+ if (this.body && this.body.querySelector('[data-editable-kind]')) {
122
+ await this.initializeEditors()
123
+ } else {
124
+ const error = new Error("[Panda CMS] Frame body or editable elements not found")
125
+ console.error(error)
126
+ throw error
127
+ }
128
+ } catch (error) {
129
+ console.error("[Panda CMS] Error loading editor resources in iframe:", error)
130
+ throw error
131
+ }
132
+ })
133
+ }
134
+
135
+ ensureFrameVisibility() {
136
+ // Force frame to be visible
137
+ this.frame.style.display = ""
138
+
139
+ // Check dimensions and fix if needed
140
+ if (this.frame.offsetWidth === 0 || this.frame.offsetHeight === 0) {
141
+ console.warn("[Panda CMS] iFrame has zero dimensions, fixing...")
142
+ this.frame.style.width = "100%"
143
+ this.frame.style.height = "100%"
144
+ this.frame.style.minHeight = "500px"
145
+ }
146
+
147
+ // Log visibility state
148
+ console.debug("[Panda CMS] Frame visibility state:", {
149
+ display: this.frame.style.display,
150
+ width: this.frame.offsetWidth,
151
+ height: this.frame.offsetHeight,
152
+ visible: this.frame.offsetParent !== null
153
+ })
154
+ }
155
+
156
+ initializeEditors() {
157
+ console.debug("[Panda CMS] Starting editor initialization")
158
+
159
+ // Get all editable elements
160
+ const plainTextElements = this.body.querySelectorAll('[data-editable-kind="plain_text"], [data-editable-kind="markdown"], [data-editable-kind="html"]')
161
+ const richTextElements = this.body.querySelectorAll('[data-editable-kind="rich_text"]')
162
+
163
+ console.debug(`[Panda CMS] Found ${plainTextElements.length} plain text elements and ${richTextElements.length} rich text elements`)
164
+
165
+ // Always ensure frame is visible
166
+ this.ensureFrameVisibility()
167
+
168
+ // Initialize editors if they exist
169
+ if (plainTextElements.length > 0 || richTextElements.length > 0) {
170
+ this.initializePlainTextEditors()
171
+ this.initializeRichTextEditors()
172
+ }
173
+ }
174
+
175
+ initializePlainTextEditors() {
176
+ this.editorsInitialized.plain = false
177
+ const plainTextElements = this.body.querySelectorAll('[data-editable-kind="plain_text"], [data-editable-kind="markdown"], [data-editable-kind="html"]')
178
+ console.debug(`[Panda CMS] Found ${plainTextElements.length} plain text elements`)
179
+
180
+ plainTextElements.forEach(element => {
181
+ const editor = new PlainTextEditor(element, this.frameDocument, {
182
+ autosave: this.autosaveValue,
183
+ adminPath: this.adminPathValue,
184
+ csrfToken: this.csrfToken
185
+ })
186
+ this.editors.push(editor)
187
+ })
188
+
189
+ this.editorsInitialized.plain = true
190
+ this.checkAllEditorsInitialized()
191
+ }
192
+
193
+ async initializeRichTextEditors() {
194
+ this.editorsInitialized.rich = false
195
+ const richTextElements = this.body.querySelectorAll('[data-editable-kind="rich_text"]')
196
+ console.debug(`[Panda CMS] Found ${richTextElements.length} rich text elements`)
197
+
198
+ if (richTextElements.length > 0) {
199
+ // Verify Editor.js is available in the iframe context
200
+ if (!this.frameDocument.defaultView.EditorJS) {
201
+ const error = new Error("Editor.js not loaded in iframe context")
202
+ console.error("[Panda CMS]", error)
203
+ throw error // This will bubble up and fail the test
204
+ }
205
+
206
+ const initializer = new EditorJSInitializer(this.frameDocument, true)
207
+
208
+ // Don't wrap in try/catch to let errors bubble up
209
+ const editors = await Promise.all(
210
+ Array.from(richTextElements).map(async element => {
211
+ // Create holder element before initialization
212
+ const holderId = `editor-${Math.random().toString(36).substr(2, 9)}`
213
+ const holderElement = this.frameDocument.createElement('div')
214
+ holderElement.id = holderId
215
+ holderElement.className = 'editor-js-holder codex-editor'
216
+ element.appendChild(holderElement)
217
+
218
+ // Wait for the holder element to be in the DOM
219
+ await new Promise(resolve => setTimeout(resolve, 0))
220
+
221
+ // Verify the holder element exists
222
+ const verifyHolder = this.frameDocument.getElementById(holderId)
223
+ if (!verifyHolder) {
224
+ const error = new Error(`Failed to create editor holder element ${holderId}`)
225
+ console.error("[Panda CMS]", error)
226
+ throw error // This will bubble up and fail the test
227
+ }
228
+
229
+ console.debug(`[Panda CMS] Created editor holder: ${holderId}`, {
230
+ exists: !!verifyHolder,
231
+ parent: element.id || 'no-id',
232
+ editorJSAvailable: !!this.frameDocument.defaultView.EditorJS
233
+ })
234
+
235
+ // Initialize editor with empty data
236
+ const editor = await initializer.initialize(holderElement, {}, holderId)
237
+
238
+ // Set up save handler for this editor
239
+ const saveButton = parent.document.getElementById('saveEditableButton')
240
+ if (saveButton) {
241
+ saveButton.addEventListener('click', async () => {
242
+ const outputData = await editor.save()
243
+ outputData.source = "editorJS"
244
+
245
+ const pageId = element.getAttribute("data-editable-page-id")
246
+ const blockContentId = element.getAttribute("data-editable-block-content-id")
247
+
248
+ const response = await fetch(`${this.adminPathValue}/pages/${pageId}/block_contents/${blockContentId}`, {
249
+ method: "PATCH",
250
+ headers: {
251
+ "Content-Type": "application/json",
252
+ "X-CSRF-Token": this.csrfToken
253
+ },
254
+ body: JSON.stringify({ content: outputData })
255
+ })
256
+
257
+ if (!response.ok) {
258
+ const error = new Error('Save failed')
259
+ console.error("[Panda CMS]", error)
260
+ throw error
261
+ }
262
+
263
+ this.handleSuccess()
264
+ })
265
+ } else {
266
+ console.warn("[Panda CMS] Save button not found")
267
+ }
268
+
269
+ return editor
270
+ })
271
+ )
272
+
273
+ // Filter out any null editors and add the valid ones
274
+ const validEditors = editors.filter(editor => editor !== null)
275
+ this.editors.push(...validEditors)
276
+
277
+ // If we didn't get any valid editors, that's an error
278
+ if (validEditors.length === 0) {
279
+ const error = new Error("No editors were successfully initialized")
280
+ console.error("[Panda CMS]", error)
281
+ throw error
282
+ }
283
+ }
284
+
285
+ this.editorsInitialized.rich = true
286
+ this.checkAllEditorsInitialized()
287
+ }
288
+
289
+ checkAllEditorsInitialized() {
290
+ console.log("[Panda CMS] Editor initialization status:", this.editorsInitialized)
291
+
292
+ // Always ensure frame is visible
293
+ this.ensureFrameVisibility()
294
+ }
295
+
296
+ handleError(error) {
297
+ const errorMessage = parent.document.getElementById("errorMessage")
298
+ if (errorMessage) {
299
+ errorMessage.getElementsByClassName('flash-message-text')[0].textContent = error
300
+ errorMessage.classList.remove("hidden")
301
+ setTimeout(() => {
302
+ errorMessage.classList.add("hidden")
303
+ }, 3000)
304
+ }
305
+ console.error("[Panda CMS] Error:", error)
306
+
307
+ // Throw the error to fail the test
308
+ throw error
309
+ }
310
+
311
+ handleSuccess() {
312
+ const successMessage = parent.document.getElementById("successMessage")
313
+ if (successMessage) {
314
+ successMessage.classList.remove("hidden")
315
+ setTimeout(() => {
316
+ successMessage.classList.add("hidden")
317
+ }, 3000)
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,48 @@
1
+ console.debug("[Panda CMS] Importing Panda CMS Stimulus Controller...")
2
+
3
+ import { Application as PandaCMSApplication } from "@hotwired/stimulus"
4
+
5
+ const pandaCmsApplication = PandaCMSApplication.start()
6
+
7
+ console.debug("[Panda CMS] Application started...")
8
+
9
+ // Configure Stimulus development experience
10
+ pandaCmsApplication.debug = false
11
+ window.pandaCmsStimulus = pandaCmsApplication
12
+
13
+ console.debug("[Panda CMS] window.pandaCmsStimulus available...")
14
+
15
+ console.debug("[Panda CMS] Registering controllers...")
16
+
17
+ // Use the same paths as defined in _importmap.html.erb
18
+ import DashboardController from "panda/cms/controllers/dashboard_controller"
19
+ pandaCmsApplication.register("dashboard", DashboardController)
20
+
21
+ import EditorFormController from "panda/cms/controllers/editor_form_controller"
22
+ pandaCmsApplication.register("editor-form", EditorFormController)
23
+
24
+ import SlugController from "panda/cms/controllers/slug_controller"
25
+ pandaCmsApplication.register("slug", SlugController)
26
+
27
+ import EditorIframeController from "panda/cms/controllers/editor_iframe_controller"
28
+ pandaCmsApplication.register("editor-iframe", EditorIframeController)
29
+
30
+ console.debug("[Panda CMS] Registering components...")
31
+
32
+ // Import and register all TailwindCSS Components or just the ones you need
33
+ import { Alert, Autosave, ColorPreview, Dropdown, Modal, Tabs, Popover, Toggle, Slideover } from "tailwindcss-stimulus-components"
34
+ pandaCmsApplication.register('alert', Alert)
35
+ pandaCmsApplication.register('autosave', Autosave)
36
+ pandaCmsApplication.register('color-preview', ColorPreview)
37
+ pandaCmsApplication.register('dropdown', Dropdown)
38
+ pandaCmsApplication.register('modal', Modal)
39
+ pandaCmsApplication.register('popover', Popover)
40
+ pandaCmsApplication.register('slideover', Slideover)
41
+ pandaCmsApplication.register('tabs', Tabs)
42
+ pandaCmsApplication.register('toggle', Toggle)
43
+
44
+ console.debug("[Panda CMS] Components registered...")
45
+
46
+ export { pandaCmsApplication }
47
+
48
+ console.debug("[Panda CMS] Application exported...")
@@ -0,0 +1,87 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static targets = [
5
+ "existing_root",
6
+ "input_select",
7
+ "input_text",
8
+ "output_text",
9
+ ];
10
+
11
+ connect() {
12
+ console.debug("[Panda CMS] Slug handler connected...");
13
+ // Generate path on initial load if title exists
14
+ if (this.input_textTarget.value) {
15
+ this.generatePath();
16
+ }
17
+ }
18
+
19
+ generatePath() {
20
+ try {
21
+ const slug = this.createSlug(this.input_textTarget.value);
22
+ // For posts, we want to store just the slug part
23
+ const prefix = this.output_textTarget.dataset.prefix || "";
24
+ this.output_textTarget.value = "/" + slug;
25
+
26
+ // If there's a prefix, show it in the UI but don't include it in the value
27
+ if (prefix) {
28
+ const prefixSpan = this.output_textTarget.previousElementSibling ||
29
+ (() => {
30
+ const span = document.createElement('span');
31
+ span.className = 'prefix';
32
+ this.output_textTarget.parentNode.insertBefore(span, this.output_textTarget);
33
+ return span;
34
+ })();
35
+ prefixSpan.textContent = prefix;
36
+ }
37
+
38
+ console.log("Have set the path to: " + this.output_textTarget.value);
39
+ } catch (error) {
40
+ console.error("Error generating path:", error);
41
+ // Add error class to path field
42
+ this.output_textTarget.classList.add("error");
43
+ }
44
+ }
45
+
46
+ setPrePath() {
47
+ try {
48
+ const match = this.input_selectTarget.options[this.input_selectTarget.selectedIndex].text.match(/.*\((.*)\)$/);
49
+ if (match) {
50
+ this.parent_slugs = match[1];
51
+ const prePath = (this.existing_rootTarget.value + this.parent_slugs).replace(/\/$/, "");
52
+ const prefixSpan = this.output_textTarget.previousElementSibling;
53
+ if (prefixSpan) {
54
+ prefixSpan.textContent = prePath;
55
+ }
56
+ console.log("Have set the pre-path to: " + prePath);
57
+ }
58
+ } catch (error) {
59
+ console.error("Error setting pre-path:", error);
60
+ }
61
+ }
62
+
63
+ // TODO: Invoke a library or helper which can be shared with the backend
64
+ // and check for uniqueness at the same time
65
+ createSlug(input) {
66
+ if (!input) return "";
67
+
68
+ var str = input
69
+ .toLowerCase()
70
+ .trim()
71
+ .replace(/[^\w\s-]/g, "-")
72
+ .replace(/&/g, "and")
73
+ .replace(/[\s_-]+/g, "-")
74
+ .trim();
75
+
76
+ return this.trimStartEnd(str, "-");
77
+ }
78
+
79
+ trimStartEnd(str, ch) {
80
+ var start = 0;
81
+ var end = str.length;
82
+
83
+ while (start < end && str[start] === ch) ++start;
84
+ while (end > start && str[end - 1] === ch) --end;
85
+ return start > 0 || end < str.length ? str.substring(start, end) : str;
86
+ }
87
+ }
@@ -0,0 +1,80 @@
1
+ export class CSSExtractor {
2
+ /**
3
+ * Extracts CSS rules from within a specific selector and transforms them for EditorJS
4
+ * @param {string} css - The CSS content to parse
5
+ * @returns {string} The extracted and transformed CSS rules
6
+ */
7
+ static extractStyles(css) {
8
+ const rules = []
9
+ let inComponents = false
10
+ let inContentRule = false
11
+ let braceCount = 0
12
+ let currentRule = ''
13
+
14
+ // Split CSS into lines and process each line
15
+ const lines = css.split('\n')
16
+
17
+ for (const line of lines) {
18
+ const trimmedLine = line.trim()
19
+
20
+ // Check if we're entering components layer
21
+ if (trimmedLine === '@layer components {') {
22
+ inComponents = true
23
+ continue
24
+ }
25
+
26
+ // Only process lines within components layer
27
+ if (!inComponents) continue
28
+
29
+ // If we find the .content selector
30
+ if (!inContentRule && trimmedLine.startsWith('.content')) {
31
+ inContentRule = true
32
+ braceCount++
33
+ // Transform the selector for EditorJS
34
+ currentRule = '.codex-editor__redactor .ce-block .ce-block__content'
35
+ if (trimmedLine.includes('{')) {
36
+ currentRule += ' {'
37
+ }
38
+ continue
39
+ }
40
+
41
+ // If we're inside a content rule
42
+ if (inContentRule) {
43
+ // Transform selectors for EditorJS
44
+ let transformedLine = line
45
+ .replace(/\.content\s+/g, '.codex-editor__redactor .ce-block .ce-block__content ')
46
+ .replace(/\bh1\b(?![-_])/g, 'h1.ce-header')
47
+ .replace(/\bh2\b(?![-_])/g, 'h2.ce-header')
48
+ .replace(/\bh3\b(?![-_])/g, 'h3.ce-header')
49
+ .replace(/\bul\b(?![-_])/g, 'ul.cdx-list')
50
+ .replace(/\bol\b(?![-_])/g, 'ol.cdx-list')
51
+ .replace(/\bli\b(?![-_])/g, 'li.cdx-list__item')
52
+ .replace(/\bblockquote\b(?![-_])/g, '.cdx-quote')
53
+
54
+ currentRule += '\n' + transformedLine
55
+
56
+ // Count braces to handle nested rules
57
+ braceCount += (trimmedLine.match(/{/g) || []).length
58
+ braceCount -= (trimmedLine.match(/}/g) || []).length
59
+
60
+ // If braces are balanced, we've found the end of the rule
61
+ if (braceCount === 0) {
62
+ rules.push(currentRule)
63
+ inContentRule = false
64
+ currentRule = ''
65
+ }
66
+ }
67
+ }
68
+
69
+ return rules.join('\n\n')
70
+ }
71
+
72
+ /**
73
+ * Gets all styles from a stylesheet that apply to the editor
74
+ * @param {string} css - The CSS content to parse
75
+ * @returns {string} The extracted CSS rules
76
+ */
77
+ static getEditorStyles(css) {
78
+ return this.extractStyles(css)
79
+ }
80
+ }