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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/panda.cms.css +0 -50
  3. data/app/components/panda/cms/admin/table_component.html.erb +4 -1
  4. data/app/components/panda/cms/code_component.rb +2 -1
  5. data/app/components/panda/cms/rich_text_component.html.erb +86 -2
  6. data/app/components/panda/cms/rich_text_component.rb +131 -20
  7. data/app/controllers/panda/cms/admin/block_contents_controller.rb +18 -7
  8. data/app/controllers/panda/cms/admin/files_controller.rb +22 -12
  9. data/app/controllers/panda/cms/admin/posts_controller.rb +33 -11
  10. data/app/controllers/panda/cms/pages_controller.rb +29 -0
  11. data/app/controllers/panda/cms/posts_controller.rb +26 -4
  12. data/app/helpers/panda/cms/admin/posts_helper.rb +23 -32
  13. data/app/helpers/panda/cms/posts_helper.rb +32 -0
  14. data/app/javascript/panda/cms/controllers/dashboard_controller.js +0 -1
  15. data/app/javascript/panda/cms/controllers/editor_form_controller.js +134 -11
  16. data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +395 -130
  17. data/app/javascript/panda/cms/controllers/slug_controller.js +33 -43
  18. data/app/javascript/panda/cms/editor/editor_js_config.js +202 -73
  19. data/app/javascript/panda/cms/editor/editor_js_initializer.js +243 -194
  20. data/app/javascript/panda/cms/editor/plain_text_editor.js +1 -1
  21. data/app/javascript/panda/cms/editor/resource_loader.js +89 -0
  22. data/app/javascript/panda/cms/editor/rich_text_editor.js +162 -0
  23. data/app/models/panda/cms/page.rb +18 -0
  24. data/app/models/panda/cms/post.rb +61 -3
  25. data/app/models/panda/cms/redirect.rb +2 -2
  26. data/app/views/panda/cms/admin/posts/_form.html.erb +15 -4
  27. data/app/views/panda/cms/admin/posts/index.html.erb +5 -3
  28. data/config/routes.rb +34 -6
  29. data/db/migrate/20250106223303_add_author_id_to_panda_cms_posts.rb +5 -0
  30. data/lib/panda/cms/editor_js_content.rb +14 -1
  31. data/lib/panda/cms/engine.rb +4 -0
  32. data/lib/panda-cms/version.rb +1 -1
  33. 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
- try {
21
- const slug = this.createSlug(this.input_textTarget.value);
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
- // If there's a prefix, show it in the UI but don't include it in the value
27
- if (prefix) {
28
- const prefixSpan = this.output_textTarget.previousElementSibling ||
29
- (() => {
30
- const span = document.createElement('span');
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
- console.log("Have set the path to: " + this.output_textTarget.value);
39
- } catch (error) {
40
- console.error("Error generating path:", error);
41
- // Add error class to path field
42
- this.output_textTarget.classList.add("error");
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
- const prefixSpan = this.output_textTarget.previousElementSibling;
53
- if (prefixSpan) {
54
- prefixSpan.textContent = prePath;
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 (error) {
59
- console.error("Error setting pre-path:", error);
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
- if (!input) return "";
67
-
68
- var str = input
64
+ return input
69
65
  .toLowerCase()
70
- .trim()
71
- .replace(/[^\w\s-]/g, "-")
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
- var start = 0;
81
- var end = str.length;
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
- /* Editor layout styles */
19
- .ce-toolbar__content {
20
- margin: 0 !important;
21
- margin-left: 40px;
22
- max-width: 100% !important;
23
- width: 100% !important;
24
- }
25
-
26
- .ce-block__content {
27
- max-width: 100%;
28
- margin: 0 !important;
29
- margin-left: 10px !important;
30
- }
31
-
32
- /* Ensure proper nesting for content styles to apply */
33
- .codex-editor .codex-editor__redactor {
34
- position: relative;
35
- }
36
-
37
- .codex-editor .codex-editor__redactor .ce-block {
38
- position: relative;
39
- }
40
-
41
- .codex-editor .codex-editor__redactor .ce-block .ce-block__content {
42
- position: relative;
43
- }
44
-
45
- /* Remove default editor styles that might interfere */
46
- .ce-header {
47
- padding: 0 !important;
48
- margin: 0 !important;
49
- background: none !important;
50
- border: none !important;
51
- }
52
-
53
- .ce-paragraph {
54
- padding: 0 !important;
55
- margin: 0 !important;
56
- line-height: inherit !important;
57
- }
58
-
59
- /* Lists */
60
- .ce-block--list ul,
61
- .ce-block--list ol {
62
- margin: 0;
63
- padding-left: inherit;
64
- }
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
- .ce-block--list li {
67
- margin: 0;
68
- padding-left: inherit;
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 editor toolbar is above content */
72
- .ce-toolbar {
73
- z-index: 100;
74
- }
175
+ /* Ensure last block has bottom spacing */
176
+ .ce-block:last-child {
177
+ padding-bottom: 2em;
178
+ }
75
179
 
76
- /* Style the block selection */
77
- .ce-block--selected {
78
- background-color: rgba(16, 64, 113, 0.05);
79
- border-radius: 4px;
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: window.Header,
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: window.Paragraph,
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: window.NestedList,
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: window.Quote,
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
- table: {
127
- class: window.Table,
255
+ image: {
256
+ class: win.SimpleImage,
128
257
  inlineToolbar: true,
129
258
  config: {
130
- rows: 2,
131
- cols: 2
259
+ placeholder: 'Paste an image URL...'
132
260
  }
133
261
  },
134
- image: {
135
- class: window.SimpleImage,
262
+ table: {
263
+ class: win.Table,
136
264
  inlineToolbar: true,
137
265
  config: {
138
- placeholder: 'Paste an image URL...'
266
+ rows: 2,
267
+ cols: 2
139
268
  }
140
269
  },
141
270
  embed: {
142
- class: window.Embed,
271
+ class: win.Embed,
143
272
  inlineToolbar: true,
144
273
  config: {
145
274
  services: {