panda-cms 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +73 -0
- data/Rakefile +7 -0
- data/app/assets/builds/panda.cms.css +2808 -0
- data/app/assets/config/panda_cms_manifest.js +4 -0
- data/app/assets/stylesheets/panda/cms/application.tailwind.css +162 -0
- data/app/assets/stylesheets/panda/cms/editor.css +120 -0
- data/app/builders/panda/cms/form_builder.rb +234 -0
- data/app/components/panda/cms/admin/button_component.rb +70 -0
- data/app/components/panda/cms/admin/container_component.html.erb +13 -0
- data/app/components/panda/cms/admin/container_component.rb +13 -0
- data/app/components/panda/cms/admin/flash_message_component.html.erb +31 -0
- data/app/components/panda/cms/admin/flash_message_component.rb +47 -0
- data/app/components/panda/cms/admin/heading_component.rb +45 -0
- data/app/components/panda/cms/admin/panel_component.html.erb +7 -0
- data/app/components/panda/cms/admin/panel_component.rb +13 -0
- data/app/components/panda/cms/admin/slideover_component.html.erb +9 -0
- data/app/components/panda/cms/admin/slideover_component.rb +15 -0
- data/app/components/panda/cms/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/cms/admin/statistics_component.rb +17 -0
- data/app/components/panda/cms/admin/tab_bar_component.html.erb +35 -0
- data/app/components/panda/cms/admin/tab_bar_component.rb +15 -0
- data/app/components/panda/cms/admin/table_component.html.erb +29 -0
- data/app/components/panda/cms/admin/table_component.rb +46 -0
- data/app/components/panda/cms/admin/tag_component.rb +35 -0
- data/app/components/panda/cms/admin/user_activity_component.html.erb +5 -0
- data/app/components/panda/cms/admin/user_activity_component.rb +33 -0
- data/app/components/panda/cms/admin/user_display_component.html.erb +17 -0
- data/app/components/panda/cms/admin/user_display_component.rb +21 -0
- data/app/components/panda/cms/code_component.rb +64 -0
- data/app/components/panda/cms/grid_component.html.erb +6 -0
- data/app/components/panda/cms/grid_component.rb +15 -0
- data/app/components/panda/cms/menu_component.html.erb +6 -0
- data/app/components/panda/cms/menu_component.rb +58 -0
- data/app/components/panda/cms/page_menu_component.html.erb +21 -0
- data/app/components/panda/cms/page_menu_component.rb +38 -0
- data/app/components/panda/cms/rich_text_component.html.erb +6 -0
- data/app/components/panda/cms/rich_text_component.rb +84 -0
- data/app/components/panda/cms/text_component.rb +72 -0
- data/app/constraints/panda/cms/admin_constraint.rb +18 -0
- data/app/controllers/panda/cms/admin/block_contents_controller.rb +52 -0
- data/app/controllers/panda/cms/admin/dashboard_controller.rb +20 -0
- data/app/controllers/panda/cms/admin/files_controller.rb +21 -0
- data/app/controllers/panda/cms/admin/forms_controller.rb +53 -0
- data/app/controllers/panda/cms/admin/menus_controller.rb +30 -0
- data/app/controllers/panda/cms/admin/pages_controller.rb +91 -0
- data/app/controllers/panda/cms/admin/posts_controller.rb +146 -0
- data/app/controllers/panda/cms/admin/sessions_controller.rb +94 -0
- data/app/controllers/panda/cms/admin/settings/bulk_editor_controller.rb +37 -0
- data/app/controllers/panda/cms/admin/settings_controller.rb +20 -0
- data/app/controllers/panda/cms/application_controller.rb +57 -0
- data/app/controllers/panda/cms/errors_controller.rb +33 -0
- data/app/controllers/panda/cms/form_submissions_controller.rb +23 -0
- data/app/controllers/panda/cms/pages_controller.rb +72 -0
- data/app/controllers/panda/cms/posts_controller.rb +13 -0
- data/app/helpers/panda/cms/admin/files_helper.rb +6 -0
- data/app/helpers/panda/cms/admin/pages_helper.rb +6 -0
- data/app/helpers/panda/cms/admin/posts_helper.rb +48 -0
- data/app/helpers/panda/cms/application_helper.rb +120 -0
- data/app/helpers/panda/cms/pages_helper.rb +6 -0
- data/app/helpers/panda/cms/theme_helper.rb +18 -0
- data/app/javascript/panda/cms/@editorjs--editorjs.js +2577 -0
- data/app/javascript/panda/cms/@hotwired--stimulus.js +4 -0
- data/app/javascript/panda/cms/@hotwired--turbo.js +160 -0
- data/app/javascript/panda/cms/@rails--actioncable--src.js +4 -0
- data/app/javascript/panda/cms/application_panda_cms.js +39 -0
- data/app/javascript/panda/cms/controllers/dashboard_controller.js +7 -0
- data/app/javascript/panda/cms/controllers/editor_form_controller.js +77 -0
- data/app/javascript/panda/cms/controllers/editor_iframe_controller.js +320 -0
- data/app/javascript/panda/cms/controllers/index.js +48 -0
- data/app/javascript/panda/cms/controllers/slug_controller.js +87 -0
- data/app/javascript/panda/cms/editor/css_extractor.js +80 -0
- data/app/javascript/panda/cms/editor/editor_js_config.js +177 -0
- data/app/javascript/panda/cms/editor/editor_js_initializer.js +285 -0
- data/app/javascript/panda/cms/editor/plain_text_editor.js +110 -0
- data/app/javascript/panda/cms/editor/resource_loader.js +115 -0
- data/app/javascript/panda/cms/tailwindcss-stimulus-components.js +4 -0
- data/app/jobs/panda/cms/application_job.rb +6 -0
- data/app/jobs/panda/cms/record_visit_job.rb +31 -0
- data/app/mailers/panda/cms/application_mailer.rb +8 -0
- data/app/mailers/panda/cms/form_mailer.rb +21 -0
- data/app/models/action_text/rich_text_version.rb +6 -0
- data/app/models/panda/cms/application_record.rb +7 -0
- data/app/models/panda/cms/block.rb +34 -0
- data/app/models/panda/cms/block_content.rb +18 -0
- data/app/models/panda/cms/block_content_version.rb +8 -0
- data/app/models/panda/cms/breadcrumb.rb +12 -0
- data/app/models/panda/cms/current.rb +17 -0
- data/app/models/panda/cms/form.rb +9 -0
- data/app/models/panda/cms/form_submission.rb +7 -0
- data/app/models/panda/cms/menu.rb +52 -0
- data/app/models/panda/cms/menu_item.rb +58 -0
- data/app/models/panda/cms/page.rb +96 -0
- data/app/models/panda/cms/page_version.rb +8 -0
- data/app/models/panda/cms/post.rb +60 -0
- data/app/models/panda/cms/post_version.rb +8 -0
- data/app/models/panda/cms/redirect.rb +11 -0
- data/app/models/panda/cms/template.rb +124 -0
- data/app/models/panda/cms/template_version.rb +8 -0
- data/app/models/panda/cms/user.rb +31 -0
- data/app/models/panda/cms/version.rb +8 -0
- data/app/models/panda/cms/visit.rb +9 -0
- data/app/services/panda/cms/html_to_editor_js_converter.rb +200 -0
- data/app/views/active_storage/blobs/blobs/_blob.html.erb +14 -0
- data/app/views/layouts/action_text/contents/_content.html.erb +3 -0
- data/app/views/layouts/panda/cms/application.html.erb +41 -0
- data/app/views/layouts/panda/cms/public.html.erb +3 -0
- data/app/views/panda/cms/admin/dashboard/show.html.erb +12 -0
- data/app/views/panda/cms/admin/files/index.html.erb +124 -0
- data/app/views/panda/cms/admin/files/show.html.erb +2 -0
- data/app/views/panda/cms/admin/forms/edit.html.erb +0 -0
- data/app/views/panda/cms/admin/forms/index.html.erb +13 -0
- data/app/views/panda/cms/admin/forms/new.html.erb +15 -0
- data/app/views/panda/cms/admin/forms/show.html.erb +35 -0
- data/app/views/panda/cms/admin/menus/index.html.erb +8 -0
- data/app/views/panda/cms/admin/pages/edit.html.erb +36 -0
- data/app/views/panda/cms/admin/pages/index.html.erb +22 -0
- data/app/views/panda/cms/admin/pages/new.html.erb +15 -0
- data/app/views/panda/cms/admin/pages/show.html.erb +1 -0
- data/app/views/panda/cms/admin/posts/_form.html.erb +29 -0
- data/app/views/panda/cms/admin/posts/edit.html.erb +6 -0
- data/app/views/panda/cms/admin/posts/index.html.erb +18 -0
- data/app/views/panda/cms/admin/posts/new.html.erb +6 -0
- data/app/views/panda/cms/admin/sessions/new.html.erb +17 -0
- data/app/views/panda/cms/admin/settings/bulk_editor/new.html.erb +68 -0
- data/app/views/panda/cms/admin/settings/index.html.erb +21 -0
- data/app/views/panda/cms/admin/settings/insta.html +4 -0
- data/app/views/panda/cms/admin/shared/_breadcrumbs.html.erb +28 -0
- data/app/views/panda/cms/admin/shared/_flash.html.erb +5 -0
- data/app/views/panda/cms/admin/shared/_sidebar.html.erb +41 -0
- data/app/views/panda/cms/form_mailer/notification_email.html.erb +11 -0
- data/app/views/panda/cms/shared/_editor.html.erb +0 -0
- data/app/views/panda/cms/shared/_favicons.html.erb +9 -0
- data/app/views/panda/cms/shared/_footer.html.erb +2 -0
- data/app/views/panda/cms/shared/_header.html.erb +15 -0
- data/app/views/panda/cms/shared/_importmap.html.erb +33 -0
- data/config/importmap.rb +13 -0
- data/config/initializers/inflections.rb +3 -0
- data/config/initializers/panda/cms/form_errors.rb +38 -0
- data/config/initializers/panda/cms/healthcheck_log_silencer.rb +11 -0
- data/config/initializers/panda/cms/paper_trail.rb +7 -0
- data/config/initializers/panda/cms.rb +10 -0
- data/config/initializers/zeitwork.rb +3 -0
- data/config/locales/en.yml +49 -0
- data/config/puma/test.rb +9 -0
- data/config/routes.rb +48 -0
- data/config/tailwind.config.js +37 -0
- data/db/migrate/20240205223709_create_panda_cms_pages.rb +9 -0
- data/db/migrate/20240219213327_create_panda_cms_page_versions.rb +14 -0
- data/db/migrate/20240303002805_create_panda_cms_templates.rb +11 -0
- data/db/migrate/20240303003434_create_panda_cms_template_versions.rb +14 -0
- data/db/migrate/20240303022441_create_panda_cms_blocks.rb +13 -0
- data/db/migrate/20240303024256_create_panda_cms_block_contents.rb +10 -0
- data/db/migrate/20240303024746_create_panda_cms_block_content_versions.rb +14 -0
- data/db/migrate/20240303233238_add_panda_cms_menu_table.rb +10 -0
- data/db/migrate/20240303234724_add_panda_cms_menu_item_table.rb +12 -0
- data/db/migrate/20240304134343_add_parent_id_to_panda_cms_pages.rb +5 -0
- data/db/migrate/20240305000000_convert_html_content_to_editor_js.rb +82 -0
- data/db/migrate/20240315125411_add_status_to_panda_cms_pages.rb +9 -0
- data/db/migrate/20240315125421_add_nested_sets_to_panda_cms_pages.rb +16 -0
- data/db/migrate/20240316212822_add_kind_to_panda_cms_menus.rb +6 -0
- data/db/migrate/20240316221425_add_start_page_to_panda_cms_menus.rb +5 -0
- data/db/migrate/20240316230706_add_nested_to_panda_cms_menu_items.rb +24 -0
- data/db/migrate/20240317010532_create_panda_cms_users.rb +12 -0
- data/db/migrate/20240317161534_add_max_uses_to_panda_cms_template.rb +7 -0
- data/db/migrate/20240317163053_reset_counter_cache_on_panda_cms_template.rb +5 -0
- data/db/migrate/20240317214827_create_panda_cms_redirects.rb +14 -0
- data/db/migrate/20240317230622_create_panda_cms_visits.rb +13 -0
- data/db/migrate/20240324205703_create_active_storage_tables.active_storage.rb +58 -0
- data/db/migrate/20240408084718_default_panda_cms_users_admin_to_false.rb +5 -0
- data/db/migrate/20240701225422_add_service_name_to_active_storage_blobs.active_storage.rb +22 -0
- data/db/migrate/20240701225423_create_active_storage_variant_records.active_storage.rb +28 -0
- data/db/migrate/20240701225424_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +8 -0
- data/db/migrate/20240804235210_create_panda_cms_forms.rb +11 -0
- data/db/migrate/20240805013612_create_panda_cms_form_submissions.rb +9 -0
- data/db/migrate/20240805121123_create_panda_cms_posts.rb +27 -0
- data/db/migrate/20240805123104_create_panda_cms_post_versions.rb +14 -0
- data/db/migrate/20240806112735_fix_panda_cms_visits_column_names.rb +13 -0
- data/db/migrate/20240806204412_add_completion_path_to_panda_cms_forms.rb +5 -0
- data/db/migrate/20240820081917_change_form_submissions_to_submission_count.rb +5 -0
- data/db/migrate/20240904200605_create_action_text_tables.action_text.rb +24 -0
- data/db/migrate/20240923234535_add_depth_to_panda_cms_menus.rb +11 -0
- data/db/migrate/20241031205109_add_cached_content_to_panda_cms_block_contents.rb +5 -0
- data/db/migrate/20241119214548_convert_post_content_to_editor_js.rb +35 -0
- data/db/migrate/20241119214549_remove_action_text_from_posts.rb +9 -0
- data/db/migrate/20241120000419_remove_post_tag_references.rb +19 -0
- data/db/migrate/20241120110943_add_editor_js_to_posts.rb +27 -0
- data/db/migrate/20241120113859_add_cached_content_to_panda_cms_posts.rb +5 -0
- data/db/migrate/20241123234140_remove_post_tag_id_from_posts.rb +5 -0
- data/db/migrate/migrate +1 -0
- data/db/seeds.rb +5 -0
- data/lib/generators/panda/cms/install_generator.rb +29 -0
- data/lib/panda/cms/bulk_editor.rb +171 -0
- data/lib/panda/cms/demo_site_generator.rb +67 -0
- data/lib/panda/cms/editor_js/blocks/alert.rb +34 -0
- data/lib/panda/cms/editor_js/blocks/base.rb +33 -0
- data/lib/panda/cms/editor_js/blocks/header.rb +15 -0
- data/lib/panda/cms/editor_js/blocks/image.rb +36 -0
- data/lib/panda/cms/editor_js/blocks/list.rb +32 -0
- data/lib/panda/cms/editor_js/blocks/paragraph.rb +15 -0
- data/lib/panda/cms/editor_js/blocks/quote.rb +41 -0
- data/lib/panda/cms/editor_js/blocks/table.rb +50 -0
- data/lib/panda/cms/editor_js/renderer.rb +124 -0
- data/lib/panda/cms/editor_js.rb +16 -0
- data/lib/panda/cms/editor_js_content.rb +21 -0
- data/lib/panda/cms/engine.rb +257 -0
- data/lib/panda/cms/exceptions_app.rb +26 -0
- data/lib/panda/cms/railtie.rb +11 -0
- data/lib/panda/cms/slug.rb +24 -0
- data/lib/panda/cms.rb +0 -0
- data/lib/panda-cms/version.rb +5 -0
- data/lib/panda-cms.rb +81 -0
- data/lib/tasks/panda_cms.rake +54 -0
- data/lib/templates/erb/scaffold/_form.html.erb.tt +43 -0
- data/lib/templates/erb/scaffold/edit.html.erb.tt +8 -0
- data/lib/templates/erb/scaffold/index.html.erb.tt +14 -0
- data/lib/templates/erb/scaffold/new.html.erb.tt +7 -0
- data/lib/templates/erb/scaffold/partial.html.erb.tt +22 -0
- data/lib/templates/erb/scaffold/show.html.erb.tt +15 -0
- data/public/panda-cms-assets/favicons/android-chrome-192x192.png +0 -0
- data/public/panda-cms-assets/favicons/android-chrome-512x512.png +0 -0
- data/public/panda-cms-assets/favicons/apple-touch-icon.png +0 -0
- data/public/panda-cms-assets/favicons/browserconfig.xml +9 -0
- data/public/panda-cms-assets/favicons/favicon-16x16.png +0 -0
- data/public/panda-cms-assets/favicons/favicon-32x32.png +0 -0
- data/public/panda-cms-assets/favicons/favicon.ico +0 -0
- data/public/panda-cms-assets/favicons/mstile-150x150.png +0 -0
- data/public/panda-cms-assets/favicons/safari-pinned-tab.svg +61 -0
- data/public/panda-cms-assets/favicons/site.webmanifest +14 -0
- data/public/panda-cms-assets/panda-logo-screenprint.png +0 -0
- data/public/panda-cms-assets/panda-nav.png +0 -0
- data/public/panda-cms-assets/rich_text_editor.css +568 -0
- metadata +654 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["editorContainer", "hiddenField"];
|
5
|
+
static values = {
|
6
|
+
editorId: String,
|
7
|
+
};
|
8
|
+
|
9
|
+
connect() {
|
10
|
+
this.initializeEditor();
|
11
|
+
}
|
12
|
+
|
13
|
+
async initializeEditor() {
|
14
|
+
if (this.editor) return;
|
15
|
+
|
16
|
+
try {
|
17
|
+
const holderId =
|
18
|
+
this.editorIdValue + "_holder" ||
|
19
|
+
`editor-${Math.random().toString(36).substring(2, 9)}`;
|
20
|
+
let holderDiv = document.createElement("div");
|
21
|
+
holderDiv.id = holderId;
|
22
|
+
holderDiv.className = "codex-editor";
|
23
|
+
this.editorContainerTarget.innerHTML = "";
|
24
|
+
this.editorContainerTarget.appendChild(holderDiv);
|
25
|
+
|
26
|
+
const { getEditorConfig } = await import(
|
27
|
+
"panda/cms/editor/editor_js_config"
|
28
|
+
);
|
29
|
+
const config = getEditorConfig(holderId, this.getInitialContent());
|
30
|
+
|
31
|
+
editor_content_post;
|
32
|
+
|
33
|
+
this.editor = new EditorJS({
|
34
|
+
...config,
|
35
|
+
holder: holderId,
|
36
|
+
autofocus: false,
|
37
|
+
minHeight: 1,
|
38
|
+
logLevel: "ERROR",
|
39
|
+
onChange: () => {
|
40
|
+
if (!this.editor) return;
|
41
|
+
this.editor.save().then((outputData) => {
|
42
|
+
outputData.source = "editorJS";
|
43
|
+
this.hiddenFieldTarget.value = JSON.stringify(outputData);
|
44
|
+
});
|
45
|
+
},
|
46
|
+
});
|
47
|
+
} catch (error) {
|
48
|
+
console.error("[Panda CMS] Editor setup failed:", error);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
|
52
|
+
getInitialContent() {
|
53
|
+
try {
|
54
|
+
const value = this.hiddenFieldTarget.value;
|
55
|
+
if (value && value !== "{}") {
|
56
|
+
const data = JSON.parse(value);
|
57
|
+
if (data.blocks) return data;
|
58
|
+
}
|
59
|
+
} catch (e) {
|
60
|
+
console.warn("[Panda CMS] Could not parse initial content:", e);
|
61
|
+
}
|
62
|
+
|
63
|
+
return {
|
64
|
+
time: Date.now(),
|
65
|
+
blocks: [{ type: "paragraph", data: { text: "" } }],
|
66
|
+
version: "2.28.2",
|
67
|
+
source: "editorJS",
|
68
|
+
};
|
69
|
+
}
|
70
|
+
|
71
|
+
disconnect() {
|
72
|
+
if (this.editor) {
|
73
|
+
this.editor.destroy();
|
74
|
+
this.editor = null;
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
@@ -0,0 +1,320 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
2
|
+
import { PlainTextEditor } from "panda/cms/editor/plain_text_editor"
|
3
|
+
import { EditorJSInitializer } from "panda/cms/editor/editor_js_initializer"
|
4
|
+
|
5
|
+
export default class extends Controller {
|
6
|
+
static values = {
|
7
|
+
pageId: Number,
|
8
|
+
adminPath: String,
|
9
|
+
autosave: Boolean
|
10
|
+
}
|
11
|
+
|
12
|
+
connect() {
|
13
|
+
console.debug("[Panda CMS] EditorIframe controller connected")
|
14
|
+
this.frame = this.element
|
15
|
+
this.setupControls()
|
16
|
+
this.setupFrame()
|
17
|
+
this.editors = []
|
18
|
+
this.editorsInitialized = {
|
19
|
+
plain: false,
|
20
|
+
rich: false
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
setupControls() {
|
25
|
+
// Create editor controls if they don't exist
|
26
|
+
if (!parent.document.querySelector('.editor-controls')) {
|
27
|
+
const controls = parent.document.createElement('div')
|
28
|
+
controls.className = 'editor-controls'
|
29
|
+
parent.document.body.appendChild(controls)
|
30
|
+
}
|
31
|
+
|
32
|
+
// Create save button if it doesn't exist
|
33
|
+
if (!parent.document.getElementById('saveEditableButton')) {
|
34
|
+
const saveButton = parent.document.createElement('a')
|
35
|
+
saveButton.id = 'saveEditableButton'
|
36
|
+
saveButton.href = '#'
|
37
|
+
saveButton.textContent = 'Save Changes'
|
38
|
+
saveButton.className = 'btn btn-primary'
|
39
|
+
parent.document.querySelector('.editor-controls').appendChild(saveButton)
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
setupFrame() {
|
44
|
+
// Always show the frame initially to ensure it's visible for tests
|
45
|
+
this.frame.style.display = ""
|
46
|
+
this.frame.style.width = "100%"
|
47
|
+
this.frame.style.height = "100%"
|
48
|
+
this.frame.style.minHeight = "500px"
|
49
|
+
|
50
|
+
// Get CSRF token
|
51
|
+
this.csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ""
|
52
|
+
|
53
|
+
// Setup frame load handler
|
54
|
+
this.frame.addEventListener("load", async () => {
|
55
|
+
console.debug("[Panda CMS] Frame loaded")
|
56
|
+
this.frameDocument = this.frame.contentDocument || this.frame.contentWindow.document
|
57
|
+
this.body = this.frameDocument.body
|
58
|
+
this.head = this.frameDocument.head
|
59
|
+
|
60
|
+
// Set up error handling for the iframe
|
61
|
+
this.frameDocument.defaultView.onerror = (message, source, lineno, colno, error) => {
|
62
|
+
// Relay the error to the parent window
|
63
|
+
const fullMessage = `iFrame Error: ${message} (${source}:${lineno}:${colno})`
|
64
|
+
console.error(fullMessage, error)
|
65
|
+
|
66
|
+
// Throw the error in the parent context for Cuprite to catch
|
67
|
+
setTimeout(() => {
|
68
|
+
throw new Error(fullMessage)
|
69
|
+
}, 0)
|
70
|
+
|
71
|
+
return false // Let the error propagate
|
72
|
+
}
|
73
|
+
|
74
|
+
// Set up unhandled rejection handling for the iframe
|
75
|
+
this.frameDocument.defaultView.onunhandledrejection = (event) => {
|
76
|
+
const fullMessage = `iFrame Unhandled Promise Rejection: ${event.reason}`
|
77
|
+
console.error(fullMessage)
|
78
|
+
|
79
|
+
// Throw the error in the parent context for Cuprite to catch
|
80
|
+
setTimeout(() => {
|
81
|
+
throw event.reason
|
82
|
+
}, 0)
|
83
|
+
}
|
84
|
+
|
85
|
+
// Ensure frame is visible after load
|
86
|
+
this.frame.style.display = ""
|
87
|
+
this.ensureFrameVisibility()
|
88
|
+
|
89
|
+
// Wait for document to be ready
|
90
|
+
if (this.frameDocument.readyState !== 'complete') {
|
91
|
+
await new Promise(resolve => {
|
92
|
+
this.frameDocument.addEventListener('DOMContentLoaded', resolve)
|
93
|
+
})
|
94
|
+
}
|
95
|
+
|
96
|
+
// Load Editor.js resources in the iframe context
|
97
|
+
try {
|
98
|
+
const { EDITOR_JS_RESOURCES, EDITOR_JS_CSS } = await import("panda/cms/editor/editor_js_config")
|
99
|
+
const { ResourceLoader } = await import("panda/cms/editor/resource_loader")
|
100
|
+
|
101
|
+
// First load EditorJS core
|
102
|
+
const editorCore = EDITOR_JS_RESOURCES[0]
|
103
|
+
await ResourceLoader.loadScript(this.frameDocument, this.head, editorCore)
|
104
|
+
|
105
|
+
// Then load all tools in parallel
|
106
|
+
const toolLoads = EDITOR_JS_RESOURCES.slice(1).map(async (resource) => {
|
107
|
+
await ResourceLoader.loadScript(this.frameDocument, this.head, resource)
|
108
|
+
})
|
109
|
+
|
110
|
+
// Load CSS directly
|
111
|
+
await ResourceLoader.embedCSS(this.frameDocument, this.head, EDITOR_JS_CSS)
|
112
|
+
|
113
|
+
// Wait for all resources to load
|
114
|
+
await Promise.all(toolLoads)
|
115
|
+
console.debug("[Panda CMS] Editor resources loaded in iframe")
|
116
|
+
|
117
|
+
// Wait a small amount of time for scripts to initialize
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
119
|
+
|
120
|
+
// Initialize editors only if we have the body and editable elements
|
121
|
+
if (this.body && this.body.querySelector('[data-editable-kind]')) {
|
122
|
+
await this.initializeEditors()
|
123
|
+
} else {
|
124
|
+
const error = new Error("[Panda CMS] Frame body or editable elements not found")
|
125
|
+
console.error(error)
|
126
|
+
throw error
|
127
|
+
}
|
128
|
+
} catch (error) {
|
129
|
+
console.error("[Panda CMS] Error loading editor resources in iframe:", error)
|
130
|
+
throw error
|
131
|
+
}
|
132
|
+
})
|
133
|
+
}
|
134
|
+
|
135
|
+
ensureFrameVisibility() {
|
136
|
+
// Force frame to be visible
|
137
|
+
this.frame.style.display = ""
|
138
|
+
|
139
|
+
// Check dimensions and fix if needed
|
140
|
+
if (this.frame.offsetWidth === 0 || this.frame.offsetHeight === 0) {
|
141
|
+
console.warn("[Panda CMS] iFrame has zero dimensions, fixing...")
|
142
|
+
this.frame.style.width = "100%"
|
143
|
+
this.frame.style.height = "100%"
|
144
|
+
this.frame.style.minHeight = "500px"
|
145
|
+
}
|
146
|
+
|
147
|
+
// Log visibility state
|
148
|
+
console.debug("[Panda CMS] Frame visibility state:", {
|
149
|
+
display: this.frame.style.display,
|
150
|
+
width: this.frame.offsetWidth,
|
151
|
+
height: this.frame.offsetHeight,
|
152
|
+
visible: this.frame.offsetParent !== null
|
153
|
+
})
|
154
|
+
}
|
155
|
+
|
156
|
+
initializeEditors() {
|
157
|
+
console.debug("[Panda CMS] Starting editor initialization")
|
158
|
+
|
159
|
+
// Get all editable elements
|
160
|
+
const plainTextElements = this.body.querySelectorAll('[data-editable-kind="plain_text"], [data-editable-kind="markdown"], [data-editable-kind="html"]')
|
161
|
+
const richTextElements = this.body.querySelectorAll('[data-editable-kind="rich_text"]')
|
162
|
+
|
163
|
+
console.debug(`[Panda CMS] Found ${plainTextElements.length} plain text elements and ${richTextElements.length} rich text elements`)
|
164
|
+
|
165
|
+
// Always ensure frame is visible
|
166
|
+
this.ensureFrameVisibility()
|
167
|
+
|
168
|
+
// Initialize editors if they exist
|
169
|
+
if (plainTextElements.length > 0 || richTextElements.length > 0) {
|
170
|
+
this.initializePlainTextEditors()
|
171
|
+
this.initializeRichTextEditors()
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
initializePlainTextEditors() {
|
176
|
+
this.editorsInitialized.plain = false
|
177
|
+
const plainTextElements = this.body.querySelectorAll('[data-editable-kind="plain_text"], [data-editable-kind="markdown"], [data-editable-kind="html"]')
|
178
|
+
console.debug(`[Panda CMS] Found ${plainTextElements.length} plain text elements`)
|
179
|
+
|
180
|
+
plainTextElements.forEach(element => {
|
181
|
+
const editor = new PlainTextEditor(element, this.frameDocument, {
|
182
|
+
autosave: this.autosaveValue,
|
183
|
+
adminPath: this.adminPathValue,
|
184
|
+
csrfToken: this.csrfToken
|
185
|
+
})
|
186
|
+
this.editors.push(editor)
|
187
|
+
})
|
188
|
+
|
189
|
+
this.editorsInitialized.plain = true
|
190
|
+
this.checkAllEditorsInitialized()
|
191
|
+
}
|
192
|
+
|
193
|
+
async initializeRichTextEditors() {
|
194
|
+
this.editorsInitialized.rich = false
|
195
|
+
const richTextElements = this.body.querySelectorAll('[data-editable-kind="rich_text"]')
|
196
|
+
console.debug(`[Panda CMS] Found ${richTextElements.length} rich text elements`)
|
197
|
+
|
198
|
+
if (richTextElements.length > 0) {
|
199
|
+
// Verify Editor.js is available in the iframe context
|
200
|
+
if (!this.frameDocument.defaultView.EditorJS) {
|
201
|
+
const error = new Error("Editor.js not loaded in iframe context")
|
202
|
+
console.error("[Panda CMS]", error)
|
203
|
+
throw error // This will bubble up and fail the test
|
204
|
+
}
|
205
|
+
|
206
|
+
const initializer = new EditorJSInitializer(this.frameDocument, true)
|
207
|
+
|
208
|
+
// Don't wrap in try/catch to let errors bubble up
|
209
|
+
const editors = await Promise.all(
|
210
|
+
Array.from(richTextElements).map(async element => {
|
211
|
+
// Create holder element before initialization
|
212
|
+
const holderId = `editor-${Math.random().toString(36).substr(2, 9)}`
|
213
|
+
const holderElement = this.frameDocument.createElement('div')
|
214
|
+
holderElement.id = holderId
|
215
|
+
holderElement.className = 'editor-js-holder codex-editor'
|
216
|
+
element.appendChild(holderElement)
|
217
|
+
|
218
|
+
// Wait for the holder element to be in the DOM
|
219
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
220
|
+
|
221
|
+
// Verify the holder element exists
|
222
|
+
const verifyHolder = this.frameDocument.getElementById(holderId)
|
223
|
+
if (!verifyHolder) {
|
224
|
+
const error = new Error(`Failed to create editor holder element ${holderId}`)
|
225
|
+
console.error("[Panda CMS]", error)
|
226
|
+
throw error // This will bubble up and fail the test
|
227
|
+
}
|
228
|
+
|
229
|
+
console.debug(`[Panda CMS] Created editor holder: ${holderId}`, {
|
230
|
+
exists: !!verifyHolder,
|
231
|
+
parent: element.id || 'no-id',
|
232
|
+
editorJSAvailable: !!this.frameDocument.defaultView.EditorJS
|
233
|
+
})
|
234
|
+
|
235
|
+
// Initialize editor with empty data
|
236
|
+
const editor = await initializer.initialize(holderElement, {}, holderId)
|
237
|
+
|
238
|
+
// Set up save handler for this editor
|
239
|
+
const saveButton = parent.document.getElementById('saveEditableButton')
|
240
|
+
if (saveButton) {
|
241
|
+
saveButton.addEventListener('click', async () => {
|
242
|
+
const outputData = await editor.save()
|
243
|
+
outputData.source = "editorJS"
|
244
|
+
|
245
|
+
const pageId = element.getAttribute("data-editable-page-id")
|
246
|
+
const blockContentId = element.getAttribute("data-editable-block-content-id")
|
247
|
+
|
248
|
+
const response = await fetch(`${this.adminPathValue}/pages/${pageId}/block_contents/${blockContentId}`, {
|
249
|
+
method: "PATCH",
|
250
|
+
headers: {
|
251
|
+
"Content-Type": "application/json",
|
252
|
+
"X-CSRF-Token": this.csrfToken
|
253
|
+
},
|
254
|
+
body: JSON.stringify({ content: outputData })
|
255
|
+
})
|
256
|
+
|
257
|
+
if (!response.ok) {
|
258
|
+
const error = new Error('Save failed')
|
259
|
+
console.error("[Panda CMS]", error)
|
260
|
+
throw error
|
261
|
+
}
|
262
|
+
|
263
|
+
this.handleSuccess()
|
264
|
+
})
|
265
|
+
} else {
|
266
|
+
console.warn("[Panda CMS] Save button not found")
|
267
|
+
}
|
268
|
+
|
269
|
+
return editor
|
270
|
+
})
|
271
|
+
)
|
272
|
+
|
273
|
+
// Filter out any null editors and add the valid ones
|
274
|
+
const validEditors = editors.filter(editor => editor !== null)
|
275
|
+
this.editors.push(...validEditors)
|
276
|
+
|
277
|
+
// If we didn't get any valid editors, that's an error
|
278
|
+
if (validEditors.length === 0) {
|
279
|
+
const error = new Error("No editors were successfully initialized")
|
280
|
+
console.error("[Panda CMS]", error)
|
281
|
+
throw error
|
282
|
+
}
|
283
|
+
}
|
284
|
+
|
285
|
+
this.editorsInitialized.rich = true
|
286
|
+
this.checkAllEditorsInitialized()
|
287
|
+
}
|
288
|
+
|
289
|
+
checkAllEditorsInitialized() {
|
290
|
+
console.log("[Panda CMS] Editor initialization status:", this.editorsInitialized)
|
291
|
+
|
292
|
+
// Always ensure frame is visible
|
293
|
+
this.ensureFrameVisibility()
|
294
|
+
}
|
295
|
+
|
296
|
+
handleError(error) {
|
297
|
+
const errorMessage = parent.document.getElementById("errorMessage")
|
298
|
+
if (errorMessage) {
|
299
|
+
errorMessage.getElementsByClassName('flash-message-text')[0].textContent = error
|
300
|
+
errorMessage.classList.remove("hidden")
|
301
|
+
setTimeout(() => {
|
302
|
+
errorMessage.classList.add("hidden")
|
303
|
+
}, 3000)
|
304
|
+
}
|
305
|
+
console.error("[Panda CMS] Error:", error)
|
306
|
+
|
307
|
+
// Throw the error to fail the test
|
308
|
+
throw error
|
309
|
+
}
|
310
|
+
|
311
|
+
handleSuccess() {
|
312
|
+
const successMessage = parent.document.getElementById("successMessage")
|
313
|
+
if (successMessage) {
|
314
|
+
successMessage.classList.remove("hidden")
|
315
|
+
setTimeout(() => {
|
316
|
+
successMessage.classList.add("hidden")
|
317
|
+
}, 3000)
|
318
|
+
}
|
319
|
+
}
|
320
|
+
}
|
@@ -0,0 +1,48 @@
|
|
1
|
+
console.debug("[Panda CMS] Importing Panda CMS Stimulus Controller...")
|
2
|
+
|
3
|
+
import { Application as PandaCMSApplication } from "@hotwired/stimulus"
|
4
|
+
|
5
|
+
const pandaCmsApplication = PandaCMSApplication.start()
|
6
|
+
|
7
|
+
console.debug("[Panda CMS] Application started...")
|
8
|
+
|
9
|
+
// Configure Stimulus development experience
|
10
|
+
pandaCmsApplication.debug = false
|
11
|
+
window.pandaCmsStimulus = pandaCmsApplication
|
12
|
+
|
13
|
+
console.debug("[Panda CMS] window.pandaCmsStimulus available...")
|
14
|
+
|
15
|
+
console.debug("[Panda CMS] Registering controllers...")
|
16
|
+
|
17
|
+
// Use the same paths as defined in _importmap.html.erb
|
18
|
+
import DashboardController from "panda/cms/controllers/dashboard_controller"
|
19
|
+
pandaCmsApplication.register("dashboard", DashboardController)
|
20
|
+
|
21
|
+
import EditorFormController from "panda/cms/controllers/editor_form_controller"
|
22
|
+
pandaCmsApplication.register("editor-form", EditorFormController)
|
23
|
+
|
24
|
+
import SlugController from "panda/cms/controllers/slug_controller"
|
25
|
+
pandaCmsApplication.register("slug", SlugController)
|
26
|
+
|
27
|
+
import EditorIframeController from "panda/cms/controllers/editor_iframe_controller"
|
28
|
+
pandaCmsApplication.register("editor-iframe", EditorIframeController)
|
29
|
+
|
30
|
+
console.debug("[Panda CMS] Registering components...")
|
31
|
+
|
32
|
+
// Import and register all TailwindCSS Components or just the ones you need
|
33
|
+
import { Alert, Autosave, ColorPreview, Dropdown, Modal, Tabs, Popover, Toggle, Slideover } from "tailwindcss-stimulus-components"
|
34
|
+
pandaCmsApplication.register('alert', Alert)
|
35
|
+
pandaCmsApplication.register('autosave', Autosave)
|
36
|
+
pandaCmsApplication.register('color-preview', ColorPreview)
|
37
|
+
pandaCmsApplication.register('dropdown', Dropdown)
|
38
|
+
pandaCmsApplication.register('modal', Modal)
|
39
|
+
pandaCmsApplication.register('popover', Popover)
|
40
|
+
pandaCmsApplication.register('slideover', Slideover)
|
41
|
+
pandaCmsApplication.register('tabs', Tabs)
|
42
|
+
pandaCmsApplication.register('toggle', Toggle)
|
43
|
+
|
44
|
+
console.debug("[Panda CMS] Components registered...")
|
45
|
+
|
46
|
+
export { pandaCmsApplication }
|
47
|
+
|
48
|
+
console.debug("[Panda CMS] Application exported...")
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = [
|
5
|
+
"existing_root",
|
6
|
+
"input_select",
|
7
|
+
"input_text",
|
8
|
+
"output_text",
|
9
|
+
];
|
10
|
+
|
11
|
+
connect() {
|
12
|
+
console.debug("[Panda CMS] Slug handler connected...");
|
13
|
+
// Generate path on initial load if title exists
|
14
|
+
if (this.input_textTarget.value) {
|
15
|
+
this.generatePath();
|
16
|
+
}
|
17
|
+
}
|
18
|
+
|
19
|
+
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;
|
25
|
+
|
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
|
+
}
|
37
|
+
|
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");
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
setPrePath() {
|
47
|
+
try {
|
48
|
+
const match = this.input_selectTarget.options[this.input_selectTarget.selectedIndex].text.match(/.*\((.*)\)$/);
|
49
|
+
if (match) {
|
50
|
+
this.parent_slugs = match[1];
|
51
|
+
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);
|
57
|
+
}
|
58
|
+
} catch (error) {
|
59
|
+
console.error("Error setting pre-path:", error);
|
60
|
+
}
|
61
|
+
}
|
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
|
+
createSlug(input) {
|
66
|
+
if (!input) return "";
|
67
|
+
|
68
|
+
var str = input
|
69
|
+
.toLowerCase()
|
70
|
+
.trim()
|
71
|
+
.replace(/[^\w\s-]/g, "-")
|
72
|
+
.replace(/&/g, "and")
|
73
|
+
.replace(/[\s_-]+/g, "-")
|
74
|
+
.trim();
|
75
|
+
|
76
|
+
return this.trimStartEnd(str, "-");
|
77
|
+
}
|
78
|
+
|
79
|
+
trimStartEnd(str, ch) {
|
80
|
+
var start = 0;
|
81
|
+
var end = str.length;
|
82
|
+
|
83
|
+
while (start < end && str[start] === ch) ++start;
|
84
|
+
while (end > start && str[end - 1] === ch) --end;
|
85
|
+
return start > 0 || end < str.length ? str.substring(start, end) : str;
|
86
|
+
}
|
87
|
+
}
|
@@ -0,0 +1,80 @@
|
|
1
|
+
export class CSSExtractor {
|
2
|
+
/**
|
3
|
+
* Extracts CSS rules from within a specific selector and transforms them for EditorJS
|
4
|
+
* @param {string} css - The CSS content to parse
|
5
|
+
* @returns {string} The extracted and transformed CSS rules
|
6
|
+
*/
|
7
|
+
static extractStyles(css) {
|
8
|
+
const rules = []
|
9
|
+
let inComponents = false
|
10
|
+
let inContentRule = false
|
11
|
+
let braceCount = 0
|
12
|
+
let currentRule = ''
|
13
|
+
|
14
|
+
// Split CSS into lines and process each line
|
15
|
+
const lines = css.split('\n')
|
16
|
+
|
17
|
+
for (const line of lines) {
|
18
|
+
const trimmedLine = line.trim()
|
19
|
+
|
20
|
+
// Check if we're entering components layer
|
21
|
+
if (trimmedLine === '@layer components {') {
|
22
|
+
inComponents = true
|
23
|
+
continue
|
24
|
+
}
|
25
|
+
|
26
|
+
// Only process lines within components layer
|
27
|
+
if (!inComponents) continue
|
28
|
+
|
29
|
+
// If we find the .content selector
|
30
|
+
if (!inContentRule && trimmedLine.startsWith('.content')) {
|
31
|
+
inContentRule = true
|
32
|
+
braceCount++
|
33
|
+
// Transform the selector for EditorJS
|
34
|
+
currentRule = '.codex-editor__redactor .ce-block .ce-block__content'
|
35
|
+
if (trimmedLine.includes('{')) {
|
36
|
+
currentRule += ' {'
|
37
|
+
}
|
38
|
+
continue
|
39
|
+
}
|
40
|
+
|
41
|
+
// If we're inside a content rule
|
42
|
+
if (inContentRule) {
|
43
|
+
// Transform selectors for EditorJS
|
44
|
+
let transformedLine = line
|
45
|
+
.replace(/\.content\s+/g, '.codex-editor__redactor .ce-block .ce-block__content ')
|
46
|
+
.replace(/\bh1\b(?![-_])/g, 'h1.ce-header')
|
47
|
+
.replace(/\bh2\b(?![-_])/g, 'h2.ce-header')
|
48
|
+
.replace(/\bh3\b(?![-_])/g, 'h3.ce-header')
|
49
|
+
.replace(/\bul\b(?![-_])/g, 'ul.cdx-list')
|
50
|
+
.replace(/\bol\b(?![-_])/g, 'ol.cdx-list')
|
51
|
+
.replace(/\bli\b(?![-_])/g, 'li.cdx-list__item')
|
52
|
+
.replace(/\bblockquote\b(?![-_])/g, '.cdx-quote')
|
53
|
+
|
54
|
+
currentRule += '\n' + transformedLine
|
55
|
+
|
56
|
+
// Count braces to handle nested rules
|
57
|
+
braceCount += (trimmedLine.match(/{/g) || []).length
|
58
|
+
braceCount -= (trimmedLine.match(/}/g) || []).length
|
59
|
+
|
60
|
+
// If braces are balanced, we've found the end of the rule
|
61
|
+
if (braceCount === 0) {
|
62
|
+
rules.push(currentRule)
|
63
|
+
inContentRule = false
|
64
|
+
currentRule = ''
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
return rules.join('\n\n')
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Gets all styles from a stylesheet that apply to the editor
|
74
|
+
* @param {string} css - The CSS content to parse
|
75
|
+
* @returns {string} The extracted CSS rules
|
76
|
+
*/
|
77
|
+
static getEditorStyles(css) {
|
78
|
+
return this.extractStyles(css)
|
79
|
+
}
|
80
|
+
}
|