panda-editor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +28 -0
- data/README.md +146 -0
- data/app/javascript/panda/editor/application.js +25 -0
- data/app/javascript/panda/editor/css_extractor.js +80 -0
- data/app/javascript/panda/editor/editor_js_config.js +306 -0
- data/app/javascript/panda/editor/editor_js_initializer.js +334 -0
- data/app/javascript/panda/editor/plain_text_editor.js +110 -0
- data/app/javascript/panda/editor/resource_loader.js +204 -0
- data/app/javascript/panda/editor/rich_text_editor.js +162 -0
- data/app/services/panda/editor/html_to_editor_js_converter.rb +195 -0
- data/config/importmap.rb +15 -0
- data/lib/panda/editor/asset_loader.rb +151 -0
- data/lib/panda/editor/blocks/alert.rb +34 -0
- data/lib/panda/editor/blocks/base.rb +33 -0
- data/lib/panda/editor/blocks/header.rb +15 -0
- data/lib/panda/editor/blocks/image.rb +37 -0
- data/lib/panda/editor/blocks/list.rb +32 -0
- data/lib/panda/editor/blocks/paragraph.rb +16 -0
- data/lib/panda/editor/blocks/quote.rb +42 -0
- data/lib/panda/editor/blocks/table.rb +50 -0
- data/lib/panda/editor/content.rb +61 -0
- data/lib/panda/editor/engine.rb +34 -0
- data/lib/panda/editor/renderer.rb +125 -0
- data/lib/panda/editor/version.rb +7 -0
- data/lib/panda/editor.rb +25 -0
- data/lib/tasks/assets.rake +110 -0
- data/panda-editor.gemspec +40 -0
- metadata +142 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a96fbed654e5115930ed44a6e5561c732a36c27be96d10888989570ec4c6a705
|
4
|
+
data.tar.gz: 0efd2b485067e1f83d59e2903cab40279dc8830fdbe073b5ae3ae5d31346ff29
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7961ae1af4bddbdd37543d7c73cffd3aaf3962cd75815c1206c6023cebc1e2e1095ac1d5de218132f4041e94eb1a3a48a02b4bc8d17f4b92655ba74c33a88427
|
7
|
+
data.tar.gz: '0602738109c87304058c6fd0c52cfc87f71ab19ed3fcc1b6766da150687c2bab232358395c31dbfc9e41f402377944039590defb03e3b4c0aa4ccf05e78a8298'
|
data/LICENSE
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
BSD 3-Clause License
|
2
|
+
|
3
|
+
Copyright © 2024, Otaina Limited
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
9
|
+
list of conditions and the following disclaimer.
|
10
|
+
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
13
|
+
and/or other materials provided with the distribution.
|
14
|
+
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
16
|
+
contributors may be used to endorse or promote products derived from
|
17
|
+
this software without specific prior written permission.
|
18
|
+
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
# Panda Editor
|
2
|
+
|
3
|
+
A modular, extensible rich text editor using EditorJS for Rails applications. Extracted from [Panda CMS](https://github.com/tastybamboo/panda-cms).
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- 🎨 **Rich Content Blocks**: Paragraph, Header, List, Quote, Table, Image, Alert, and more
|
8
|
+
- 🔧 **Extensible Architecture**: Easy to add custom block types
|
9
|
+
- 🚀 **Rails Integration**: Works seamlessly with Rails 7.1+
|
10
|
+
- 💾 **Smart Caching**: Automatic HTML caching for performance
|
11
|
+
- 🎯 **Clean API**: Simple concern-based integration for ActiveRecord models
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Add this line to your application's Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'panda-editor'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
bundle install
|
25
|
+
```
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Basic Setup
|
30
|
+
|
31
|
+
Include the concern in your model:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
class Post < ApplicationRecord
|
35
|
+
include Panda::Editor::Content
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
This adds:
|
40
|
+
- `content` field for storing EditorJS JSON
|
41
|
+
- `cached_content` field for storing rendered HTML
|
42
|
+
- Automatic HTML generation on save
|
43
|
+
|
44
|
+
### Rendering Content
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# In your controller
|
48
|
+
@post = Post.find(params[:id])
|
49
|
+
|
50
|
+
# In your view
|
51
|
+
<%= raw @post.cached_content %>
|
52
|
+
|
53
|
+
# Or render directly from JSON
|
54
|
+
renderer = Panda::Editor::Renderer.new(@post.content)
|
55
|
+
<%= raw renderer.render %>
|
56
|
+
```
|
57
|
+
|
58
|
+
### JavaScript Integration
|
59
|
+
|
60
|
+
In your application.js:
|
61
|
+
|
62
|
+
```javascript
|
63
|
+
import { EditorJSInitializer } from "panda/editor"
|
64
|
+
|
65
|
+
// Initialize an editor
|
66
|
+
const element = document.querySelector("#editor")
|
67
|
+
const editor = new EditorJSInitializer(element, {
|
68
|
+
data: existingContent,
|
69
|
+
onSave: (data) => {
|
70
|
+
// Handle save
|
71
|
+
}
|
72
|
+
})
|
73
|
+
```
|
74
|
+
|
75
|
+
### Custom Block Types
|
76
|
+
|
77
|
+
Create a custom block:
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
class CustomBlock < Panda::Editor::Blocks::Base
|
81
|
+
def render
|
82
|
+
html_safe("<div class='custom'>#{sanitize(data['text'])}</div>")
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Register it
|
87
|
+
Panda::Editor::Engine.config.custom_renderers['custom'] = CustomBlock
|
88
|
+
```
|
89
|
+
|
90
|
+
## Available Blocks
|
91
|
+
|
92
|
+
- **Paragraph**: Standard text content
|
93
|
+
- **Header**: H1-H6 headings
|
94
|
+
- **List**: Ordered and unordered lists
|
95
|
+
- **Quote**: Blockquotes with captions
|
96
|
+
- **Table**: Tables with optional headers
|
97
|
+
- **Image**: Images with captions and styling options
|
98
|
+
- **Alert**: Alert/notification boxes
|
99
|
+
|
100
|
+
## Configuration
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
# config/initializers/panda_editor.rb
|
104
|
+
Panda::Editor::Engine.configure do |config|
|
105
|
+
# Add custom EditorJS tools
|
106
|
+
config.editor_js_tools = ['customTool']
|
107
|
+
|
108
|
+
# Register custom renderers
|
109
|
+
config.custom_renderers = {
|
110
|
+
'customBlock' => MyCustomBlockRenderer
|
111
|
+
}
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
## Assets
|
116
|
+
|
117
|
+
### Development
|
118
|
+
Uses Rails importmaps for individual module loading.
|
119
|
+
|
120
|
+
### Production
|
121
|
+
Compiled assets are automatically downloaded from GitHub releases or can be compiled locally:
|
122
|
+
|
123
|
+
```bash
|
124
|
+
rake panda_editor:assets:compile
|
125
|
+
```
|
126
|
+
|
127
|
+
## Development
|
128
|
+
|
129
|
+
After checking out the repo, run:
|
130
|
+
|
131
|
+
```bash
|
132
|
+
bundle install
|
133
|
+
bundle exec rspec
|
134
|
+
```
|
135
|
+
|
136
|
+
## Contributing
|
137
|
+
|
138
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/tastybamboo/panda-editor.
|
139
|
+
|
140
|
+
## License
|
141
|
+
|
142
|
+
The gem is available as open source under the terms of the [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause).
|
143
|
+
|
144
|
+
## Copyright
|
145
|
+
|
146
|
+
Copyright © 2024, Otaina Limited
|
@@ -0,0 +1,25 @@
|
|
1
|
+
// Panda Editor Application JavaScript
|
2
|
+
// This file serves as the entry point for all EditorJS functionality
|
3
|
+
|
4
|
+
import { EditorJSInitializer } from "./editor_js_initializer"
|
5
|
+
import { EditorJSConfig } from "./editor_js_config"
|
6
|
+
import { RichTextEditor } from "./rich_text_editor"
|
7
|
+
|
8
|
+
// Export for global access
|
9
|
+
window.PandaEditor = {
|
10
|
+
EditorJSInitializer,
|
11
|
+
EditorJSConfig,
|
12
|
+
RichTextEditor,
|
13
|
+
VERSION: "0.1.0"
|
14
|
+
}
|
15
|
+
|
16
|
+
// Auto-initialize on DOMContentLoaded
|
17
|
+
document.addEventListener("DOMContentLoaded", () => {
|
18
|
+
console.log("[Panda Editor] Loaded v" + window.PandaEditor.VERSION)
|
19
|
+
|
20
|
+
// Auto-initialize any editors on the page
|
21
|
+
const editors = document.querySelectorAll("[data-panda-editor]")
|
22
|
+
editors.forEach(element => {
|
23
|
+
new EditorJSInitializer(element)
|
24
|
+
})
|
25
|
+
})
|
@@ -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
|
+
}
|
@@ -0,0 +1,306 @@
|
|
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
|
+
.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; }
|
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
|
+
}
|
174
|
+
|
175
|
+
/* Ensure last block has bottom spacing */
|
176
|
+
.ce-block:last-child {
|
177
|
+
padding-bottom: 2em;
|
178
|
+
}
|
179
|
+
|
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
|
+
`
|
189
|
+
|
190
|
+
export const getEditorConfig = (elementId, previousData, doc = document) => {
|
191
|
+
// Validate holder element exists
|
192
|
+
const holder = doc.getElementById(elementId)
|
193
|
+
if (!holder) {
|
194
|
+
throw new Error(`Editor holder element ${elementId} not found`)
|
195
|
+
}
|
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
|
+
|
203
|
+
const config = {
|
204
|
+
holder: elementId,
|
205
|
+
data: previousData || {},
|
206
|
+
placeholder: 'Click the + button to add content...',
|
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
|
+
},
|
222
|
+
tools: {
|
223
|
+
header: {
|
224
|
+
class: win.Header,
|
225
|
+
inlineToolbar: true,
|
226
|
+
config: {
|
227
|
+
placeholder: 'Enter a header',
|
228
|
+
levels: [1, 2, 3, 4, 5, 6],
|
229
|
+
defaultLevel: 2
|
230
|
+
}
|
231
|
+
},
|
232
|
+
paragraph: {
|
233
|
+
class: win.Paragraph,
|
234
|
+
inlineToolbar: true,
|
235
|
+
config: {
|
236
|
+
placeholder: 'Start writing or press Tab to add content...'
|
237
|
+
}
|
238
|
+
},
|
239
|
+
list: {
|
240
|
+
class: win.NestedList,
|
241
|
+
inlineToolbar: true,
|
242
|
+
config: {
|
243
|
+
defaultStyle: 'unordered',
|
244
|
+
enableLineBreaks: true
|
245
|
+
}
|
246
|
+
},
|
247
|
+
quote: {
|
248
|
+
class: win.Quote,
|
249
|
+
inlineToolbar: true,
|
250
|
+
config: {
|
251
|
+
quotePlaceholder: 'Enter a quote',
|
252
|
+
captionPlaceholder: 'Quote\'s author'
|
253
|
+
}
|
254
|
+
},
|
255
|
+
image: {
|
256
|
+
class: win.SimpleImage,
|
257
|
+
inlineToolbar: true,
|
258
|
+
config: {
|
259
|
+
placeholder: 'Paste an image URL...'
|
260
|
+
}
|
261
|
+
},
|
262
|
+
table: {
|
263
|
+
class: win.Table,
|
264
|
+
inlineToolbar: true,
|
265
|
+
config: {
|
266
|
+
rows: 2,
|
267
|
+
cols: 2
|
268
|
+
}
|
269
|
+
},
|
270
|
+
embed: {
|
271
|
+
class: win.Embed,
|
272
|
+
inlineToolbar: true,
|
273
|
+
config: {
|
274
|
+
services: {
|
275
|
+
youtube: true,
|
276
|
+
vimeo: true
|
277
|
+
}
|
278
|
+
}
|
279
|
+
}
|
280
|
+
}
|
281
|
+
}
|
282
|
+
|
283
|
+
// Remove any undefined tools from the config
|
284
|
+
config.tools = Object.fromEntries(
|
285
|
+
Object.entries(config.tools)
|
286
|
+
.filter(([_, value]) => value?.class !== undefined)
|
287
|
+
.map(([name, tool]) => {
|
288
|
+
if (!tool.class) {
|
289
|
+
throw new Error(`Tool ${name} has no class defined`)
|
290
|
+
}
|
291
|
+
return [name, tool]
|
292
|
+
})
|
293
|
+
)
|
294
|
+
|
295
|
+
// Allow applications to customize the config through Ruby
|
296
|
+
if (window.PANDA_CMS_EDITOR_JS_CONFIG) {
|
297
|
+
Object.assign(config.tools, window.PANDA_CMS_EDITOR_JS_CONFIG)
|
298
|
+
}
|
299
|
+
|
300
|
+
// Allow applications to customize the config through JavaScript
|
301
|
+
if (typeof window.customizeEditorJS === 'function') {
|
302
|
+
window.customizeEditorJS(config)
|
303
|
+
}
|
304
|
+
|
305
|
+
return config
|
306
|
+
}
|