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.
Files changed (34) 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 +1 -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 +3 -6
  32. data/lib/panda-cms/version.rb +1 -1
  33. data/lib/panda-cms.rb +5 -11
  34. 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
- 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: {