panda-cms 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/assets/builds/panda.cms.css +0 -50
- data/app/components/panda/cms/admin/table_component.html.erb +4 -1
- data/app/components/panda/cms/code_component.rb +2 -1
- data/app/components/panda/cms/rich_text_component.html.erb +86 -2
- data/app/components/panda/cms/rich_text_component.rb +131 -20
- data/app/controllers/panda/cms/admin/block_contents_controller.rb +18 -7
- data/app/controllers/panda/cms/admin/files_controller.rb +22 -12
- data/app/controllers/panda/cms/admin/posts_controller.rb +33 -11
- data/app/controllers/panda/cms/pages_controller.rb +29 -0
- data/app/controllers/panda/cms/posts_controller.rb +26 -4
- data/app/helpers/panda/cms/admin/posts_helper.rb +23 -32
- data/app/helpers/panda/cms/posts_helper.rb +32 -0
- data/app/javascript/panda/cms/controllers/dashboard_controller.js +0 -1
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +134 -11
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +395 -130
- data/app/javascript/panda/cms/controllers/slug_controller.js +33 -43
- data/app/javascript/panda/cms/editor/editor_js_config.js +202 -73
- data/app/javascript/panda/cms/editor/editor_js_initializer.js +243 -194
- data/app/javascript/panda/cms/editor/plain_text_editor.js +1 -1
- data/app/javascript/panda/cms/editor/resource_loader.js +89 -0
- data/app/javascript/panda/cms/editor/rich_text_editor.js +162 -0
- data/app/models/panda/cms/page.rb +18 -0
- data/app/models/panda/cms/post.rb +61 -3
- data/app/models/panda/cms/redirect.rb +2 -2
- data/app/views/panda/cms/admin/posts/_form.html.erb +15 -4
- data/app/views/panda/cms/admin/posts/index.html.erb +5 -3
- data/config/routes.rb +34 -6
- data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +5 -0
- data/lib/panda/cms/editor_js_content.rb +14 -1
- data/lib/panda/cms/engine.rb +4 -0
- data/lib/panda-cms/version.rb +1 -1
- metadata +5 -2
@@ -8,6 +8,10 @@ export default class extends Controller {
|
|
8
8
|
"output_text",
|
9
9
|
];
|
10
10
|
|
11
|
+
static values = {
|
12
|
+
addDatePrefix: { type: Boolean, default: false }
|
13
|
+
}
|
14
|
+
|
11
15
|
connect() {
|
12
16
|
console.debug("[Panda CMS] Slug handler connected...");
|
13
17
|
// Generate path on initial load if title exists
|
@@ -17,29 +21,27 @@ export default class extends Controller {
|
|
17
21
|
}
|
18
22
|
|
19
23
|
generatePath() {
|
20
|
-
|
21
|
-
|
22
|
-
// For posts, we want to store just the slug part
|
23
|
-
const prefix = this.output_textTarget.dataset.prefix || "";
|
24
|
-
this.output_textTarget.value = "/" + slug;
|
24
|
+
const title = this.input_textTarget.value;
|
25
|
+
if (!title) return;
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
span.className = 'prefix';
|
32
|
-
this.output_textTarget.parentNode.insertBefore(span, this.output_textTarget);
|
33
|
-
return span;
|
34
|
-
})();
|
35
|
-
prefixSpan.textContent = prefix;
|
36
|
-
}
|
27
|
+
// Convert title to slug format
|
28
|
+
const slug = title
|
29
|
+
.toLowerCase()
|
30
|
+
.replace(/[^a-z0-9]+/g, "-")
|
31
|
+
.replace(/^-+|-+$/g, "");
|
37
32
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
33
|
+
// Only add year/month prefix for posts
|
34
|
+
if (this.addDatePrefixValue) {
|
35
|
+
// Get current date for year/month
|
36
|
+
const now = new Date();
|
37
|
+
const year = now.getFullYear();
|
38
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
39
|
+
|
40
|
+
// Add leading slash and use date format
|
41
|
+
this.output_textTarget.value = `/${year}/${month}/${slug}`;
|
42
|
+
} else {
|
43
|
+
// Add leading slash for regular pages
|
44
|
+
this.output_textTarget.value = `/${slug}`;
|
43
45
|
}
|
44
46
|
}
|
45
47
|
|
@@ -49,37 +51,25 @@ export default class extends Controller {
|
|
49
51
|
if (match) {
|
50
52
|
this.parent_slugs = match[1];
|
51
53
|
const prePath = (this.existing_rootTarget.value + this.parent_slugs).replace(/\/$/, "");
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
}
|
56
|
-
console.log("Have set the pre-path to: " + prePath);
|
54
|
+
// Ensure we don't double up slashes
|
55
|
+
const currentPath = this.output_textTarget.value.replace(/^\//, "");
|
56
|
+
this.output_textTarget.value = `${prePath}/${currentPath}`;
|
57
57
|
}
|
58
|
-
} catch (
|
59
|
-
console.error("Error setting pre-path:",
|
58
|
+
} catch (e) {
|
59
|
+
console.error("[Panda CMS] Error setting pre-path:", e);
|
60
60
|
}
|
61
61
|
}
|
62
62
|
|
63
|
-
// TODO: Invoke a library or helper which can be shared with the backend
|
64
|
-
// and check for uniqueness at the same time
|
65
63
|
createSlug(input) {
|
66
|
-
|
67
|
-
|
68
|
-
var str = input
|
64
|
+
return input
|
69
65
|
.toLowerCase()
|
70
|
-
.
|
71
|
-
.replace(
|
72
|
-
.replace(/&/g, "and")
|
73
|
-
.replace(/[\s_-]+/g, "-")
|
74
|
-
.trim();
|
75
|
-
|
76
|
-
return this.trimStartEnd(str, "-");
|
66
|
+
.replace(/[^a-z0-9]+/g, "-")
|
67
|
+
.replace(/^-+|-+$/g, "");
|
77
68
|
}
|
78
69
|
|
79
70
|
trimStartEnd(str, ch) {
|
80
|
-
|
81
|
-
|
82
|
-
|
71
|
+
let start = 0,
|
72
|
+
end = str.length;
|
83
73
|
while (start < end && str[start] === ch) ++start;
|
84
74
|
while (end > start && str[end - 1] === ch) --end;
|
85
75
|
return start > 0 || end < str.length ? str.substring(start, end) : str;
|
@@ -15,69 +15,177 @@ if (window.PANDA_CMS_EDITOR_JS_RESOURCES) {
|
|
15
15
|
}
|
16
16
|
|
17
17
|
export const EDITOR_JS_CSS = `
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
}
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
.ce-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
}
|
18
|
+
.codex-editor {
|
19
|
+
position: relative;
|
20
|
+
}
|
21
|
+
.codex-editor::before {
|
22
|
+
content: '';
|
23
|
+
position: absolute;
|
24
|
+
left: 0;
|
25
|
+
top: 0;
|
26
|
+
bottom: 0;
|
27
|
+
width: 65px;
|
28
|
+
margin-right: 5px;
|
29
|
+
background-color: #f9fafb;
|
30
|
+
border-right: 2px dashed #e5e7eb;
|
31
|
+
z-index: 0;
|
32
|
+
}
|
33
|
+
.ce-block {
|
34
|
+
padding-left: 70px;
|
35
|
+
position: relative;
|
36
|
+
min-height: 40px;
|
37
|
+
margin: 0;
|
38
|
+
padding-bottom: 1em;
|
39
|
+
}
|
40
|
+
.ce-block__content {
|
41
|
+
position: relative;
|
42
|
+
max-width: none;
|
43
|
+
margin: 0;
|
44
|
+
}
|
45
|
+
.ce-paragraph {
|
46
|
+
padding: 0;
|
47
|
+
line-height: 1.6;
|
48
|
+
min-height: 1.6em;
|
49
|
+
margin: 0;
|
50
|
+
}
|
51
|
+
/* Override inherited heading styles */
|
52
|
+
.ce-header h1,
|
53
|
+
.ce-header h2,
|
54
|
+
.ce-header h3,
|
55
|
+
.ce-header h4,
|
56
|
+
.ce-header h5,
|
57
|
+
.ce-header h6 {
|
58
|
+
margin: 0;
|
59
|
+
padding: 0;
|
60
|
+
line-height: 1.6;
|
61
|
+
font-weight: 600;
|
62
|
+
}
|
63
|
+
.ce-header h1 { font-size: 2em; }
|
64
|
+
.ce-header h2 { font-size: 1.5em; }
|
65
|
+
.ce-header h3 { font-size: 1.17em; }
|
66
|
+
.ce-header h4 { font-size: 1em; }
|
67
|
+
.ce-header h5 { font-size: 0.83em; }
|
68
|
+
.ce-header h6 { font-size: 0.67em; }
|
65
69
|
|
66
|
-
.
|
67
|
-
|
68
|
-
|
69
|
-
}
|
70
|
+
.codex-editor__redactor {
|
71
|
+
padding-bottom: 150px !important;
|
72
|
+
min-height: 100px !important;
|
73
|
+
}
|
74
|
+
/* Base toolbar styles */
|
75
|
+
.ce-toolbar {
|
76
|
+
left: 0 !important;
|
77
|
+
right: auto !important;
|
78
|
+
background: none !important;
|
79
|
+
position: absolute !important;
|
80
|
+
width: 65px !important;
|
81
|
+
height: 40px !important;
|
82
|
+
display: flex !important;
|
83
|
+
align-items: center !important;
|
84
|
+
justify-content: flex-start !important;
|
85
|
+
padding: 0 !important;
|
86
|
+
margin-left: -70px !important;
|
87
|
+
margin-top: -5px !important;
|
88
|
+
opacity: 1 !important;
|
89
|
+
visibility: visible !important;
|
90
|
+
pointer-events: all !important;
|
91
|
+
z-index: 2 !important;
|
92
|
+
}
|
93
|
+
/* Ensure toolbar is visible for all blocks */
|
94
|
+
.ce-block .ce-toolbar {
|
95
|
+
display: flex !important;
|
96
|
+
opacity: 1 !important;
|
97
|
+
visibility: visible !important;
|
98
|
+
}
|
99
|
+
.ce-toolbar__content {
|
100
|
+
max-width: none;
|
101
|
+
left: 70px !important;
|
102
|
+
display: flex !important;
|
103
|
+
position: relative !important;
|
104
|
+
}
|
105
|
+
.ce-toolbar__actions {
|
106
|
+
position: relative !important;
|
107
|
+
left: 5px !important;
|
108
|
+
opacity: 1 !important;
|
109
|
+
visibility: visible !important;
|
110
|
+
background: transparent !important;
|
111
|
+
z-index: 2;
|
112
|
+
display: flex !important;
|
113
|
+
align-items: center !important;
|
114
|
+
gap: 5px !important;
|
115
|
+
height: 40px !important;
|
116
|
+
padding: 0 !important;
|
117
|
+
}
|
118
|
+
.ce-toolbar__plus {
|
119
|
+
position: relative !important;
|
120
|
+
left: 0px !important;
|
121
|
+
opacity: 1 !important;
|
122
|
+
visibility: visible !important;
|
123
|
+
background: transparent !important;
|
124
|
+
border: none !important;
|
125
|
+
z-index: 2;
|
126
|
+
display: block !important;
|
127
|
+
}
|
128
|
+
.ce-toolbar__settings-btn {
|
129
|
+
position: relative !important;
|
130
|
+
left: -10px !important;
|
131
|
+
opacity: 1 !important;
|
132
|
+
visibility: visible !important;
|
133
|
+
background: transparent !important;
|
134
|
+
border: none !important;
|
135
|
+
z-index: 2;
|
136
|
+
display: block !important;
|
137
|
+
}
|
138
|
+
/* Style the search input */
|
139
|
+
.ce-popover__search {
|
140
|
+
padding-left: 3px !important;
|
141
|
+
}
|
142
|
+
.ce-popover__search input {
|
143
|
+
outline: none !important;
|
144
|
+
box-shadow: none !important;
|
145
|
+
border: none !important;
|
146
|
+
}
|
147
|
+
.ce-popover__search input::placeholder {
|
148
|
+
content: 'Search';
|
149
|
+
}
|
150
|
+
/* Ensure popups still work */
|
151
|
+
.ce-popover {
|
152
|
+
z-index: 4;
|
153
|
+
}
|
154
|
+
.ce-inline-toolbar {
|
155
|
+
z-index: 3;
|
156
|
+
}
|
157
|
+
/* Override any hiding behavior */
|
158
|
+
.ce-toolbar--closed,
|
159
|
+
.ce-toolbar--opened,
|
160
|
+
.ce-toolbar--showed {
|
161
|
+
display: flex !important;
|
162
|
+
opacity: 1 !important;
|
163
|
+
visibility: visible !important;
|
164
|
+
}
|
165
|
+
/* Force toolbar to show on every block */
|
166
|
+
.ce-block:not(:focus):not(:hover) .ce-toolbar,
|
167
|
+
.ce-block--selected .ce-toolbar,
|
168
|
+
.ce-block--focused .ce-toolbar,
|
169
|
+
.ce-block--hover .ce-toolbar {
|
170
|
+
opacity: 1 !important;
|
171
|
+
visibility: visible !important;
|
172
|
+
display: flex !important;
|
173
|
+
}
|
70
174
|
|
71
|
-
/* Ensure
|
72
|
-
.ce-
|
73
|
-
|
74
|
-
}
|
175
|
+
/* Ensure last block has bottom spacing */
|
176
|
+
.ce-block:last-child {
|
177
|
+
padding-bottom: 2em;
|
178
|
+
}
|
75
179
|
|
76
|
-
/*
|
77
|
-
.ce-
|
78
|
-
|
79
|
-
|
80
|
-
|
180
|
+
/* Reset all block type margins */
|
181
|
+
.ce-header,
|
182
|
+
.ce-paragraph,
|
183
|
+
.ce-quote,
|
184
|
+
.ce-list {
|
185
|
+
margin: 0 !important;
|
186
|
+
padding: 0 !important;
|
187
|
+
}
|
188
|
+
`
|
81
189
|
|
82
190
|
export const getEditorConfig = (elementId, previousData, doc = document) => {
|
83
191
|
// Validate holder element exists
|
@@ -86,14 +194,34 @@ export const getEditorConfig = (elementId, previousData, doc = document) => {
|
|
86
194
|
throw new Error(`Editor holder element ${elementId} not found`)
|
87
195
|
}
|
88
196
|
|
197
|
+
// Get the correct window context
|
198
|
+
const win = doc.defaultView || window
|
199
|
+
|
200
|
+
// Ensure we have a clean holder element
|
201
|
+
holder.innerHTML = ""
|
202
|
+
|
89
203
|
const config = {
|
90
204
|
holder: elementId,
|
91
205
|
data: previousData || {},
|
92
206
|
placeholder: 'Click the + button to add content...',
|
93
207
|
inlineToolbar: true,
|
208
|
+
onChange: () => {
|
209
|
+
// Ensure the editor is properly initialized before handling changes
|
210
|
+
if (holder && holder.querySelector('.codex-editor')) {
|
211
|
+
const event = new Event('editor:change', { bubbles: true })
|
212
|
+
holder.dispatchEvent(event)
|
213
|
+
}
|
214
|
+
},
|
215
|
+
i18n: {
|
216
|
+
toolbar: {
|
217
|
+
filter: {
|
218
|
+
placeholder: 'Search'
|
219
|
+
}
|
220
|
+
}
|
221
|
+
},
|
94
222
|
tools: {
|
95
223
|
header: {
|
96
|
-
class:
|
224
|
+
class: win.Header,
|
97
225
|
inlineToolbar: true,
|
98
226
|
config: {
|
99
227
|
placeholder: 'Enter a header',
|
@@ -102,44 +230,45 @@ export const getEditorConfig = (elementId, previousData, doc = document) => {
|
|
102
230
|
}
|
103
231
|
},
|
104
232
|
paragraph: {
|
105
|
-
class:
|
233
|
+
class: win.Paragraph,
|
106
234
|
inlineToolbar: true,
|
107
235
|
config: {
|
108
236
|
placeholder: 'Start writing or press Tab to add content...'
|
109
237
|
}
|
110
238
|
},
|
111
239
|
list: {
|
112
|
-
class:
|
240
|
+
class: win.NestedList,
|
113
241
|
inlineToolbar: true,
|
114
242
|
config: {
|
115
|
-
defaultStyle: 'unordered'
|
243
|
+
defaultStyle: 'unordered',
|
244
|
+
enableLineBreaks: true
|
116
245
|
}
|
117
246
|
},
|
118
247
|
quote: {
|
119
|
-
class:
|
248
|
+
class: win.Quote,
|
120
249
|
inlineToolbar: true,
|
121
250
|
config: {
|
122
251
|
quotePlaceholder: 'Enter a quote',
|
123
252
|
captionPlaceholder: 'Quote\'s author'
|
124
253
|
}
|
125
254
|
},
|
126
|
-
|
127
|
-
class:
|
255
|
+
image: {
|
256
|
+
class: win.SimpleImage,
|
128
257
|
inlineToolbar: true,
|
129
258
|
config: {
|
130
|
-
|
131
|
-
cols: 2
|
259
|
+
placeholder: 'Paste an image URL...'
|
132
260
|
}
|
133
261
|
},
|
134
|
-
|
135
|
-
class:
|
262
|
+
table: {
|
263
|
+
class: win.Table,
|
136
264
|
inlineToolbar: true,
|
137
265
|
config: {
|
138
|
-
|
266
|
+
rows: 2,
|
267
|
+
cols: 2
|
139
268
|
}
|
140
269
|
},
|
141
270
|
embed: {
|
142
|
-
class:
|
271
|
+
class: win.Embed,
|
143
272
|
inlineToolbar: true,
|
144
273
|
config: {
|
145
274
|
services: {
|