panda-cms 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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
+ }