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,177 @@
|
|
1
|
+
export const EDITOR_JS_RESOURCES = [
|
2
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2",
|
3
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3",
|
4
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/header@2.8.1",
|
5
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/nested-list@1.4.2",
|
6
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/quote@2.6.0",
|
7
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/simple-image@1.6.0",
|
8
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/table@2.3.0",
|
9
|
+
"https://cdn.jsdelivr.net/npm/@editorjs/embed@2.7.0"
|
10
|
+
]
|
11
|
+
|
12
|
+
// Allow applications to add their own resources
|
13
|
+
if (window.PANDA_CMS_EDITOR_JS_RESOURCES) {
|
14
|
+
EDITOR_JS_RESOURCES.push(...window.PANDA_CMS_EDITOR_JS_RESOURCES)
|
15
|
+
}
|
16
|
+
|
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
|
+
}
|
65
|
+
|
66
|
+
.ce-block--list li {
|
67
|
+
margin: 0;
|
68
|
+
padding-left: inherit;
|
69
|
+
}
|
70
|
+
|
71
|
+
/* Ensure editor toolbar is above content */
|
72
|
+
.ce-toolbar {
|
73
|
+
z-index: 100;
|
74
|
+
}
|
75
|
+
|
76
|
+
/* Style the block selection */
|
77
|
+
.ce-block--selected {
|
78
|
+
background-color: rgba(16, 64, 113, 0.05);
|
79
|
+
border-radius: 4px;
|
80
|
+
}`
|
81
|
+
|
82
|
+
export const getEditorConfig = (elementId, previousData, doc = document) => {
|
83
|
+
// Validate holder element exists
|
84
|
+
const holder = doc.getElementById(elementId)
|
85
|
+
if (!holder) {
|
86
|
+
throw new Error(`Editor holder element ${elementId} not found`)
|
87
|
+
}
|
88
|
+
|
89
|
+
const config = {
|
90
|
+
holder: elementId,
|
91
|
+
data: previousData || {},
|
92
|
+
placeholder: 'Click the + button to add content...',
|
93
|
+
inlineToolbar: true,
|
94
|
+
tools: {
|
95
|
+
header: {
|
96
|
+
class: window.Header,
|
97
|
+
inlineToolbar: true,
|
98
|
+
config: {
|
99
|
+
placeholder: 'Enter a header',
|
100
|
+
levels: [1, 2, 3, 4, 5, 6],
|
101
|
+
defaultLevel: 2
|
102
|
+
}
|
103
|
+
},
|
104
|
+
paragraph: {
|
105
|
+
class: window.Paragraph,
|
106
|
+
inlineToolbar: true,
|
107
|
+
config: {
|
108
|
+
placeholder: 'Start writing or press Tab to add content...'
|
109
|
+
}
|
110
|
+
},
|
111
|
+
list: {
|
112
|
+
class: window.NestedList,
|
113
|
+
inlineToolbar: true,
|
114
|
+
config: {
|
115
|
+
defaultStyle: 'unordered'
|
116
|
+
}
|
117
|
+
},
|
118
|
+
quote: {
|
119
|
+
class: window.Quote,
|
120
|
+
inlineToolbar: true,
|
121
|
+
config: {
|
122
|
+
quotePlaceholder: 'Enter a quote',
|
123
|
+
captionPlaceholder: 'Quote\'s author'
|
124
|
+
}
|
125
|
+
},
|
126
|
+
table: {
|
127
|
+
class: window.Table,
|
128
|
+
inlineToolbar: true,
|
129
|
+
config: {
|
130
|
+
rows: 2,
|
131
|
+
cols: 2
|
132
|
+
}
|
133
|
+
},
|
134
|
+
image: {
|
135
|
+
class: window.SimpleImage,
|
136
|
+
inlineToolbar: true,
|
137
|
+
config: {
|
138
|
+
placeholder: 'Paste an image URL...'
|
139
|
+
}
|
140
|
+
},
|
141
|
+
embed: {
|
142
|
+
class: window.Embed,
|
143
|
+
inlineToolbar: true,
|
144
|
+
config: {
|
145
|
+
services: {
|
146
|
+
youtube: true,
|
147
|
+
vimeo: true
|
148
|
+
}
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
}
|
153
|
+
|
154
|
+
// Remove any undefined tools from the config
|
155
|
+
config.tools = Object.fromEntries(
|
156
|
+
Object.entries(config.tools)
|
157
|
+
.filter(([_, value]) => value?.class !== undefined)
|
158
|
+
.map(([name, tool]) => {
|
159
|
+
if (!tool.class) {
|
160
|
+
throw new Error(`Tool ${name} has no class defined`)
|
161
|
+
}
|
162
|
+
return [name, tool]
|
163
|
+
})
|
164
|
+
)
|
165
|
+
|
166
|
+
// Allow applications to customize the config through Ruby
|
167
|
+
if (window.PANDA_CMS_EDITOR_JS_CONFIG) {
|
168
|
+
Object.assign(config.tools, window.PANDA_CMS_EDITOR_JS_CONFIG)
|
169
|
+
}
|
170
|
+
|
171
|
+
// Allow applications to customize the config through JavaScript
|
172
|
+
if (typeof window.customizeEditorJS === 'function') {
|
173
|
+
window.customizeEditorJS(config)
|
174
|
+
}
|
175
|
+
|
176
|
+
return config
|
177
|
+
}
|
@@ -0,0 +1,285 @@
|
|
1
|
+
import { ResourceLoader } from "panda/cms/editor/resource_loader"
|
2
|
+
import { EDITOR_JS_RESOURCES, EDITOR_JS_CSS, getEditorConfig } from "panda/cms/editor/editor_js_config"
|
3
|
+
import { CSSExtractor } from "panda/cms/editor/css_extractor"
|
4
|
+
|
5
|
+
export class EditorJSInitializer {
|
6
|
+
constructor(document, withinIFrame = false) {
|
7
|
+
this.document = document
|
8
|
+
this.withinIFrame = withinIFrame
|
9
|
+
}
|
10
|
+
|
11
|
+
/**
|
12
|
+
* Initializes the EditorJS instance for a given element.
|
13
|
+
* This method loads necessary resources and returns the JavaScript code for initializing the editor.
|
14
|
+
*
|
15
|
+
* @param {HTMLElement} element - The DOM element to initialize the editor on
|
16
|
+
* @param {Object} initialData - The initial data for the editor
|
17
|
+
* @param {string} editorId - The ID to use for the editor holder
|
18
|
+
* @returns {Promise<EditorJS>} A promise that resolves to the editor instance
|
19
|
+
*/
|
20
|
+
async initialize(element, initialData = {}, editorId = null) {
|
21
|
+
await this.loadResources()
|
22
|
+
const result = await this.initializeEditor(element, initialData, editorId)
|
23
|
+
return result
|
24
|
+
}
|
25
|
+
|
26
|
+
/**
|
27
|
+
* Gets the application's styles from its configured stylesheet
|
28
|
+
* @returns {Promise<string>} The extracted CSS rules
|
29
|
+
*/
|
30
|
+
async getApplicationStyles() {
|
31
|
+
try {
|
32
|
+
// Get the configured stylesheet URL, defaulting to Tailwind Rails default
|
33
|
+
const stylesheetUrl = window.PANDA_CMS_CONFIG?.stylesheetUrl || '/assets/application.tailwind.css'
|
34
|
+
|
35
|
+
// Fetch the CSS content
|
36
|
+
const response = await fetch(stylesheetUrl)
|
37
|
+
const css = await response.text()
|
38
|
+
return CSSExtractor.getEditorStyles(css)
|
39
|
+
} catch (error) {
|
40
|
+
return ''
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Loads the necessary resources for the EditorJS instance.
|
46
|
+
* This method fetches the required scripts and stylesheets and embeds them into the document.
|
47
|
+
*/
|
48
|
+
async loadResources() {
|
49
|
+
try {
|
50
|
+
// First load EditorJS core
|
51
|
+
const editorCore = EDITOR_JS_RESOURCES[0]
|
52
|
+
await ResourceLoader.loadScript(this.document, this.document.head, editorCore)
|
53
|
+
|
54
|
+
// Then load all tools in parallel
|
55
|
+
const toolLoads = EDITOR_JS_RESOURCES.slice(1).map(async (resource) => {
|
56
|
+
await ResourceLoader.loadScript(this.document, this.document.head, resource)
|
57
|
+
})
|
58
|
+
|
59
|
+
// Load CSS directly
|
60
|
+
await ResourceLoader.embedCSS(this.document, this.document.head, EDITOR_JS_CSS)
|
61
|
+
|
62
|
+
// Wait for all resources to load
|
63
|
+
await Promise.all(toolLoads)
|
64
|
+
|
65
|
+
// Wait for EditorJS to be available
|
66
|
+
await this.waitForEditorJS()
|
67
|
+
} catch (error) {
|
68
|
+
throw error
|
69
|
+
}
|
70
|
+
}
|
71
|
+
|
72
|
+
async initializeEditor(element, initialData = {}, editorId = null) {
|
73
|
+
// Generate a consistent holder ID if not provided
|
74
|
+
const holderId = editorId || `editor-${element.id || Math.random().toString(36).substr(2, 9)}`
|
75
|
+
|
76
|
+
// Create or find the holder element in the correct document context
|
77
|
+
let holderElement = this.document.getElementById(holderId)
|
78
|
+
if (!holderElement) {
|
79
|
+
// Create the holder element in the correct document context
|
80
|
+
holderElement = this.document.createElement('div')
|
81
|
+
holderElement.id = holderId
|
82
|
+
holderElement.className = 'editor-js-holder codex-editor'
|
83
|
+
|
84
|
+
// Append to the element and force a reflow
|
85
|
+
element.appendChild(holderElement)
|
86
|
+
void holderElement.offsetHeight // Force a reflow
|
87
|
+
}
|
88
|
+
|
89
|
+
// Verify the holder element exists in the correct document context
|
90
|
+
const verifyHolder = this.document.getElementById(holderId)
|
91
|
+
if (!verifyHolder) {
|
92
|
+
throw new Error(`Failed to create editor holder element ${holderId}`)
|
93
|
+
}
|
94
|
+
|
95
|
+
// Clear any existing content in the holder
|
96
|
+
holderElement.innerHTML = ''
|
97
|
+
|
98
|
+
// Add source to initial data
|
99
|
+
if (initialData && !initialData.source) {
|
100
|
+
initialData.source = "editorJS"
|
101
|
+
}
|
102
|
+
|
103
|
+
// Get the base config but pass our document context
|
104
|
+
const config = getEditorConfig(holderId, initialData, this.document)
|
105
|
+
|
106
|
+
// Override specific settings for iframe context
|
107
|
+
const editorConfig = {
|
108
|
+
...config,
|
109
|
+
holder: holderElement, // Use element reference instead of ID
|
110
|
+
minHeight: 1, // Prevent auto-height issues in iframe
|
111
|
+
autofocus: false, // Prevent focus issues
|
112
|
+
logLevel: 'ERROR', // Only show errors
|
113
|
+
tools: {
|
114
|
+
...config.tools,
|
115
|
+
// Ensure tools use the correct window context
|
116
|
+
paragraph: { ...config.tools.paragraph, class: this.document.defaultView.Paragraph },
|
117
|
+
header: { ...config.tools.header, class: this.document.defaultView.Header },
|
118
|
+
list: { ...config.tools.list, class: this.document.defaultView.NestedList },
|
119
|
+
quote: { ...config.tools.quote, class: this.document.defaultView.Quote },
|
120
|
+
table: { ...config.tools.table, class: this.document.defaultView.Table },
|
121
|
+
image: { ...config.tools.image, class: this.document.defaultView.SimpleImage },
|
122
|
+
embed: { ...config.tools.embed, class: this.document.defaultView.Embed }
|
123
|
+
}
|
124
|
+
}
|
125
|
+
|
126
|
+
// Create editor instance directly
|
127
|
+
const editor = new this.document.defaultView.EditorJS({
|
128
|
+
...editorConfig,
|
129
|
+
onReady: () => {
|
130
|
+
// Store the editor instance globally for testing
|
131
|
+
if (this.withinIFrame) {
|
132
|
+
this.document.defaultView.editor = editor
|
133
|
+
} else {
|
134
|
+
window.editor = editor
|
135
|
+
}
|
136
|
+
|
137
|
+
// Mark editor as ready
|
138
|
+
editor.isReady = true
|
139
|
+
|
140
|
+
// Force redraw of toolbar and blocks
|
141
|
+
setTimeout(async () => {
|
142
|
+
try {
|
143
|
+
const toolbar = holderElement.querySelector('.ce-toolbar')
|
144
|
+
const blockWrapper = holderElement.querySelector('.ce-block')
|
145
|
+
|
146
|
+
if (!toolbar || !blockWrapper) {
|
147
|
+
// Clear and insert a new block to force UI update
|
148
|
+
await editor.blocks.clear()
|
149
|
+
await editor.blocks.insert('paragraph')
|
150
|
+
|
151
|
+
// Force a redraw by toggling display
|
152
|
+
holderElement.style.display = 'none'
|
153
|
+
void holderElement.offsetHeight
|
154
|
+
holderElement.style.display = ''
|
155
|
+
}
|
156
|
+
|
157
|
+
// Call the ready hook if it exists
|
158
|
+
if (typeof window.onEditorJSReady === 'function') {
|
159
|
+
window.onEditorJSReady(editor)
|
160
|
+
}
|
161
|
+
} catch (error) {
|
162
|
+
console.error('Error during editor redraw:', error)
|
163
|
+
}
|
164
|
+
}, 100)
|
165
|
+
},
|
166
|
+
onChange: async (api, event) => {
|
167
|
+
try {
|
168
|
+
// Save the current editor data
|
169
|
+
const outputData = await api.saver.save()
|
170
|
+
outputData.source = "editorJS"
|
171
|
+
const contentJson = JSON.stringify(outputData)
|
172
|
+
|
173
|
+
if (!this.withinIFrame) {
|
174
|
+
// For form-based editors, update the hidden input
|
175
|
+
const form = element.closest('[data-controller="editor-form"]')
|
176
|
+
if (form) {
|
177
|
+
const hiddenInput = form.querySelector('[data-editor-form-target="hiddenField"]')
|
178
|
+
if (hiddenInput) {
|
179
|
+
hiddenInput.value = contentJson
|
180
|
+
hiddenInput.dataset.initialContent = contentJson
|
181
|
+
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }))
|
182
|
+
}
|
183
|
+
}
|
184
|
+
} else {
|
185
|
+
// For iframe-based editors, update the element's data attribute
|
186
|
+
element.setAttribute('data-content', contentJson)
|
187
|
+
element.dispatchEvent(new Event('change', { bubbles: true }))
|
188
|
+
|
189
|
+
// Get the save button from parent window
|
190
|
+
const saveButton = parent.document.getElementById('saveEditableButton')
|
191
|
+
if (saveButton) {
|
192
|
+
// Store the current content on the save button for later use
|
193
|
+
saveButton.dataset.pendingContent = contentJson
|
194
|
+
|
195
|
+
// Add click handler if not already added
|
196
|
+
if (!saveButton.hasAttribute('data-handler-attached')) {
|
197
|
+
saveButton.setAttribute('data-handler-attached', 'true')
|
198
|
+
saveButton.addEventListener('click', async () => {
|
199
|
+
try {
|
200
|
+
const pageId = element.getAttribute("data-editable-page-id")
|
201
|
+
const blockContentId = element.getAttribute("data-editable-block-content-id")
|
202
|
+
const pendingContent = JSON.parse(saveButton.dataset.pendingContent || '{}')
|
203
|
+
|
204
|
+
const response = await fetch(`${this.adminPathValue}/pages/${pageId}/block_contents/${blockContentId}`, {
|
205
|
+
method: "PATCH",
|
206
|
+
headers: {
|
207
|
+
"Content-Type": "application/json",
|
208
|
+
"X-CSRF-Token": this.csrfToken
|
209
|
+
},
|
210
|
+
body: JSON.stringify({ content: pendingContent })
|
211
|
+
})
|
212
|
+
|
213
|
+
if (!response.ok) {
|
214
|
+
throw new Error('Save failed')
|
215
|
+
}
|
216
|
+
|
217
|
+
// Clear pending content after successful save
|
218
|
+
delete saveButton.dataset.pendingContent
|
219
|
+
} catch (error) {
|
220
|
+
console.error('Error saving content:', error)
|
221
|
+
}
|
222
|
+
})
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
} catch (error) {
|
227
|
+
console.error('Error in onChange handler:', error)
|
228
|
+
}
|
229
|
+
}
|
230
|
+
})
|
231
|
+
|
232
|
+
// Store editor instance on the holder element to maintain reference
|
233
|
+
holderElement.editorInstance = editor
|
234
|
+
|
235
|
+
if (!this.withinIFrame) {
|
236
|
+
// Store the editor instance on the controller element for potential future reference
|
237
|
+
const form = element.closest('[data-controller="editor-form"]')
|
238
|
+
if (form) {
|
239
|
+
form.editorInstance = editor
|
240
|
+
}
|
241
|
+
} else {
|
242
|
+
// For iframe editors, store the instance on the element itself
|
243
|
+
element.editorInstance = editor
|
244
|
+
}
|
245
|
+
|
246
|
+
// Return a promise that resolves when the editor is ready
|
247
|
+
return new Promise((resolve, reject) => {
|
248
|
+
const timeout = setTimeout(() => {
|
249
|
+
reject(new Error('Editor initialization timed out'))
|
250
|
+
}, 30000)
|
251
|
+
|
252
|
+
const checkReady = () => {
|
253
|
+
if (editor.isReady) {
|
254
|
+
clearTimeout(timeout)
|
255
|
+
resolve(editor)
|
256
|
+
} else {
|
257
|
+
setTimeout(checkReady, 100)
|
258
|
+
}
|
259
|
+
}
|
260
|
+
checkReady()
|
261
|
+
})
|
262
|
+
}
|
263
|
+
|
264
|
+
/**
|
265
|
+
* Wait for EditorJS core to be available in window
|
266
|
+
*/
|
267
|
+
async waitForEditorJS() {
|
268
|
+
let attempts = 0
|
269
|
+
const maxAttempts = 30 // 3 seconds with 100ms intervals
|
270
|
+
|
271
|
+
await new Promise((resolve, reject) => {
|
272
|
+
const check = () => {
|
273
|
+
attempts++
|
274
|
+
if (window.EditorJS) {
|
275
|
+
resolve()
|
276
|
+
} else if (attempts >= maxAttempts) {
|
277
|
+
reject(new Error('EditorJS core failed to load'))
|
278
|
+
} else {
|
279
|
+
setTimeout(check, 100)
|
280
|
+
}
|
281
|
+
}
|
282
|
+
check()
|
283
|
+
})
|
284
|
+
}
|
285
|
+
}
|
@@ -0,0 +1,110 @@
|
|
1
|
+
export class PlainTextEditor {
|
2
|
+
/**
|
3
|
+
* Constructs a new PlainTextEditor instance.
|
4
|
+
*
|
5
|
+
* @param {HTMLElement} element - The HTML element representing the plain text editor.
|
6
|
+
* @param {HTMLIFrameElement} frame - The HTML iframe element containing the plain text editor.
|
7
|
+
* @param {Object} options - An object containing configuration options for the plain text editor.
|
8
|
+
*/
|
9
|
+
constructor(element, frame, options) {
|
10
|
+
this.element = element
|
11
|
+
this.frame = frame
|
12
|
+
this.options = options
|
13
|
+
this.setupStyles()
|
14
|
+
this.bindEvents()
|
15
|
+
}
|
16
|
+
|
17
|
+
/**
|
18
|
+
* Sets up the styles for the plain text editor element.
|
19
|
+
*
|
20
|
+
* This method applies various CSS styles to the editor element, such as a dashed border, no outline, a pointer cursor, and a background color transition. It also sets the white-space and font-family styles based on the data-editable-kind attribute of the element.
|
21
|
+
*/
|
22
|
+
setupStyles() {
|
23
|
+
this.element.style.border = "1px dashed #ccc"
|
24
|
+
this.element.style.outline = "none"
|
25
|
+
this.element.style.cursor = "pointer"
|
26
|
+
this.element.style.transition = "background 500ms linear"
|
27
|
+
this.element.style.backgroundColor = "inherit"
|
28
|
+
|
29
|
+
if (this.element.getAttribute("data-editable-kind") == "html") {
|
30
|
+
this.element.style.whiteSpace = "pre-wrap"
|
31
|
+
this.element.style.fontFamily = "monospace"
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
/**
|
36
|
+
* Binds event listeners for the plain text editor.
|
37
|
+
*
|
38
|
+
* If the `autosave` option is enabled, this method adds a `blur` event listener to the editor element, which triggers the `save()` method when the editor loses focus.
|
39
|
+
*
|
40
|
+
* Additionally, this method adds a `click` event listener to the "Save Editable" button, which also triggers the `save()` method when clicked.
|
41
|
+
*/
|
42
|
+
bindEvents() {
|
43
|
+
if (this.options.autosave) {
|
44
|
+
this.element.addEventListener("blur", () => this.save())
|
45
|
+
}
|
46
|
+
|
47
|
+
document.getElementById('saveEditableButton').addEventListener('click', () => this.save())
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Saves the content of the plain text editor to the server.
|
52
|
+
*
|
53
|
+
* This method sends a PATCH request to the server with the updated content of the plain text editor. It retrieves the necessary data from the editor element's attributes, such as the block content ID and the content type (HTML or plain text). If the save is successful, it calls the `showSuccess()` method, otherwise it calls the `showError()` method with the error.
|
54
|
+
*/
|
55
|
+
save() {
|
56
|
+
const blockContentId = this.element.getAttribute("data-editable-block-content-id")
|
57
|
+
const pageId = this.element.getAttribute("data-editable-page-id")
|
58
|
+
const content = this.element.getAttribute("data-editable-kind") == "html" ?
|
59
|
+
this.element.innerHTML :
|
60
|
+
this.element.innerText
|
61
|
+
|
62
|
+
fetch(`${this.options.adminPath}/pages/${pageId}/block_contents/${blockContentId}`, {
|
63
|
+
method: "PATCH",
|
64
|
+
headers: {
|
65
|
+
"Content-Type": "application/json",
|
66
|
+
"X-CSRF-Token": this.options.csrfToken
|
67
|
+
},
|
68
|
+
body: JSON.stringify({ content: content })
|
69
|
+
})
|
70
|
+
.then(response => response.json())
|
71
|
+
.then(() => {
|
72
|
+
// Show success message in parent window
|
73
|
+
parent.document.getElementById("successMessage").classList.remove("hidden")
|
74
|
+
setTimeout(() => {
|
75
|
+
parent.document.getElementById("successMessage").classList.add("hidden")
|
76
|
+
}, 3000)
|
77
|
+
// Show visual feedback in the editor
|
78
|
+
this.showSuccess()
|
79
|
+
})
|
80
|
+
.catch(error => this.showError(error))
|
81
|
+
}
|
82
|
+
|
83
|
+
/**
|
84
|
+
* Displays a success message by temporarily changing the background color of the editor element.
|
85
|
+
*
|
86
|
+
* This method is called after a successful save operation to provide visual feedback to the user.
|
87
|
+
*/
|
88
|
+
showSuccess() {
|
89
|
+
this.element.style.backgroundColor = "#66bd6a50"
|
90
|
+
setTimeout(() => {
|
91
|
+
this.element.style.backgroundColor = "inherit"
|
92
|
+
}, 1000)
|
93
|
+
}
|
94
|
+
|
95
|
+
/**
|
96
|
+
* Displays an error message by temporarily changing the background color of the editor element and logging the error to the console.
|
97
|
+
*
|
98
|
+
* This method is called after a failed save operation to provide visual and textual feedback to the user.
|
99
|
+
*
|
100
|
+
* @param {Error} error - The error object that occurred during the save operation.
|
101
|
+
*/
|
102
|
+
showError(error) {
|
103
|
+
this.element.style.backgroundColor = "#dc354550"
|
104
|
+
setTimeout(() => {
|
105
|
+
this.element.style.backgroundColor = "inherit"
|
106
|
+
}, 1000)
|
107
|
+
console.log(error)
|
108
|
+
alert("Error:", error)
|
109
|
+
}
|
110
|
+
}
|
@@ -0,0 +1,115 @@
|
|
1
|
+
export class ResourceLoader {
|
2
|
+
static loadedResources = new Set()
|
3
|
+
|
4
|
+
/**
|
5
|
+
* Embeds CSS styles into the document head.
|
6
|
+
*
|
7
|
+
* @param {Document} frameDocument - The document object to create elements in
|
8
|
+
* @param {HTMLElement} head - The head element to append styles to
|
9
|
+
* @param {string} css - The CSS styles to embed
|
10
|
+
* @returns {Promise} A promise that resolves when the styles are embedded
|
11
|
+
*/
|
12
|
+
static embedCSS(frameDocument, head, css) {
|
13
|
+
const cssHash = this.hashString(css)
|
14
|
+
if (this.loadedResources.has(`css:${cssHash}`)) {
|
15
|
+
console.debug("[Panda CMS] CSS already embedded, skipping")
|
16
|
+
return Promise.resolve()
|
17
|
+
}
|
18
|
+
|
19
|
+
return new Promise((resolve) => {
|
20
|
+
const style = frameDocument.createElement("style")
|
21
|
+
style.textContent = css
|
22
|
+
head.append(style)
|
23
|
+
this.loadedResources.add(`css:${cssHash}`)
|
24
|
+
resolve(style)
|
25
|
+
console.debug("[Panda CMS] Embedded CSS styles")
|
26
|
+
})
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Loads a script from a URL and appends it to the document head.
|
31
|
+
*
|
32
|
+
* @param {Document} frameDocument - The document object to create elements in
|
33
|
+
* @param {HTMLElement} head - The head element to append the script to
|
34
|
+
* @param {string} src - The URL of the script to load
|
35
|
+
* @returns {Promise} A promise that resolves when the script is loaded
|
36
|
+
*/
|
37
|
+
static loadScript(frameDocument, head, src) {
|
38
|
+
if (this.loadedResources.has(`script:${src}`)) {
|
39
|
+
console.debug(`[Panda CMS] Script already loaded: ${src}, skipping`)
|
40
|
+
return Promise.resolve()
|
41
|
+
}
|
42
|
+
|
43
|
+
return new Promise((resolve, reject) => {
|
44
|
+
const script = frameDocument.createElement("script")
|
45
|
+
script.src = src
|
46
|
+
script.onload = () => {
|
47
|
+
this.loadedResources.add(`script:${src}`)
|
48
|
+
resolve(script)
|
49
|
+
console.debug(`[Panda CMS] Script loaded: ${src}`)
|
50
|
+
}
|
51
|
+
script.onerror = () => reject(new Error(`[Panda CMS] Script load error for ${src}`))
|
52
|
+
head.append(script)
|
53
|
+
})
|
54
|
+
}
|
55
|
+
|
56
|
+
static importScript(frameDocument, head, module, src) {
|
57
|
+
const key = `module:${module}:${src}`
|
58
|
+
if (this.loadedResources.has(key)) {
|
59
|
+
console.debug(`[Panda CMS] Module already imported: ${src}, skipping`)
|
60
|
+
return Promise.resolve()
|
61
|
+
}
|
62
|
+
|
63
|
+
return new Promise((resolve, reject) => {
|
64
|
+
const script = frameDocument.createElement("script")
|
65
|
+
script.type = "module"
|
66
|
+
script.textContent = `import ${module} from "${src}"`
|
67
|
+
head.append(script)
|
68
|
+
|
69
|
+
script.onload = () => {
|
70
|
+
this.loadedResources.add(key)
|
71
|
+
console.debug(`[Panda CMS] Module script loaded: ${src}`)
|
72
|
+
resolve(script)
|
73
|
+
}
|
74
|
+
script.onerror = () => reject(new Error(`[Panda CMS] Module script load error for ${src}`))
|
75
|
+
})
|
76
|
+
}
|
77
|
+
|
78
|
+
static loadStylesheet(frameDocument, head, href) {
|
79
|
+
if (this.loadedResources.has(`stylesheet:${href}`)) {
|
80
|
+
console.debug(`[Panda CMS] Stylesheet already loaded: ${href}, skipping`)
|
81
|
+
return Promise.resolve()
|
82
|
+
}
|
83
|
+
|
84
|
+
return new Promise((resolve, reject) => {
|
85
|
+
const link = frameDocument.createElement("link")
|
86
|
+
link.rel = "stylesheet"
|
87
|
+
link.href = href
|
88
|
+
link.media = "none"
|
89
|
+
head.append(link)
|
90
|
+
|
91
|
+
link.onload = () => {
|
92
|
+
if (link.media != "all") {
|
93
|
+
link.media = "all"
|
94
|
+
}
|
95
|
+
this.loadedResources.add(`stylesheet:${href}`)
|
96
|
+
console.debug(`[Panda CMS] Stylesheet loaded: ${href}`)
|
97
|
+
resolve(link)
|
98
|
+
}
|
99
|
+
link.onerror = () => reject(new Error(`[Panda CMS] Stylesheet load error for ${href}`))
|
100
|
+
})
|
101
|
+
}
|
102
|
+
|
103
|
+
/**
|
104
|
+
* Simple string hashing function for tracking embedded CSS
|
105
|
+
*/
|
106
|
+
static hashString(str) {
|
107
|
+
let hash = 0
|
108
|
+
for (let i = 0; i < str.length; i++) {
|
109
|
+
const char = str.charCodeAt(i)
|
110
|
+
hash = ((hash << 5) - hash) + char
|
111
|
+
hash = hash & hash // Convert to 32bit integer
|
112
|
+
}
|
113
|
+
return hash.toString(36)
|
114
|
+
}
|
115
|
+
}
|