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,177 @@
1
+ export const EDITOR_JS_RESOURCES = [
2
+ "https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2",
3
+ "https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3",
4
+ "https://cdn.jsdelivr.net/npm/@editorjs/header@2.8.1",
5
+ "https://cdn.jsdelivr.net/npm/@editorjs/nested-list@1.4.2",
6
+ "https://cdn.jsdelivr.net/npm/@editorjs/quote@2.6.0",
7
+ "https://cdn.jsdelivr.net/npm/@editorjs/simple-image@1.6.0",
8
+ "https://cdn.jsdelivr.net/npm/@editorjs/table@2.3.0",
9
+ "https://cdn.jsdelivr.net/npm/@editorjs/embed@2.7.0"
10
+ ]
11
+
12
+ // Allow applications to add their own resources
13
+ if (window.PANDA_CMS_EDITOR_JS_RESOURCES) {
14
+ EDITOR_JS_RESOURCES.push(...window.PANDA_CMS_EDITOR_JS_RESOURCES)
15
+ }
16
+
17
+ export const EDITOR_JS_CSS = `
18
+ /* Editor layout styles */
19
+ .ce-toolbar__content {
20
+ margin: 0 !important;
21
+ margin-left: 40px;
22
+ max-width: 100% !important;
23
+ width: 100% !important;
24
+ }
25
+
26
+ .ce-block__content {
27
+ max-width: 100%;
28
+ margin: 0 !important;
29
+ margin-left: 10px !important;
30
+ }
31
+
32
+ /* Ensure proper nesting for content styles to apply */
33
+ .codex-editor .codex-editor__redactor {
34
+ position: relative;
35
+ }
36
+
37
+ .codex-editor .codex-editor__redactor .ce-block {
38
+ position: relative;
39
+ }
40
+
41
+ .codex-editor .codex-editor__redactor .ce-block .ce-block__content {
42
+ position: relative;
43
+ }
44
+
45
+ /* Remove default editor styles that might interfere */
46
+ .ce-header {
47
+ padding: 0 !important;
48
+ margin: 0 !important;
49
+ background: none !important;
50
+ border: none !important;
51
+ }
52
+
53
+ .ce-paragraph {
54
+ padding: 0 !important;
55
+ margin: 0 !important;
56
+ line-height: inherit !important;
57
+ }
58
+
59
+ /* Lists */
60
+ .ce-block--list ul,
61
+ .ce-block--list ol {
62
+ margin: 0;
63
+ padding-left: inherit;
64
+ }
65
+
66
+ .ce-block--list li {
67
+ margin: 0;
68
+ padding-left: inherit;
69
+ }
70
+
71
+ /* Ensure editor toolbar is above content */
72
+ .ce-toolbar {
73
+ z-index: 100;
74
+ }
75
+
76
+ /* Style the block selection */
77
+ .ce-block--selected {
78
+ background-color: rgba(16, 64, 113, 0.05);
79
+ border-radius: 4px;
80
+ }`
81
+
82
+ export const getEditorConfig = (elementId, previousData, doc = document) => {
83
+ // Validate holder element exists
84
+ const holder = doc.getElementById(elementId)
85
+ if (!holder) {
86
+ throw new Error(`Editor holder element ${elementId} not found`)
87
+ }
88
+
89
+ const config = {
90
+ holder: elementId,
91
+ data: previousData || {},
92
+ placeholder: 'Click the + button to add content...',
93
+ inlineToolbar: true,
94
+ tools: {
95
+ header: {
96
+ class: window.Header,
97
+ inlineToolbar: true,
98
+ config: {
99
+ placeholder: 'Enter a header',
100
+ levels: [1, 2, 3, 4, 5, 6],
101
+ defaultLevel: 2
102
+ }
103
+ },
104
+ paragraph: {
105
+ class: window.Paragraph,
106
+ inlineToolbar: true,
107
+ config: {
108
+ placeholder: 'Start writing or press Tab to add content...'
109
+ }
110
+ },
111
+ list: {
112
+ class: window.NestedList,
113
+ inlineToolbar: true,
114
+ config: {
115
+ defaultStyle: 'unordered'
116
+ }
117
+ },
118
+ quote: {
119
+ class: window.Quote,
120
+ inlineToolbar: true,
121
+ config: {
122
+ quotePlaceholder: 'Enter a quote',
123
+ captionPlaceholder: 'Quote\'s author'
124
+ }
125
+ },
126
+ table: {
127
+ class: window.Table,
128
+ inlineToolbar: true,
129
+ config: {
130
+ rows: 2,
131
+ cols: 2
132
+ }
133
+ },
134
+ image: {
135
+ class: window.SimpleImage,
136
+ inlineToolbar: true,
137
+ config: {
138
+ placeholder: 'Paste an image URL...'
139
+ }
140
+ },
141
+ embed: {
142
+ class: window.Embed,
143
+ inlineToolbar: true,
144
+ config: {
145
+ services: {
146
+ youtube: true,
147
+ vimeo: true
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // Remove any undefined tools from the config
155
+ config.tools = Object.fromEntries(
156
+ Object.entries(config.tools)
157
+ .filter(([_, value]) => value?.class !== undefined)
158
+ .map(([name, tool]) => {
159
+ if (!tool.class) {
160
+ throw new Error(`Tool ${name} has no class defined`)
161
+ }
162
+ return [name, tool]
163
+ })
164
+ )
165
+
166
+ // Allow applications to customize the config through Ruby
167
+ if (window.PANDA_CMS_EDITOR_JS_CONFIG) {
168
+ Object.assign(config.tools, window.PANDA_CMS_EDITOR_JS_CONFIG)
169
+ }
170
+
171
+ // Allow applications to customize the config through JavaScript
172
+ if (typeof window.customizeEditorJS === 'function') {
173
+ window.customizeEditorJS(config)
174
+ }
175
+
176
+ return config
177
+ }
@@ -0,0 +1,285 @@
1
+ import { ResourceLoader } from "panda/cms/editor/resource_loader"
2
+ import { EDITOR_JS_RESOURCES, EDITOR_JS_CSS, getEditorConfig } from "panda/cms/editor/editor_js_config"
3
+ import { CSSExtractor } from "panda/cms/editor/css_extractor"
4
+
5
+ export class EditorJSInitializer {
6
+ constructor(document, withinIFrame = false) {
7
+ this.document = document
8
+ this.withinIFrame = withinIFrame
9
+ }
10
+
11
+ /**
12
+ * Initializes the EditorJS instance for a given element.
13
+ * This method loads necessary resources and returns the JavaScript code for initializing the editor.
14
+ *
15
+ * @param {HTMLElement} element - The DOM element to initialize the editor on
16
+ * @param {Object} initialData - The initial data for the editor
17
+ * @param {string} editorId - The ID to use for the editor holder
18
+ * @returns {Promise<EditorJS>} A promise that resolves to the editor instance
19
+ */
20
+ async initialize(element, initialData = {}, editorId = null) {
21
+ await this.loadResources()
22
+ const result = await this.initializeEditor(element, initialData, editorId)
23
+ return result
24
+ }
25
+
26
+ /**
27
+ * Gets the application's styles from its configured stylesheet
28
+ * @returns {Promise<string>} The extracted CSS rules
29
+ */
30
+ async getApplicationStyles() {
31
+ try {
32
+ // Get the configured stylesheet URL, defaulting to Tailwind Rails default
33
+ const stylesheetUrl = window.PANDA_CMS_CONFIG?.stylesheetUrl || '/assets/application.tailwind.css'
34
+
35
+ // Fetch the CSS content
36
+ const response = await fetch(stylesheetUrl)
37
+ const css = await response.text()
38
+ return CSSExtractor.getEditorStyles(css)
39
+ } catch (error) {
40
+ return ''
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Loads the necessary resources for the EditorJS instance.
46
+ * This method fetches the required scripts and stylesheets and embeds them into the document.
47
+ */
48
+ async loadResources() {
49
+ try {
50
+ // First load EditorJS core
51
+ const editorCore = EDITOR_JS_RESOURCES[0]
52
+ await ResourceLoader.loadScript(this.document, this.document.head, editorCore)
53
+
54
+ // Then load all tools in parallel
55
+ const toolLoads = EDITOR_JS_RESOURCES.slice(1).map(async (resource) => {
56
+ await ResourceLoader.loadScript(this.document, this.document.head, resource)
57
+ })
58
+
59
+ // Load CSS directly
60
+ await ResourceLoader.embedCSS(this.document, this.document.head, EDITOR_JS_CSS)
61
+
62
+ // Wait for all resources to load
63
+ await Promise.all(toolLoads)
64
+
65
+ // Wait for EditorJS to be available
66
+ await this.waitForEditorJS()
67
+ } catch (error) {
68
+ throw error
69
+ }
70
+ }
71
+
72
+ async initializeEditor(element, initialData = {}, editorId = null) {
73
+ // Generate a consistent holder ID if not provided
74
+ const holderId = editorId || `editor-${element.id || Math.random().toString(36).substr(2, 9)}`
75
+
76
+ // Create or find the holder element in the correct document context
77
+ let holderElement = this.document.getElementById(holderId)
78
+ if (!holderElement) {
79
+ // Create the holder element in the correct document context
80
+ holderElement = this.document.createElement('div')
81
+ holderElement.id = holderId
82
+ holderElement.className = 'editor-js-holder codex-editor'
83
+
84
+ // Append to the element and force a reflow
85
+ element.appendChild(holderElement)
86
+ void holderElement.offsetHeight // Force a reflow
87
+ }
88
+
89
+ // Verify the holder element exists in the correct document context
90
+ const verifyHolder = this.document.getElementById(holderId)
91
+ if (!verifyHolder) {
92
+ throw new Error(`Failed to create editor holder element ${holderId}`)
93
+ }
94
+
95
+ // Clear any existing content in the holder
96
+ holderElement.innerHTML = ''
97
+
98
+ // Add source to initial data
99
+ if (initialData && !initialData.source) {
100
+ initialData.source = "editorJS"
101
+ }
102
+
103
+ // Get the base config but pass our document context
104
+ const config = getEditorConfig(holderId, initialData, this.document)
105
+
106
+ // Override specific settings for iframe context
107
+ const editorConfig = {
108
+ ...config,
109
+ holder: holderElement, // Use element reference instead of ID
110
+ minHeight: 1, // Prevent auto-height issues in iframe
111
+ autofocus: false, // Prevent focus issues
112
+ logLevel: 'ERROR', // Only show errors
113
+ tools: {
114
+ ...config.tools,
115
+ // Ensure tools use the correct window context
116
+ paragraph: { ...config.tools.paragraph, class: this.document.defaultView.Paragraph },
117
+ header: { ...config.tools.header, class: this.document.defaultView.Header },
118
+ list: { ...config.tools.list, class: this.document.defaultView.NestedList },
119
+ quote: { ...config.tools.quote, class: this.document.defaultView.Quote },
120
+ table: { ...config.tools.table, class: this.document.defaultView.Table },
121
+ image: { ...config.tools.image, class: this.document.defaultView.SimpleImage },
122
+ embed: { ...config.tools.embed, class: this.document.defaultView.Embed }
123
+ }
124
+ }
125
+
126
+ // Create editor instance directly
127
+ const editor = new this.document.defaultView.EditorJS({
128
+ ...editorConfig,
129
+ onReady: () => {
130
+ // Store the editor instance globally for testing
131
+ if (this.withinIFrame) {
132
+ this.document.defaultView.editor = editor
133
+ } else {
134
+ window.editor = editor
135
+ }
136
+
137
+ // Mark editor as ready
138
+ editor.isReady = true
139
+
140
+ // Force redraw of toolbar and blocks
141
+ setTimeout(async () => {
142
+ try {
143
+ const toolbar = holderElement.querySelector('.ce-toolbar')
144
+ const blockWrapper = holderElement.querySelector('.ce-block')
145
+
146
+ if (!toolbar || !blockWrapper) {
147
+ // Clear and insert a new block to force UI update
148
+ await editor.blocks.clear()
149
+ await editor.blocks.insert('paragraph')
150
+
151
+ // Force a redraw by toggling display
152
+ holderElement.style.display = 'none'
153
+ void holderElement.offsetHeight
154
+ holderElement.style.display = ''
155
+ }
156
+
157
+ // Call the ready hook if it exists
158
+ if (typeof window.onEditorJSReady === 'function') {
159
+ window.onEditorJSReady(editor)
160
+ }
161
+ } catch (error) {
162
+ console.error('Error during editor redraw:', error)
163
+ }
164
+ }, 100)
165
+ },
166
+ onChange: async (api, event) => {
167
+ try {
168
+ // Save the current editor data
169
+ const outputData = await api.saver.save()
170
+ outputData.source = "editorJS"
171
+ const contentJson = JSON.stringify(outputData)
172
+
173
+ if (!this.withinIFrame) {
174
+ // For form-based editors, update the hidden input
175
+ const form = element.closest('[data-controller="editor-form"]')
176
+ if (form) {
177
+ const hiddenInput = form.querySelector('[data-editor-form-target="hiddenField"]')
178
+ if (hiddenInput) {
179
+ hiddenInput.value = contentJson
180
+ hiddenInput.dataset.initialContent = contentJson
181
+ hiddenInput.dispatchEvent(new Event('change', { bubbles: true }))
182
+ }
183
+ }
184
+ } else {
185
+ // For iframe-based editors, update the element's data attribute
186
+ element.setAttribute('data-content', contentJson)
187
+ element.dispatchEvent(new Event('change', { bubbles: true }))
188
+
189
+ // Get the save button from parent window
190
+ const saveButton = parent.document.getElementById('saveEditableButton')
191
+ if (saveButton) {
192
+ // Store the current content on the save button for later use
193
+ saveButton.dataset.pendingContent = contentJson
194
+
195
+ // Add click handler if not already added
196
+ if (!saveButton.hasAttribute('data-handler-attached')) {
197
+ saveButton.setAttribute('data-handler-attached', 'true')
198
+ saveButton.addEventListener('click', async () => {
199
+ try {
200
+ const pageId = element.getAttribute("data-editable-page-id")
201
+ const blockContentId = element.getAttribute("data-editable-block-content-id")
202
+ const pendingContent = JSON.parse(saveButton.dataset.pendingContent || '{}')
203
+
204
+ const response = await fetch(`${this.adminPathValue}/pages/${pageId}/block_contents/${blockContentId}`, {
205
+ method: "PATCH",
206
+ headers: {
207
+ "Content-Type": "application/json",
208
+ "X-CSRF-Token": this.csrfToken
209
+ },
210
+ body: JSON.stringify({ content: pendingContent })
211
+ })
212
+
213
+ if (!response.ok) {
214
+ throw new Error('Save failed')
215
+ }
216
+
217
+ // Clear pending content after successful save
218
+ delete saveButton.dataset.pendingContent
219
+ } catch (error) {
220
+ console.error('Error saving content:', error)
221
+ }
222
+ })
223
+ }
224
+ }
225
+ }
226
+ } catch (error) {
227
+ console.error('Error in onChange handler:', error)
228
+ }
229
+ }
230
+ })
231
+
232
+ // Store editor instance on the holder element to maintain reference
233
+ holderElement.editorInstance = editor
234
+
235
+ if (!this.withinIFrame) {
236
+ // Store the editor instance on the controller element for potential future reference
237
+ const form = element.closest('[data-controller="editor-form"]')
238
+ if (form) {
239
+ form.editorInstance = editor
240
+ }
241
+ } else {
242
+ // For iframe editors, store the instance on the element itself
243
+ element.editorInstance = editor
244
+ }
245
+
246
+ // Return a promise that resolves when the editor is ready
247
+ return new Promise((resolve, reject) => {
248
+ const timeout = setTimeout(() => {
249
+ reject(new Error('Editor initialization timed out'))
250
+ }, 30000)
251
+
252
+ const checkReady = () => {
253
+ if (editor.isReady) {
254
+ clearTimeout(timeout)
255
+ resolve(editor)
256
+ } else {
257
+ setTimeout(checkReady, 100)
258
+ }
259
+ }
260
+ checkReady()
261
+ })
262
+ }
263
+
264
+ /**
265
+ * Wait for EditorJS core to be available in window
266
+ */
267
+ async waitForEditorJS() {
268
+ let attempts = 0
269
+ const maxAttempts = 30 // 3 seconds with 100ms intervals
270
+
271
+ await new Promise((resolve, reject) => {
272
+ const check = () => {
273
+ attempts++
274
+ if (window.EditorJS) {
275
+ resolve()
276
+ } else if (attempts >= maxAttempts) {
277
+ reject(new Error('EditorJS core failed to load'))
278
+ } else {
279
+ setTimeout(check, 100)
280
+ }
281
+ }
282
+ check()
283
+ })
284
+ }
285
+ }
@@ -0,0 +1,110 @@
1
+ export class PlainTextEditor {
2
+ /**
3
+ * Constructs a new PlainTextEditor instance.
4
+ *
5
+ * @param {HTMLElement} element - The HTML element representing the plain text editor.
6
+ * @param {HTMLIFrameElement} frame - The HTML iframe element containing the plain text editor.
7
+ * @param {Object} options - An object containing configuration options for the plain text editor.
8
+ */
9
+ constructor(element, frame, options) {
10
+ this.element = element
11
+ this.frame = frame
12
+ this.options = options
13
+ this.setupStyles()
14
+ this.bindEvents()
15
+ }
16
+
17
+ /**
18
+ * Sets up the styles for the plain text editor element.
19
+ *
20
+ * This method applies various CSS styles to the editor element, such as a dashed border, no outline, a pointer cursor, and a background color transition. It also sets the white-space and font-family styles based on the data-editable-kind attribute of the element.
21
+ */
22
+ setupStyles() {
23
+ this.element.style.border = "1px dashed #ccc"
24
+ this.element.style.outline = "none"
25
+ this.element.style.cursor = "pointer"
26
+ this.element.style.transition = "background 500ms linear"
27
+ this.element.style.backgroundColor = "inherit"
28
+
29
+ if (this.element.getAttribute("data-editable-kind") == "html") {
30
+ this.element.style.whiteSpace = "pre-wrap"
31
+ this.element.style.fontFamily = "monospace"
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Binds event listeners for the plain text editor.
37
+ *
38
+ * If the `autosave` option is enabled, this method adds a `blur` event listener to the editor element, which triggers the `save()` method when the editor loses focus.
39
+ *
40
+ * Additionally, this method adds a `click` event listener to the "Save Editable" button, which also triggers the `save()` method when clicked.
41
+ */
42
+ bindEvents() {
43
+ if (this.options.autosave) {
44
+ this.element.addEventListener("blur", () => this.save())
45
+ }
46
+
47
+ document.getElementById('saveEditableButton').addEventListener('click', () => this.save())
48
+ }
49
+
50
+ /**
51
+ * Saves the content of the plain text editor to the server.
52
+ *
53
+ * This method sends a PATCH request to the server with the updated content of the plain text editor. It retrieves the necessary data from the editor element's attributes, such as the block content ID and the content type (HTML or plain text). If the save is successful, it calls the `showSuccess()` method, otherwise it calls the `showError()` method with the error.
54
+ */
55
+ save() {
56
+ const blockContentId = this.element.getAttribute("data-editable-block-content-id")
57
+ const pageId = this.element.getAttribute("data-editable-page-id")
58
+ const content = this.element.getAttribute("data-editable-kind") == "html" ?
59
+ this.element.innerHTML :
60
+ this.element.innerText
61
+
62
+ fetch(`${this.options.adminPath}/pages/${pageId}/block_contents/${blockContentId}`, {
63
+ method: "PATCH",
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ "X-CSRF-Token": this.options.csrfToken
67
+ },
68
+ body: JSON.stringify({ content: content })
69
+ })
70
+ .then(response => response.json())
71
+ .then(() => {
72
+ // Show success message in parent window
73
+ parent.document.getElementById("successMessage").classList.remove("hidden")
74
+ setTimeout(() => {
75
+ parent.document.getElementById("successMessage").classList.add("hidden")
76
+ }, 3000)
77
+ // Show visual feedback in the editor
78
+ this.showSuccess()
79
+ })
80
+ .catch(error => this.showError(error))
81
+ }
82
+
83
+ /**
84
+ * Displays a success message by temporarily changing the background color of the editor element.
85
+ *
86
+ * This method is called after a successful save operation to provide visual feedback to the user.
87
+ */
88
+ showSuccess() {
89
+ this.element.style.backgroundColor = "#66bd6a50"
90
+ setTimeout(() => {
91
+ this.element.style.backgroundColor = "inherit"
92
+ }, 1000)
93
+ }
94
+
95
+ /**
96
+ * Displays an error message by temporarily changing the background color of the editor element and logging the error to the console.
97
+ *
98
+ * This method is called after a failed save operation to provide visual and textual feedback to the user.
99
+ *
100
+ * @param {Error} error - The error object that occurred during the save operation.
101
+ */
102
+ showError(error) {
103
+ this.element.style.backgroundColor = "#dc354550"
104
+ setTimeout(() => {
105
+ this.element.style.backgroundColor = "inherit"
106
+ }, 1000)
107
+ console.log(error)
108
+ alert("Error:", error)
109
+ }
110
+ }
@@ -0,0 +1,115 @@
1
+ export class ResourceLoader {
2
+ static loadedResources = new Set()
3
+
4
+ /**
5
+ * Embeds CSS styles into the document head.
6
+ *
7
+ * @param {Document} frameDocument - The document object to create elements in
8
+ * @param {HTMLElement} head - The head element to append styles to
9
+ * @param {string} css - The CSS styles to embed
10
+ * @returns {Promise} A promise that resolves when the styles are embedded
11
+ */
12
+ static embedCSS(frameDocument, head, css) {
13
+ const cssHash = this.hashString(css)
14
+ if (this.loadedResources.has(`css:${cssHash}`)) {
15
+ console.debug("[Panda CMS] CSS already embedded, skipping")
16
+ return Promise.resolve()
17
+ }
18
+
19
+ return new Promise((resolve) => {
20
+ const style = frameDocument.createElement("style")
21
+ style.textContent = css
22
+ head.append(style)
23
+ this.loadedResources.add(`css:${cssHash}`)
24
+ resolve(style)
25
+ console.debug("[Panda CMS] Embedded CSS styles")
26
+ })
27
+ }
28
+
29
+ /**
30
+ * Loads a script from a URL and appends it to the document head.
31
+ *
32
+ * @param {Document} frameDocument - The document object to create elements in
33
+ * @param {HTMLElement} head - The head element to append the script to
34
+ * @param {string} src - The URL of the script to load
35
+ * @returns {Promise} A promise that resolves when the script is loaded
36
+ */
37
+ static loadScript(frameDocument, head, src) {
38
+ if (this.loadedResources.has(`script:${src}`)) {
39
+ console.debug(`[Panda CMS] Script already loaded: ${src}, skipping`)
40
+ return Promise.resolve()
41
+ }
42
+
43
+ return new Promise((resolve, reject) => {
44
+ const script = frameDocument.createElement("script")
45
+ script.src = src
46
+ script.onload = () => {
47
+ this.loadedResources.add(`script:${src}`)
48
+ resolve(script)
49
+ console.debug(`[Panda CMS] Script loaded: ${src}`)
50
+ }
51
+ script.onerror = () => reject(new Error(`[Panda CMS] Script load error for ${src}`))
52
+ head.append(script)
53
+ })
54
+ }
55
+
56
+ static importScript(frameDocument, head, module, src) {
57
+ const key = `module:${module}:${src}`
58
+ if (this.loadedResources.has(key)) {
59
+ console.debug(`[Panda CMS] Module already imported: ${src}, skipping`)
60
+ return Promise.resolve()
61
+ }
62
+
63
+ return new Promise((resolve, reject) => {
64
+ const script = frameDocument.createElement("script")
65
+ script.type = "module"
66
+ script.textContent = `import ${module} from "${src}"`
67
+ head.append(script)
68
+
69
+ script.onload = () => {
70
+ this.loadedResources.add(key)
71
+ console.debug(`[Panda CMS] Module script loaded: ${src}`)
72
+ resolve(script)
73
+ }
74
+ script.onerror = () => reject(new Error(`[Panda CMS] Module script load error for ${src}`))
75
+ })
76
+ }
77
+
78
+ static loadStylesheet(frameDocument, head, href) {
79
+ if (this.loadedResources.has(`stylesheet:${href}`)) {
80
+ console.debug(`[Panda CMS] Stylesheet already loaded: ${href}, skipping`)
81
+ return Promise.resolve()
82
+ }
83
+
84
+ return new Promise((resolve, reject) => {
85
+ const link = frameDocument.createElement("link")
86
+ link.rel = "stylesheet"
87
+ link.href = href
88
+ link.media = "none"
89
+ head.append(link)
90
+
91
+ link.onload = () => {
92
+ if (link.media != "all") {
93
+ link.media = "all"
94
+ }
95
+ this.loadedResources.add(`stylesheet:${href}`)
96
+ console.debug(`[Panda CMS] Stylesheet loaded: ${href}`)
97
+ resolve(link)
98
+ }
99
+ link.onerror = () => reject(new Error(`[Panda CMS] Stylesheet load error for ${href}`))
100
+ })
101
+ }
102
+
103
+ /**
104
+ * Simple string hashing function for tracking embedded CSS
105
+ */
106
+ static hashString(str) {
107
+ let hash = 0
108
+ for (let i = 0; i < str.length; i++) {
109
+ const char = str.charCodeAt(i)
110
+ hash = ((hash << 5) - hash) + char
111
+ hash = hash & hash // Convert to 32bit integer
112
+ }
113
+ return hash.toString(36)
114
+ }
115
+ }