panda-cms 0.7.0 → 0.7.3
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 +0 -50
- data/app/components/panda/cms/admin/table_component.html.erb +1 -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 +3 -6
- data/lib/panda-cms/version.rb +1 -1
- data/lib/panda-cms.rb +5 -11
- metadata +290 -35
@@ -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: {
|