panda_cms 0.5.10 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/Rakefile +0 -1
  4. data/app/assets/builds/panda_cms.css +2432 -1
  5. data/app/assets/config/panda_cms_manifest.js +3 -0
  6. data/app/assets/stylesheets/panda_cms/application.tailwind.css +3 -27
  7. data/app/builders/panda_cms/form_builder.rb +1 -1
  8. data/app/components/panda_cms/admin/button_component.rb +6 -3
  9. data/app/components/panda_cms/admin/flash_message_component.rb +1 -1
  10. data/app/components/panda_cms/admin/tag_component.rb +1 -1
  11. data/app/components/panda_cms/code_component.rb +62 -0
  12. data/app/components/panda_cms/menu_component.html.erb +3 -0
  13. data/app/components/panda_cms/menu_component.rb +8 -1
  14. data/app/components/panda_cms/page_menu_component.html.erb +12 -8
  15. data/app/components/panda_cms/page_menu_component.rb +24 -13
  16. data/app/components/panda_cms/rich_text_component.html.erb +6 -38
  17. data/app/components/panda_cms/rich_text_component.rb +32 -7
  18. data/app/components/panda_cms/text_component.rb +25 -22
  19. data/app/controllers/panda_cms/admin/block_contents_controller.rb +1 -1
  20. data/app/controllers/panda_cms/admin/dashboard_controller.rb +14 -6
  21. data/app/controllers/panda_cms/admin/menus_controller.rb +1 -54
  22. data/app/controllers/panda_cms/admin/pages_controller.rb +2 -1
  23. data/app/controllers/panda_cms/admin/sessions_controller.rb +13 -6
  24. data/app/controllers/panda_cms/application_controller.rb +1 -1
  25. data/app/controllers/panda_cms/pages_controller.rb +1 -1
  26. data/app/controllers/panda_cms/posts_controller.rb +1 -1
  27. data/app/helpers/panda_cms/application_helper.rb +2 -2
  28. data/app/javascript/panda_cms/@editorjs--editorjs.js +2577 -0
  29. data/app/javascript/panda_cms/@hotwired--stimulus.js +4 -0
  30. data/app/javascript/panda_cms/@hotwired--turbo.js +160 -0
  31. data/app/javascript/panda_cms/@rails--actioncable--src.js +4 -0
  32. data/app/javascript/panda_cms/application_panda_cms.js +4 -0
  33. data/app/javascript/panda_cms/controllers/dashboard_controller.js +7 -0
  34. data/app/javascript/panda_cms/controllers/editor_controller.js +247 -0
  35. data/app/javascript/panda_cms/controllers/index.js +45 -0
  36. data/app/javascript/panda_cms/controllers/slug_controller.js +48 -0
  37. data/app/javascript/panda_cms/editor/plain_text_editor.js +102 -0
  38. data/app/javascript/panda_cms/editor/resource_loader.js +69 -0
  39. data/app/javascript/panda_cms/editor/rich_text_editor.js +89 -0
  40. data/app/javascript/panda_cms/tailwindcss-stimulus-components.js +4 -0
  41. data/app/lib/panda_cms/demo_site_generator.rb +1 -3
  42. data/app/lib/panda_cms/editor_js/blocks/alert.rb +32 -0
  43. data/app/lib/panda_cms/editor_js/blocks/base.rb +28 -0
  44. data/app/lib/panda_cms/editor_js/blocks/header.rb +13 -0
  45. data/app/lib/panda_cms/editor_js/blocks/image.rb +34 -0
  46. data/app/lib/panda_cms/editor_js/blocks/list.rb +30 -0
  47. data/app/lib/panda_cms/editor_js/blocks/paragraph.rb +13 -0
  48. data/app/lib/panda_cms/editor_js/blocks/quote.rb +27 -0
  49. data/app/lib/panda_cms/editor_js/blocks/table.rb +48 -0
  50. data/app/lib/panda_cms/editor_js/renderer.rb +120 -0
  51. data/app/lib/panda_cms/slug.rb +1 -1
  52. data/app/models/panda_cms/block.rb +2 -2
  53. data/app/models/panda_cms/block_content.rb +12 -2
  54. data/app/models/panda_cms/page.rb +9 -3
  55. data/app/models/panda_cms/post.rb +1 -1
  56. data/app/models/panda_cms/template.rb +4 -2
  57. data/app/models/panda_cms/user.rb +9 -1
  58. data/app/views/panda_cms/admin/dashboard/show.html.erb +11 -9
  59. data/app/views/panda_cms/admin/forms/new.html.erb +6 -7
  60. data/app/views/panda_cms/admin/menus/index.html.erb +0 -2
  61. data/app/views/panda_cms/admin/pages/edit.html.erb +22 -19
  62. data/app/views/panda_cms/admin/pages/new.html.erb +6 -7
  63. data/app/views/panda_cms/admin/posts/_form.html.erb +4 -4
  64. data/app/views/panda_cms/admin/sessions/new.html.erb +1 -2
  65. data/app/views/panda_cms/admin/shared/_sidebar.html.erb +12 -16
  66. data/app/views/panda_cms/shared/_header.html.erb +13 -14
  67. data/app/views/panda_cms/shared/_importmap.html.erb +32 -0
  68. data/config/importmap.rb +13 -10
  69. data/config/initializers/panda_cms.rb +57 -55
  70. data/config/routes.rb +9 -9
  71. data/config/tailwind.config.js +1 -0
  72. data/db/migrate/20240205223709_create_panda_cms_pages.rb +6 -4
  73. data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
  74. data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
  75. data/db/seeds.rb +1 -0
  76. data/lib/generators/panda_cms/install_generator.rb +3 -0
  77. data/lib/panda_cms/engine.rb +42 -29
  78. data/lib/panda_cms/version.rb +1 -1
  79. data/lib/panda_cms.rb +58 -10
  80. data/lib/tasks/panda_cms.rake +41 -57
  81. data/public/panda-cms-assets/rich_text_editor.css +568 -0
  82. metadata +228 -304
  83. data/app/javascript/base.js +0 -37
  84. data/app/javascript/controllers/menu_controller.js +0 -19
  85. data/app/javascript/controllers/text_controller.js +0 -78
  86. data/app/javascript/controllers/text_field_update_controller.js +0 -51
  87. data/app/javascript/vendor/stimulus-components-rails-nested-form.js +0 -2
  88. data/app/javascript/vendor/tailwindcss-stimulus-components.js +0 -2
  89. data/app/views/panda_cms/admin/menus/_form.html.erb +0 -21
  90. data/app/views/panda_cms/admin/menus/_menu_item_fields.html.erb +0 -7
  91. data/app/views/panda_cms/admin/menus/edit.html.erb +0 -58
  92. data/app/views/panda_cms/admin/menus/new.html.erb +0 -5
  93. data/db/migrate/20240804110225_add_status_to_panda_cms_pages.rb +0 -7
  94. data/public/panda-cms-assets/javascripts/base.js +0 -37
  95. data/public/panda-cms-assets/javascripts/controllers/menu_controller.js +0 -19
  96. data/public/panda-cms-assets/javascripts/controllers/text_field_update_controller.js +0 -23
  97. data/public/panda-cms-assets/javascripts/embed/editable.js +0 -358
  98. data/public/panda-cms-assets/javascripts/embed/rich_text.css +0 -1294
  99. data/public/panda-cms-assets/javascripts/vendor/stimulus-components-rails-nested-form.js +0 -2
  100. data/public/panda-cms-assets/javascripts/vendor/stimulus-loading.js +0 -113
  101. data/public/panda-cms-assets/javascripts/vendor/tailwindcss-stimulus-components.js +0 -2
