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 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
+ }