panda-editor 0.2.1 → 0.3.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.
@@ -0,0 +1,280 @@
1
+ /**
2
+ * ParagraphWithFootnotes - Custom Paragraph Block for EditorJS with Footnote Support
3
+ *
4
+ * This extends the default Paragraph tool to add support for storing and
5
+ * managing footnote data alongside the text content.
6
+ */
7
+
8
+ export default class ParagraphWithFootnotes {
9
+ /**
10
+ * EditorJS block tool interface
11
+ */
12
+ static get toolbox() {
13
+ return {
14
+ title: 'Paragraph',
15
+ icon: '<svg width="17" height="15" viewBox="0 0 336 276" xmlns="http://www.w3.org/2000/svg"><path d="M291 150V79c0-19-15-34-34-34H79c-19 0-34 15-34 34v42l67-44 81 72 56-29 42 30zm0 52l-43-30-56 30-81-67-66 39v23c0 19 15 34 34 34h178c17 0 31-13 34-29zM79 0h178c44 0 79 35 79 79v118c0 44-35 79-79 79H79c-44 0-79-35-79-79V79C0 35 35 0 79 0z"/></svg>'
16
+ };
17
+ }
18
+
19
+ static get contentless() {
20
+ return false;
21
+ }
22
+
23
+ static get enableLineBreaks() {
24
+ return true;
25
+ }
26
+
27
+ static get DEFAULT_PLACEHOLDER() {
28
+ return 'Start writing or press Tab to add content...';
29
+ }
30
+
31
+ /**
32
+ * Allow to press Enter inside the CodeTool textarea
33
+ * @returns {boolean}
34
+ * @public
35
+ */
36
+ static get enableLineBreaks() {
37
+ return true;
38
+ }
39
+
40
+ /**
41
+ * Constructor
42
+ * @param {object} params - Tool parameters
43
+ * @param {object} params.data - Previously saved data
44
+ * @param {object} params.config - Tool config
45
+ * @param {object} params.api - EditorJS API
46
+ * @param {boolean} params.readOnly - Read-only mode
47
+ */
48
+ constructor({ data, config, api, readOnly }) {
49
+ this.api = api;
50
+ this.readOnly = readOnly;
51
+
52
+ this._CSS = {
53
+ block: this.api.styles.block,
54
+ wrapper: 'ce-paragraph'
55
+ };
56
+
57
+ if (!this.readOnly) {
58
+ this.onKeyUp = this.onKeyUp.bind(this);
59
+ }
60
+
61
+ this._placeholder = config.placeholder ? config.placeholder : ParagraphWithFootnotes.DEFAULT_PLACEHOLDER;
62
+ this._data = {};
63
+ this._element = null;
64
+ this._preserveBlank = config.preserveBlank !== undefined ? config.preserveBlank : false;
65
+
66
+ this.data = data;
67
+ }
68
+
69
+ /**
70
+ * Create paragraph element
71
+ * @returns {HTMLElement}
72
+ */
73
+ render() {
74
+ const div = document.createElement('DIV');
75
+ div.classList.add(this._CSS.wrapper, this._CSS.block);
76
+ div.contentEditable = !this.readOnly;
77
+ div.dataset.placeholder = this.api.i18n.t(this._placeholder);
78
+
79
+ if (this._data.text) {
80
+ div.innerHTML = this._data.text;
81
+ }
82
+
83
+ if (!this.readOnly) {
84
+ div.addEventListener('keyup', this.onKeyUp);
85
+ }
86
+
87
+ this._element = div;
88
+
89
+ return div;
90
+ }
91
+
92
+ /**
93
+ * Handle keyup event
94
+ * @param {KeyboardEvent} event
95
+ */
96
+ onKeyUp(event) {
97
+ if (event.code !== 'Backspace' && event.code !== 'Delete') {
98
+ return;
99
+ }
100
+
101
+ const { textContent } = this._element;
102
+
103
+ if (textContent === '') {
104
+ this._element.innerHTML = '';
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Validate saved data
110
+ * @param {object} savedData - Data to validate
111
+ * @returns {boolean}
112
+ */
113
+ validate(savedData) {
114
+ if (savedData.text?.trim() === '' && !this._preserveBlank) {
115
+ return false;
116
+ }
117
+
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Save paragraph data
123
+ * @param {HTMLElement} toolsContent - Paragraph element
124
+ * @returns {object} Saved data
125
+ */
126
+ save(toolsContent) {
127
+ // Clone the element to work with it without modifying the DOM
128
+ const clone = toolsContent.cloneNode(true);
129
+
130
+ // Extract footnotes before removing markers
131
+ const footnotes = this.extractFootnotes(clone);
132
+
133
+ // Remove all footnote markers from the clone to get clean text
134
+ const markers = clone.querySelectorAll('.footnote-marker');
135
+ markers.forEach(marker => marker.remove());
136
+
137
+ const data = {
138
+ text: clone.innerHTML
139
+ };
140
+
141
+ // Add footnotes array if any exist
142
+ if (footnotes.length > 0) {
143
+ data.footnotes = footnotes;
144
+ }
145
+
146
+ return data;
147
+ }
148
+
149
+ /**
150
+ * Extract footnotes from the paragraph content
151
+ * @param {HTMLElement} element - Paragraph element
152
+ * @returns {Array} Array of footnote objects
153
+ */
154
+ extractFootnotes(element) {
155
+ const footnotes = [];
156
+ const markers = element.querySelectorAll('.footnote-marker');
157
+
158
+ markers.forEach((marker) => {
159
+ const footnoteId = marker.dataset.footnoteId;
160
+ const footnoteContent = marker.dataset.footnoteContent;
161
+
162
+ if (footnoteId && footnoteContent) {
163
+ // Calculate position by getting all text before this marker
164
+ const range = document.createRange();
165
+ range.selectNodeContents(element);
166
+ range.setEnd(marker, 0);
167
+ const position = range.toString().length;
168
+
169
+ footnotes.push({
170
+ id: footnoteId,
171
+ content: footnoteContent,
172
+ position: position
173
+ });
174
+ }
175
+ });
176
+
177
+ return footnotes;
178
+ }
179
+
180
+ /**
181
+ * Merge tool with similar block
182
+ * @param {object} data - Saved data from other block
183
+ */
184
+ merge(data) {
185
+ const newData = {
186
+ text: this.data.text + data.text
187
+ };
188
+
189
+ this.data = newData;
190
+ }
191
+
192
+ /**
193
+ * Get current data
194
+ * @returns {object}
195
+ */
196
+ get data() {
197
+ let text = this._element ? this._element.innerHTML : this._data.text || '';
198
+
199
+ this._data.text = text;
200
+
201
+ // Also update footnotes from current markers
202
+ if (this._element) {
203
+ const footnotes = this.extractFootnotes(this._element);
204
+ if (footnotes.length > 0) {
205
+ this._data.footnotes = footnotes;
206
+ }
207
+ }
208
+
209
+ return this._data;
210
+ }
211
+
212
+ /**
213
+ * Set data
214
+ * @param {object} data - Data to set
215
+ */
216
+ set data(data) {
217
+ this._data = data || {};
218
+
219
+ if (this._element) {
220
+ this._element.innerHTML = this._data.text || '';
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Used by EditorJS paste handling
226
+ * @param {string} content - Content to set
227
+ */
228
+ onPaste(event) {
229
+ const data = {
230
+ text: event.detail.data.innerHTML
231
+ };
232
+
233
+ this.data = data;
234
+ }
235
+
236
+ /**
237
+ * Enable Conversion Toolbar
238
+ * @returns {object}
239
+ */
240
+ static get conversionConfig() {
241
+ return {
242
+ export: 'text', // use 'text' property for other blocks
243
+ import: 'text' // fill 'text' property from other block's export string
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Sanitizer rules
249
+ * @returns {object}
250
+ */
251
+ static get sanitize() {
252
+ return {
253
+ text: {
254
+ br: true,
255
+ sup: {
256
+ class: 'footnote-marker',
257
+ 'data-footnote-id': true,
258
+ 'data-footnote-content': true
259
+ }
260
+ }
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Returns true to notify core that read-only is supported
266
+ * @returns {boolean}
267
+ */
268
+ static get isReadOnlySupported() {
269
+ return true;
270
+ }
271
+
272
+ /**
273
+ * Get current Tool`s data
274
+ * @returns {object} Current data
275
+ * @public
276
+ */
277
+ get currentBlock() {
278
+ return this.api.blocks.getCurrentBlockIndex();
279
+ }
280
+ }