@@ -0,0 +1,247 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { PlainTextEditor } from "panda_cms_editor/plain_text_editor";
3
+ import { ResourceLoader } from "panda_cms_editor/resource_loader";
4
+
5
+ export class EditorController extends Controller {
6
+ /**
7
+ * Defines the static values that can be set on the EditorController.
8
+ *
9
+ * @property {number} pageId - The ID of the page being edited.
10
+ * @property {string} adminPath - The path to the admin section of the application.
11
+ * @property {boolean} autosave - Whether the editor should automatically save changes.
12
+ */
13
+ static values = {
14
+ pageId: Number,
15
+ adminPath: String,
16
+ autosave: Boolean
17
+ }
18
+
19
+ /**
20
+ * Connects the EditorController to the DOM and initializes the editors.
21
+ * This method is called when the EditorController is connected to the DOM.
22
+ * It sets up the necessary properties and event listeners to manage the editors,
23
+ * and then calls the `initializeEditors()` method to start the initialization process.
24
+ */
25
+ connect() {
26
+ console.debug("[Panda CMS] Editor controller connected")
27
+ this.frame = this.element
28
+
29
+ // In CI, show what's going on, otherwise hide the frame
30
+ if (!window.location.href.includes("0.0.0.0:3001")) {
31
+ this.frame.style.display = "none"
32
+ }
33
+
34
+ if (document.querySelector('meta[name="csrf-token"]')) {
35
+ this.csrfToken = document.querySelector('meta[name="csrf-token"]').content
36
+ } else {
37
+ this.csrfToken = ""
38
+ }
39
+
40
+ this.editors = []
41
+ this.editorsInitialized = {
42
+ plain: false,
43
+ rich: false
44
+ }
45
+
46
+ this.frame.addEventListener("load", () => {
47
+ console.debug("[Panda CMS] Frame loaded")
48
+ this.frameDocument = this.frame.contentDocument || this.frame.contentWindow.document
49
+ this.body = this.frameDocument.body
50
+ this.head = this.frameDocument.head
51
+ this.initializeEditors()
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Initializes the plain text and rich text editors for the page.
57
+ * This method is responsible for finding all editable elements on the page and initializing the appropriate editor instances for them.
58
+ * It sets the editorsInitialized flags to true once the initialization is complete and calls the checkAllEditorsInitialized method.
59
+ */
60
+ initializeEditors() {
61
+ console.debug("[Panda CMS] Starting editor initialization")
62
+ this.initializePlainTextEditors()
63
+ this.initializeRichTextEditors()
64
+ }
65
+
66
+ /**
67
+ * Initializes the plain text editors for the page.
68
+ * This method is responsible for finding all elements on the page that have the "plain_text", "markdown", or "html" data-editable-kind attributes,
69
+ * and initializing a PlainTextEditor instance for each of them. It also sets the editorsInitialized.plain flag to true
70
+ * and calls the checkAllEditorsInitialized method to notify that the plain text editors have been initialized.
71
+ */
72
+ initializePlainTextEditors() {
73
+ const plainTextElements = this.body.querySelectorAll('[data-editable-kind="plain_text"], [data-editable-kind="markdown"], [data-editable-kind="html"]')
74
+ console.debug(`[Panda CMS] Found ${plainTextElements.length} plain text elements`)
75
+
76
+ plainTextElements.forEach(element => {
77
+ console.debug(`[Panda CMS] Initializing plain text editor for element:`, element)
78
+ const editor = new PlainTextEditor(element, this.frameDocument, {
79
+ autosave: this.autosaveValue,
80
+ adminPath: this.adminPathValue,
81
+ csrfToken: this.csrfToken
82
+ })
83
+ this.editors.push(editor)
84
+ })
85
+
86
+ this.editorsInitialized.plain = true
87
+ this.checkAllEditorsInitialized()
88
+ }
89
+
90
+ /**
91
+ * Initializes the rich text editors for the page.
92
+ * This method is responsible for finding all elements on the page that have the "rich_text" data-editable-kind attribute,
93
+ * and initializing a RichTextEditor instance for each of them. It also sets the editorsInitialized.rich flag to true
94
+ * and calls the checkAllEditorsInitialized method to notify that the rich text editors have been initialized.
95
+ */
96
+ initializeRichTextEditors() {
97
+ const richTextElements = this.body.querySelectorAll('[data-editable-kind="rich_text"]')
98
+ console.debug(`[Panda CMS] Found ${richTextElements.length} rich text elements`)
99
+
100
+ if (richTextElements.length > 0) {
101
+ Promise.all([
102
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/editorjs@latest"), // Base EditorJS
103
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/header@latest"), // Header Tool
104
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/list@latest"), // List Tool
105
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"), // Quote Tool
106
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/nested-list@latest"), // Nested List Tool
107
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"), // Simple Image Tool
108
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/table@latest"), // Table Tool
109
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"), // Link Tool
110
+ ResourceLoader.loadScript(this.frameDocument, this.head, "https://cdn.jsdelivr.net/npm/editorjs-alert@latest"), // Alert Tool
111
+ ResourceLoader.embedCSS(this.frameDocument, this.head, ".ce-toolbar__content { margin: 0 !important; margin-left: 40px; max-width: 100% !important; width: 100% !important; } .ce-block__content { max-width: 100%; margin: 0 !important; margin-left: 10px !important; }")
112
+ ]).then(() => {
113
+ richTextElements.forEach(element => {
114
+ console.debug(`[Panda CMS] Initializing rich text editor for element:`, element)
115
+
116
+ // TODO: Need a way to override this per-site ... or rather, block/editor?
117
+ const elementAsId = element.id.replace(/-/g, "_")
118
+ const adminPathValue = this.adminPathValue
119
+ const pageId = element.getAttribute("data-editable-page-id")
120
+ const blockContentId = element.getAttribute("data-editable-block-content-id")
121
+ const csrfToken = this.csrfToken
122
+ const previousData = element.getAttribute("data-editable-previous-data")
123
+ const editorConfig = `{
124
+ holder: '${element.id}',
125
+ data: ${previousData},
126
+ tools: {
127
+ header: {
128
+ class: Header,
129
+ config: {
130
+ placeholder: 'Enter a header',
131
+ levels: [2, 3],
132
+ defaultLevel: 2
133
+ }
134
+ },
135
+ list: {
136
+ class: NestedList,
137
+ inlineToolbar: true,
138
+ config: {
139
+ defaultStyle: 'unordered'
140
+ },
141
+ },
142
+ alert: {
143
+ class: Alert,
144
+ inlineToolbar: true,
145
+ config: {
146
+ defaultType: 'primary',
147
+ messagePlaceholder: 'Enter something',
148
+ types: {
149
+ primary: 'Primary',
150
+ secondary: 'Secondary',
151
+ success: 'Success',
152
+ danger: 'Danger',
153
+ warning: 'Warning',
154
+ info: 'Info'
155
+ }
156
+ }
157
+ },
158
+ quote: Quote,
159
+ table: {
160
+ class: Table,
161
+ inlineToolbar: true,
162
+ config: {
163
+ rows: 2,
164
+ cols: 3
165
+ }
166
+ },
167
+ image: SimpleImage,
168
+ embed: {
169
+ class: Embed,
170
+ config: {
171
+ services: {
172
+ youtube: true,
173
+ instagram: true,
174
+ miro: true,
175
+ vimeo: true,
176
+ pinterest: true,
177
+ github: true
178
+ }
179
+ }
180
+ },
181
+ }
182
+ }`
183
+
184
+ // console.log(editorConfig);
185
+
186
+ ResourceLoader.embedScript(
187
+ `EditorJS configuration for ${element.id}`,
188
+ this.frameDocument,
189
+ this.head,
190
+ `
191
+ const ${elementAsId} = new EditorJS(${editorConfig})
192
+ parent.document.getElementById('saveEditableButton').addEventListener('click', (element) => {
193
+ ${elementAsId}.save().then((outputData) => {
194
+ outputData.source = "editorJS"
195
+ console.log('Saving successful, here is outputData:')
196
+ dataToSend = JSON.stringify({ content: outputData })
197
+ console.log(dataToSend)
198
+ fetch("${adminPathValue}/pages/${pageId}/block_contents/${blockContentId}", {
199
+ method: "PATCH",
200
+ headers: {
201
+ "Content-Type": "application/json",
202
+ "X-CSRF-Token": "${this.csrfToken}"
203
+ },
204
+ body: dataToSend
205
+ })
206
+ .then(response => console.log(response))
207
+ .then(() => alert("Saved"))
208
+ .catch((error) => alert("Saving failed (1): " + error))
209
+ }).catch((error) => {
210
+ console.log('Saving failed: ', error)
211
+ alert('Saving failed (2): ' + error)
212
+ })
213
+ })
214
+
215
+ console.debug("[Panda CMS] Initialized rich text editor for element: ${elementAsId}")
216
+ `
217
+ )
218
+ this.editors.push(elementAsId)
219
+ })
220
+ }).then(() => {
221
+ this.editorsInitialized.rich = true
222
+ this.checkAllEditorsInitialized()
223
+ })
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Checks if all editors have been initialized and sets the iFrame to visible if so.
229
+ * This is called after the plain text and rich text editors have been initialized.
230
+ */
231
+ checkAllEditorsInitialized() {
232
+ console.debug("[Panda CMS] Editor initialization status:", this.editorsInitialized)
233
+ if (this.editorsInitialized.plain && this.editorsInitialized.rich) {
234
+ console.debug("[Panda CMS] All editors initialized, showing iFrame")
235
+ this.setiFrameVisible()
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Sets the visibility of the iFrame to visible.
241
+ * This is called after all editors have been initialized to show the iFrame.
242
+ */
243
+ setiFrameVisible() {
244
+ console.debug("[Panda CMS] Setting iFrame to visible")
245
+ this.frame.style.display = ""
246
+ }
247
+ }
@@ -0,0 +1,45 @@
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
+ // Grab our internal controllers manually, prefixed with panda_cms_controllers so not to conflict
18
+ import { DashboardController } from "panda_cms_controllers/dashboard_controller"
19
+ pandaCmsApplication.register("dashboard", DashboardController)
20
+
21
+ import { SlugController } from "panda_cms_controllers/slug_controller"
22
+ pandaCmsApplication.register("slug", SlugController)
23
+
24
+ import { EditorController } from "panda_cms_controllers/editor_controller"
25
+ pandaCmsApplication.register("editor", EditorController)
26
+
27
+ console.debug("[Panda CMS] Registering components...")
28
+
29
+ // Import and register all TailwindCSS Components or just the ones you need
30
+ import { Alert, Autosave, ColorPreview, Dropdown, Modal, Tabs, Popover, Toggle, Slideover } from "tailwindcss-stimulus-components"
31
+ pandaCmsApplication.register('alert', Alert)
32
+ pandaCmsApplication.register('autosave', Autosave)
33
+ pandaCmsApplication.register('color-preview', ColorPreview)
34
+ pandaCmsApplication.register('dropdown', Dropdown)
35
+ pandaCmsApplication.register('modal', Modal)
36
+ pandaCmsApplication.register('popover', Popover)
37
+ pandaCmsApplication.register('slideover', Slideover)
38
+ pandaCmsApplication.register('tabs', Tabs)
39
+ pandaCmsApplication.register('toggle', Toggle)
40
+
41
+ console.debug("[Panda CMS] Components registered...")
42
+
43
+ export { pandaCmsApplication }
44
+
45
+ console.debug("[Panda CMS] Application exported...")
@@ -0,0 +1,48 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export class SlugController 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
+ }
14
+
15
+ generatePath() {
16
+ this.output_textTarget.value = "/" + this.createSlug(this.input_textTarget.value);
17
+ console.log("Have set the path to: " + this.output_textTarget.value);
18
+ }
19
+
20
+ setPrePath() {
21
+ this.parent_slugs = this.input_selectTarget.options[this.input_selectTarget.selectedIndex].text.match(/.*\((.*)\)$/)[1];
22
+ this.output_textTarget.previousSibling.innerHTML = (this.existing_rootTarget.value + this.parent_slugs).replace(/\/$/, "");
23
+ console.log("Have set the pre-path to: " + this.output_textTarget.previousSibling.innerHTML);
24
+ }
25
+
26
+ // TODO: Invoke a library or helper which can be shared with the backend
27
+ // and check for uniqueness at the same time
28
+ createSlug(input) {
29
+ var str = input
30
+ .toLowerCase()
31
+ .trim()
32
+ .replace(/[^\w\s-]/g, "-")
33
+ .replace(/&/g, "and")
34
+ .replace(/[\s_-]+/g, "-")
35
+ .trim();
36
+
37
+ return this.trimStartEnd(str, "-");
38
+ }
39
+
40
+ trimStartEnd(str, ch) {
41
+ var start = 0;
42
+ var end = str.length;
43
+
44
+ while (start < end && str[start] === ch) ++start;
45
+ while (end > start && str[end - 1] === ch) --end;
46
+ return start > 0 || end < str.length ? str.substring(start, end) : str;
47
+ }
48
+ }
@@ -0,0 +1,102 @@
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.innerText :
60
+ this.element.innerHTML
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(() => this.showSuccess())
72
+ .catch(error => this.showError(error))
73
+ }
74
+
75
+ /**
76
+ * Displays a success message by temporarily changing the background color of the editor element.
77
+ *
78
+ * This method is called after a successful save operation to provide visual feedback to the user.
79
+ */
80
+ showSuccess() {
81
+ this.element.style.backgroundColor = "#66bd6a50"
82
+ setTimeout(() => {
83
+ this.element.style.backgroundColor = "inherit"
84
+ }, 1000)
85
+ }
86
+
87
+ /**
88
+ * Displays an error message by temporarily changing the background color of the editor element and logging the error to the console.
89
+ *
90
+ * This method is called after a failed save operation to provide visual and textual feedback to the user.
91
+ *
92
+ * @param {Error} error - The error object that occurred during the save operation.
93
+ */
94
+ showError(error) {
95
+ this.element.style.backgroundColor = "#dc354550"
96
+ setTimeout(() => {
97
+ this.element.style.backgroundColor = "inherit"
98
+ }, 1000)
99
+ console.log(error)
100
+ alert("Error:", error)
101
+ }
102
+ }
@@ -0,0 +1,69 @@
1
+ export class ResourceLoader {
2
+ static loadScript(frameDocument, head, src) {
3
+ return new Promise((resolve, reject) => {
4
+ const script = frameDocument.createElement("script")
5
+ script.src = src
6
+ head.append(script)
7
+
8
+ script.onload = () => {
9
+ console.debug(`[Panda CMS] Script loaded: ${src}`)
10
+ resolve(script)
11
+ }
12
+ script.onerror = () => reject(new Error(`[Panda CMS] Script load error for ${src}`))
13
+ })
14
+ }
15
+
16
+ static importScript(frameDocument, head, module, src) {
17
+ return new Promise((resolve, reject) => {
18
+ const script = frameDocument.createElement("script")
19
+ script.type = "module"
20
+ script.textContent = `import ${module} from "${src}"`
21
+ head.append(script)
22
+
23
+ script.onload = () => {
24
+ console.debug(`[Panda CMS] Module script loaded: ${src}`)
25
+ resolve(script)
26
+ }
27
+ script.onerror = () => reject(new Error(`[Panda CMS] Module script load error for ${src}`))
28
+ })
29
+ }
30
+
31
+ static embedScript(description, frameDocument, head, code) {
32
+ return new Promise((resolve) => {
33
+ const script = frameDocument.createElement("script")
34
+ script.textContent = code
35
+ head.append(script)
36
+ resolve(script)
37
+ console.debug(`[Panda CMS] Embedded script loaded (${description})`)
38
+ })
39
+ }
40
+
41
+ static loadStylesheet(frameDocument, head, href) {
42
+ return new Promise((resolve, reject) => {
43
+ const link = frameDocument.createElement("link")
44
+ link.rel = "stylesheet"
45
+ link.href = href
46
+ link.media = "none"
47
+ head.append(link)
48
+
49
+ link.onload = () => {
50
+ if (link.media != "all") {
51
+ link.media = "all"
52
+ }
53
+ console.debug(`[Panda CMS] Stylesheet loaded: ${href}`)
54
+ resolve(link)
55
+ }
56
+ link.onerror = () => reject(new Error(`[Panda CMS] Stylesheet load error for ${href}`))
57
+ })
58
+ }
59
+
60
+ static embedCSS(frameDocument, head, css) {
61
+ return new Promise((resolve) => {
62
+ const style = frameDocument.createElement("style")
63
+ style.textContent = css
64
+ head.append(style)
65
+ console.debug(`[Panda CMS] Embedded CSS loaded`)
66
+ resolve(style)
67
+ })
68
+ }
69
+ }
@@ -0,0 +1,89 @@
1
+ // import EditorJS from '@editorjs/editorjs';
2
+
3
+ // export class RichTextEditor {
4
+ // /**
5
+ // * Constructs a new RichTextEditor instance.
6
+ // *
7
+ // * @param {HTMLElement} element - The HTML element that represents the rich text editor.
8
+ // * @param {HTMLIFrameElement} frame - The HTML iframe element containing the rich text editor.
9
+ // * @param {Object} options - An object containing configuration options for the rich text editor.
10
+ // */
11
+ // constructor(element, frame, options) {
12
+ // this.element = element
13
+ // this.frame = frame
14
+ // this.options = options
15
+ // this.bindEvents()
16
+ // }
17
+
18
+ // save() {
19
+
20
+ // }
21
+
22
+ // /**
23
+ // * Binds event listeners for the rich text editor.
24
+ // *
25
+ // * 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.
26
+ // *
27
+ // * Additionally, this method adds a `click` event listener to the "Save Editable" button, which also triggers the `save()` method when clicked.
28
+ // */
29
+ // bindEvents() {
30
+ // if (this.options.autosave) {
31
+ // this.element.addEventListener("blur", () => this.save())
32
+ // }
33
+
34
+ // document.getElementById('saveEditableButton').addEventListener('click', () => this.save())
35
+ // }
36
+
37
+ // /**
38
+ // * Saves the content of the plain text editor to the server.
39
+ // *
40
+ // * 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.
41
+ // */
42
+ // save() {
43
+ // alert('save');
44
+ // return false;
45
+
46
+ // const blockContentId = this.element.getAttribute("data-editable-block-content-id")
47
+ // const pageId = this.element.getAttribute("data-editable-page-id")
48
+
49
+ // fetch(`${this.options.adminPath}/pages/${pageId}/block_contents/${blockContentId}`, {
50
+ // method: "PATCH",
51
+ // headers: {
52
+ // "Content-Type": "application/json",
53
+ // "X-CSRF-Token": this.options.csrfToken
54
+ // },
55
+ // body: JSON.stringify({ content: content })
56
+ // })
57
+ // .then(response => response.json())
58
+ // .then(() => this.showSuccess())
59
+ // .catch(error => this.showError(error))
60
+ // }
61
+
62
+ // /**
63
+ // * Displays a success message by temporarily changing the background color of the editor element.
64
+ // *
65
+ // * This method is called after a successful save operation to provide visual feedback to the user.
66
+ // */
67
+ // showSuccess() {
68
+ // this.element.style.backgroundColor = "#66bd6a50"
69
+ // setTimeout(() => {
70
+ // this.element.style.backgroundColor = "inherit"
71
+ // }, 1000)
72
+ // }
73
+
74
+ // /**
75
+ // * Displays an error message by temporarily changing the background color of the editor element and logging the error to the console.
76
+ // *
77
+ // * This method is called after a failed save operation to provide visual and textual feedback to the user.
78
+ // *
79
+ // * @param {Error} error - The error object that occurred during the save operation.
80
+ // */
81
+ // showError(error) {
82
+ // this.element.style.backgroundColor = "#dc354550"
83
+ // setTimeout(() => {
84
+ // this.element.style.backgroundColor = "inherit"
85
+ // }, 1000)
86
+ // console.log(error)
87
+ // alert("Error:", error)
88
+ // }
89
+ // }
@@ -0,0 +1,4 @@
1
+ // tailwindcss-stimulus-components@6.1.2 downloaded from https://ga.jspm.io/npm:tailwindcss-stimulus-components@6.1.2/dist/tailwindcss-stimulus-components.module.js
2
+
3
+ import{Controller as e}from"@hotwired/stimulus";var t=Object.defineProperty;var V=(e,s,a)=>s in e?t(e,s,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[s]=a;var i=(e,t,s)=>V(e,typeof t!="symbol"?t+"":t,s);async function n(e,t,s={}){t?await T(e,s):await b(e,s)}async function T(e,t={}){let{transitionClasses:s,fromClasses:a,toClasses:o,toggleClass:r}=C("Enter",e,t);return v(e,{firstFrame(){e.classList.add(...s.split(" ")),e.classList.add(...a.split(" ")),e.classList.remove(...o.split(" ")),e.classList.remove(...r.split(" "))},secondFrame(){e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))},ending(){e.classList.remove(...s.split(" "))}})}async function b(e,t={}){let{transitionClasses:s,fromClasses:a,toClasses:o,toggleClass:r}=C("Leave",e,t);return v(e,{firstFrame(){e.classList.add(...a.split(" ")),e.classList.remove(...o.split(" ")),e.classList.add(...s.split(" "))},secondFrame(){e.classList.remove(...a.split(" ")),e.classList.add(...o.split(" "))},ending(){e.classList.remove(...s.split(" ")),e.classList.add(...r.split(" "))}})}function C(e,t,s){return{transitionClasses:t.dataset[`transition${e}`]||s[e.toLowerCase()]||e.toLowerCase(),fromClasses:t.dataset[`transition${e}From`]||s[`${e.toLowerCase()}From`]||`${e.toLowerCase()}-from`,toClasses:t.dataset[`transition${e}To`]||s[`${e.toLowerCase()}To`]||`${e.toLowerCase()}-to`,toggleClass:t.dataset.toggleClass||s.toggleClass||s.toggle||"hidden"}}function L(e){e._stimulus_transition={timeout:null,interrupted:!1}}function I(e){e._stimulus_transition&&e._stimulus_transition.interrupt&&e._stimulus_transition.interrupt()}function v(e,t){e._stimulus_transition&&I(e);let s,a,o;return L(e),e._stimulus_transition.cleanup=()=>{a||t.firstFrame(),o||t.secondFrame(),t.ending(),e._stimulus_transition=null},e._stimulus_transition.interrupt=()=>{s=!0,e._stimulus_transition.timeout&&clearTimeout(e._stimulus_transition.timeout),e._stimulus_transition.cleanup()},new Promise((r=>{s||requestAnimationFrame((()=>{s||(t.firstFrame(),a=!0,requestAnimationFrame((()=>{s||(t.secondFrame(),o=!0,e._stimulus_transition&&(e._stimulus_transition.timeout=setTimeout((()=>{s||e._stimulus_transition.cleanup(),r()}),w(e))))})))}))}))}function w(e){let t=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,s=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;return t===0&&(t=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),t+s}var s=class extends e{connect(){setTimeout((()=>{T(this.element)}),this.showDelayValue),this.hasDismissAfterValue&&setTimeout((()=>{this.close()}),this.dismissAfterValue)}close(){b(this.element).then((()=>{this.element.remove()}))}};i(s,"values",{dismissAfter:Number,showDelay:{type:Number,default:0}});var a=class extends e{connect(){this.timeout=null}save(){clearTimeout(this.timeout),this.timeout=setTimeout((()=>{this.statusTarget.textContent=this.submittingTextValue,this.formTarget.requestSubmit()}),this.submitDurationValue)}success(){this.setStatus(this.successTextValue)}error(){this.setStatus(this.errorTextValue)}setStatus(e){this.statusTarget.textContent=e,this.timeout=setTimeout((()=>{this.statusTarget.textContent=""}),this.statusDurationValue)}};i(a,"targets",["form","status"]),i(a,"values",{submitDuration:{type:Number,default:1e3},statusDuration:{type:Number,default:2e3},submittingText:{type:String,default:"Saving..."},successText:{type:String,default:"Saved!"},errorText:{type:String,default:"Unable to save."}});var o=class extends e{update(){this.preview=this.colorTarget.value}set preview(e){this.previewTarget.style[this.styleValue]=e;let t=this._getContrastYIQ(e);this.styleValue==="color"?this.previewTarget.style.backgroundColor=t:this.previewTarget.style.color=t}_getContrastYIQ(e){e=e.replace("#","");let t=128,s=parseInt(e.substr(0,2),16),a=parseInt(e.substr(2,2),16),o=parseInt(e.substr(4,2),16);return(s*299+a*587+o*114)/1e3>=t?"#000":"#fff"}};i(o,"targets",["preview","color"]),i(o,"values",{style:{type:String,default:"backgroundColor"}});var r=class extends e{connect(){this.boundBeforeCache=this.beforeCache.bind(this),document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}openValueChanged(){n(this.menuTarget,this.openValue,this.transitionOptions),this.openValue===!0&&this.hasMenuItemTarget&&this.menuItemTargets[0].focus()}show(){this.openValue=!0}close(){this.openValue=!1}hide(e){this.closeOnClickOutsideValue&&e.target.nodeType&&this.element.contains(e.target)===!1&&this.openValue&&(this.openValue=!1),this.closeOnEscapeValue&&e.key==="Escape"&&this.openValue&&(this.openValue=!1)}toggle(){this.openValue=!this.openValue}nextItem(e){e.preventDefault(),this.menuItemTargets[this.nextIndex].focus()}previousItem(e){e.preventDefault(),this.menuItemTargets[this.previousIndex].focus()}get currentItemIndex(){return this.menuItemTargets.indexOf(document.activeElement)}get nextIndex(){return Math.min(this.currentItemIndex+1,this.menuItemTargets.length-1)}get previousIndex(){return Math.max(this.currentItemIndex-1,0)}get transitionOptions(){return{enter:this.hasEnterClass?this.enterClass:"transition ease-out duration-100",enterFrom:this.hasEnterFromClass?this.enterFromClass:"transform opacity-0 scale-95",enterTo:this.hasEnterToClass?this.enterToClass:"transform opacity-100 scale-100",leave:this.hasLeaveClass?this.leaveClass:"transition ease-in duration-75",leaveFrom:this.hasLeaveFromClass?this.leaveFromClass:"transform opacity-100 scale-100",leaveTo:this.hasLeaveToClass?this.leaveToClass:"transform opacity-0 scale-95",toggleClass:this.hasToggleClass?this.toggleClass:"hidden"}}beforeCache(){this.openValue=!1,this.menuTarget.classList.add("hidden")}};i(r,"targets",["menu","button","menuItem"]),i(r,"values",{open:{type:Boolean,default:!1},closeOnEscape:{type:Boolean,default:!0},closeOnClickOutside:{type:Boolean,default:!0}}),i(r,"classes",["enter","enterFrom","enterTo","leave","leaveFrom","leaveTo","toggle"]);var l=class extends e{connect(){this.openValue&&this.open(),this.boundBeforeCache=this.beforeCache.bind(this),document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.dialogTarget.show()}hide(){this.close()}beforeCache(){this.close()}};i(l,"targets",["dialog"]),i(l,"values",{open:Boolean});var u=class extends e{openValueChanged(){n(this.contentTarget,this.openValue),this.shouldAutoDismiss&&this.scheduleDismissal()}show(e){this.shouldAutoDismiss&&this.scheduleDismissal(),this.openValue=!0}hide(){this.openValue=!1}toggle(){this.openValue=!this.openValue}get shouldAutoDismiss(){return this.openValue&&this.hasDismissAfterValue}scheduleDismissal(){this.hasDismissAfterValue&&(this.cancelDismissal(),this.timeoutId=setTimeout((()=>{this.hide(),this.timeoutId=void 0}),this.dismissAfterValue))}cancelDismissal(){typeof this.timeoutId=="number"&&(clearTimeout(this.timeoutId),this.timeoutId=void 0)}};i(u,"targets",["content"]),i(u,"values",{dismissAfter:Number,open:{type:Boolean,default:!1}});var h=class extends e{connect(){this.openValue&&this.open(),this.boundBeforeCache=this.beforeCache,document.addEventListener("turbo:before-cache",this.boundBeforeCache)}disconnect(){document.removeEventListener("turbo:before-cache",this.boundBeforeCache)}open(){this.dialogTarget.showModal()}close(){this.dialogTarget.setAttribute("closing",""),Promise.all(this.dialogTarget.getAnimations().map((e=>e.finished))).then((()=>{this.dialogTarget.removeAttribute("closing"),this.dialogTarget.close()}))}backdropClose(e){e.target.nodeName=="DIALOG"&&this.close()}show(){this.open()}hide(){this.close()}beforeCache(){this.close()}};i(h,"targets",["dialog"]),i(h,"values",{open:Boolean});var c=class extends e{initialize(){this.updateAnchorValue&&this.anchor&&(this.indexValue=this.tabTargets.findIndex((e=>e.id===this.anchor)))}connect(){this.showTab()}change(e){e.currentTarget.tagName==="SELECT"?this.indexValue=e.currentTarget.selectedIndex:e.currentTarget.dataset.index?this.indexValue=e.currentTarget.dataset.index:e.currentTarget.dataset.id?this.indexValue=this.tabTargets.findIndex((t=>t.id==e.currentTarget.dataset.id)):this.indexValue=this.tabTargets.indexOf(e.currentTarget)}nextTab(){this.indexValue=Math.min(this.indexValue+1,this.tabsCount-1)}previousTab(){this.indexValue=Math.max(this.indexValue-1,0)}firstTab(){this.indexValue=0}lastTab(){this.indexValue=this.tabsCount-1}indexValueChanged(){if(this.showTab(),this.dispatch("tab-change",{target:this.tabTargets[this.indexValue],detail:{activeIndex:this.indexValue}}),this.updateAnchorValue){let e=this.tabTargets[this.indexValue].id;if(this.scrollToAnchorValue)location.hash=e;else{let t=window.location.href.split("#")[0]+"#"+e;typeof Turbo<"u"?Turbo.navigator.history.replace(new URL(t)):history.replaceState({},document.title,t)}}}showTab(){this.panelTargets.forEach(((e,t)=>{let s=this.tabTargets[t];t===this.indexValue?(e.classList.remove("hidden"),s.ariaSelected="true",s.dataset.active=!0,this.hasInactiveTabClass&&s?.classList?.remove(...this.inactiveTabClasses),this.hasActiveTabClass&&s?.classList?.add(...this.activeTabClasses)):(e.classList.add("hidden"),s.ariaSelected=null,delete s.dataset.active,this.hasActiveTabClass&&s?.classList?.remove(...this.activeTabClasses),this.hasInactiveTabClass&&s?.classList?.add(...this.inactiveTabClasses))})),this.hasSelectTarget&&(this.selectTarget.selectedIndex=this.indexValue),this.scrollActiveTabIntoViewValue&&this.scrollToActiveTab()}scrollToActiveTab(){let e=this.element.querySelector("[aria-selected]");e&&e.scrollIntoView({inline:"center"})}get tabsCount(){return this.tabTargets.length}get anchor(){return document.URL.split("#").length>1?document.URL.split("#")[1]:null}};i(c,"classes",["activeTab","inactiveTab"]),i(c,"targets",["tab","panel","select"]),i(c,"values",{index:0,updateAnchor:Boolean,scrollToAnchor:Boolean,scrollActiveTabIntoView:Boolean});var d=class extends e{toggle(e){this.openValue=!this.openValue,this.animate()}toggleInput(e){this.openValue=e.target.checked,this.animate()}hide(){this.openValue=!1,this.animate()}show(){this.openValue=!0,this.animate()}animate(){this.toggleableTargets.forEach((e=>{n(e,this.openValue)}))}};i(d,"targets",["toggleable"]),i(d,"values",{open:{type:Boolean,default:!1}});export{s as Alert,a as Autosave,o as ColorPreview,r as Dropdown,l as Modal,u as Popover,h as Slideover,c as Tabs,d as Toggle,n as transition};
4
+
@@ -24,8 +24,6 @@ module PandaCms
24
24
  @templates[key] = PandaCms::Template.find_or_create_by!(template)
25
25
  end
26
26
 
27
- PandaCms::Template.generate_missing_blocks
28
-
29
27
  @templates
30
28
  end
31
29
 
@@ -36,7 +34,7 @@ module PandaCms
36
34
  def create_pages
37
35
  @pages[:home] = PandaCms::Page.find_or_create_by!({path: "/", title: "Home", template: @templates[:homepage]})
38
36
  @pages[:about] = PandaCms::Page.find_or_create_by!({path: "/about", title: "About", template: @templates[:page], parent: @pages[:home]})
39
- @pages[:terms] = PandaCms::Page.find_or_create_by!({path: "/terms-and-conditions", title: "Terms & Conditions", template: @templates[:page], parent: @pages[:home]})
37
+ @pages[:terms] = PandaCms::Page.find_or_create_by!({path: "/terms-and-conditions", title: "Terms & Conditions", template: @templates[:page], parent: @pages[:home], status: "hidden"})
40
38
 
41
39
  PandaCms::Page.reset_column_information
42
40
  PandaCms::Page.rebuild!