panda_cms 0.6.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/builds/panda_cms.css +17 -0
- data/app/assets/config/panda_cms_manifest.js +1 -0
- data/app/components/panda_cms/code_component.rb +3 -1
- data/app/components/panda_cms/menu_component.html.erb +3 -0
- data/app/components/panda_cms/menu_component.rb +8 -1
- data/app/components/panda_cms/page_menu_component.html.erb +7 -5
- data/app/components/panda_cms/page_menu_component.rb +3 -1
- data/app/components/panda_cms/rich_text_component.html.erb +2 -2
- data/app/components/panda_cms/rich_text_component.rb +12 -4
- data/app/components/panda_cms/text_component.rb +1 -1
- data/app/controllers/panda_cms/admin/block_contents_controller.rb +1 -1
- data/app/javascript/panda_cms/@editorjs--editorjs.js +2577 -0
- data/app/javascript/panda_cms/controllers/editor_controller.js +247 -0
- data/app/javascript/panda_cms/controllers/index.js +10 -7
- data/app/javascript/panda_cms/editor/plain_text_editor.js +102 -0
- data/app/javascript/panda_cms/editor/resource_loader.js +69 -0
- data/app/javascript/panda_cms/editor/rich_text_editor.js +89 -0
- data/app/lib/panda_cms/demo_site_generator.rb +0 -2
- data/app/lib/panda_cms/editor_js/blocks/alert.rb +32 -0
- data/app/lib/panda_cms/editor_js/blocks/base.rb +28 -0
- data/app/lib/panda_cms/editor_js/blocks/header.rb +13 -0
- data/app/lib/panda_cms/editor_js/blocks/image.rb +34 -0
- data/app/lib/panda_cms/editor_js/blocks/list.rb +30 -0
- data/app/lib/panda_cms/editor_js/blocks/paragraph.rb +13 -0
- data/app/lib/panda_cms/editor_js/blocks/quote.rb +27 -0
- data/app/lib/panda_cms/editor_js/blocks/table.rb +48 -0
- data/app/lib/panda_cms/editor_js/renderer.rb +120 -0
- data/app/models/panda_cms/block_content.rb +12 -2
- data/app/views/panda_cms/admin/pages/edit.html.erb +10 -9
- data/app/views/panda_cms/shared/_header.html.erb +2 -3
- data/app/views/panda_cms/shared/_importmap.html.erb +13 -3
- data/config/importmap.rb +3 -1
- data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +5 -3
- data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
- data/db/seeds.rb +1 -0
- data/lib/panda_cms/engine.rb +16 -8
- data/lib/panda_cms/version.rb +1 -1
- metadata +89 -103
- data/app/javascript/panda_cms/panda_cms_editable.js +0 -248
@@ -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
|
+
}
|
@@ -1,10 +1,10 @@
|
|
1
|
-
console.debug("[Panda CMS] Importing Panda CMS Stimulus Controller...")
|
1
|
+
console.debug("[Panda CMS] Importing Panda CMS Stimulus Controller...")
|
2
2
|
|
3
3
|
import { Application as PandaCmsApplication } from "@hotwired/stimulus"
|
4
4
|
|
5
5
|
const pandaCmsApplication = PandaCmsApplication.start()
|
6
6
|
|
7
|
-
console.debug("[Panda CMS] Application started...")
|
7
|
+
console.debug("[Panda CMS] Application started...")
|
8
8
|
|
9
9
|
// Configure Stimulus development experience
|
10
10
|
pandaCmsApplication.debug = false
|
@@ -14,13 +14,16 @@ console.debug("[Panda CMS] window.pandaCmsStimulus available...")
|
|
14
14
|
|
15
15
|
console.debug("[Panda CMS] Registering controllers...")
|
16
16
|
|
17
|
-
// Grab our internal controllers manually
|
18
|
-
import { DashboardController } from "
|
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
19
|
pandaCmsApplication.register("dashboard", DashboardController)
|
20
20
|
|
21
|
-
import { SlugController } from "
|
21
|
+
import { SlugController } from "panda_cms_controllers/slug_controller"
|
22
22
|
pandaCmsApplication.register("slug", SlugController)
|
23
23
|
|
24
|
+
import { EditorController } from "panda_cms_controllers/editor_controller"
|
25
|
+
pandaCmsApplication.register("editor", EditorController)
|
26
|
+
|
24
27
|
console.debug("[Panda CMS] Registering components...")
|
25
28
|
|
26
29
|
// Import and register all TailwindCSS Components or just the ones you need
|
@@ -35,8 +38,8 @@ pandaCmsApplication.register('slideover', Slideover)
|
|
35
38
|
pandaCmsApplication.register('tabs', Tabs)
|
36
39
|
pandaCmsApplication.register('toggle', Toggle)
|
37
40
|
|
38
|
-
console.debug("[Panda CMS] Components registered...")
|
41
|
+
console.debug("[Panda CMS] Components registered...")
|
39
42
|
|
40
43
|
export { pandaCmsApplication }
|
41
44
|
|
42
|
-
console.debug("[Panda CMS] Application exported...")
|
45
|
+
console.debug("[Panda CMS] Application exported...")
|
@@ -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,32 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Alert < Base
|
5
|
+
def render
|
6
|
+
message = sanitize(data["message"])
|
7
|
+
type = data["type"] || "primary"
|
8
|
+
|
9
|
+
html_safe(
|
10
|
+
"<div class=\"#{alert_classes(type)} p-4 mb-4 rounded-lg\">" \
|
11
|
+
"#{message}" \
|
12
|
+
"</div>"
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def alert_classes(type)
|
19
|
+
case type
|
20
|
+
when "primary" then "bg-blue-100 text-blue-800"
|
21
|
+
when "secondary" then "bg-gray-100 text-gray-800"
|
22
|
+
when "success" then "bg-green-100 text-green-800"
|
23
|
+
when "danger" then "bg-red-100 text-red-800"
|
24
|
+
when "warning" then "bg-yellow-100 text-yellow-800"
|
25
|
+
when "info" then "bg-indigo-100 text-indigo-800"
|
26
|
+
else "bg-blue-100 text-blue-800"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Base
|
5
|
+
attr_reader :data, :options
|
6
|
+
|
7
|
+
def initialize(data, options = {})
|
8
|
+
@data = data
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def render
|
13
|
+
""
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def sanitize(text)
|
19
|
+
Rails::Html::SafeListSanitizer.new.sanitize(text, tags: %w[b i u a code])
|
20
|
+
end
|
21
|
+
|
22
|
+
def html_safe(text)
|
23
|
+
text.to_s.html_safe
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PandaCms
|
2
|
+
module EditorJs
|
3
|
+
module Blocks
|
4
|
+
class Image < Base
|
5
|
+
def render
|
6
|
+
url = data["url"]
|
7
|
+
caption = sanitize(data["caption"])
|
8
|
+
with_border = data["withBorder"]
|
9
|
+
with_background = data["withBackground"]
|
10
|
+
stretched = data["stretched"]
|
11
|
+
|
12
|
+
css_classes = ["prose"]
|
13
|
+
css_classes << "border" if with_border
|
14
|
+
css_classes << "bg-gray-100" if with_background
|
15
|
+
css_classes << "w-full" if stretched
|
16
|
+
|
17
|
+
html_safe(<<~HTML)
|
18
|
+
<figure class="#{css_classes.join(" ")}">
|
19
|
+
<img src="#{url}" alt="#{caption}" />
|
20
|
+
#{caption_element(caption)}
|
21
|
+
</figure>
|
22
|
+
HTML
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def caption_element(caption)
|
28
|
+
return "" if caption.blank?
|
29
|
+
"<figcaption>#{caption}</figcaption>"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|