@licium/editor-plugin-details 1.0.2 → 1.0.13

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.
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
- import type { PluginContext, PluginInfo, I18n } from '@licium/editor';
1
+ /* eslint-disable */
2
+ import type { PluginContext, PluginInfo, I18n, MdNode } from '@licium/editor';
2
3
  import { PluginOptions } from '@t/index';
3
4
  import { addLangs } from './i18n/langs';
5
+ import './css/plugin.css';
4
6
 
5
7
  const PREFIX = 'toastui-editor-';
6
8
 
@@ -13,6 +15,136 @@ function createToolbarItemOption(i18n: I18n) {
13
15
  };
14
16
  }
15
17
 
18
+ function handleDetailsExit(context: PluginContext, event: KeyboardEvent): void {
19
+ const { instance } = context as any;
20
+
21
+ if (!instance) {
22
+ console.log('[Details Plugin] No instance found');
23
+ return;
24
+ }
25
+
26
+ const editor = instance.getCurrentModeEditor();
27
+
28
+ if (!editor || !editor.view) {
29
+ console.log('[Details Plugin] No editor view found');
30
+ return;
31
+ }
32
+
33
+ const { view } = editor;
34
+ const { state } = view;
35
+ const { selection } = state;
36
+ const { $from } = selection;
37
+
38
+ // Find details node in ancestry
39
+ let detailsDepth = -1;
40
+
41
+ for (let d = $from.depth; d > 0; d -= 1) {
42
+ if ($from.node(d).type.name === 'details') {
43
+ detailsDepth = d;
44
+ break;
45
+ }
46
+ }
47
+
48
+ if (detailsDepth < 0) {
49
+ return;
50
+ }
51
+
52
+ const detailsNode = $from.node(detailsDepth);
53
+ const nodeStart = $from.before(detailsDepth);
54
+ const nodeEnd = nodeStart + detailsNode.nodeSize;
55
+ const afterPara = $from.after();
56
+ const isAtEnd = afterPara >= nodeEnd - 1;
57
+
58
+ console.log('[Details Plugin] Inside details:', {
59
+ key: event.key,
60
+ isAtEnd,
61
+ afterPara,
62
+ nodeEnd,
63
+ parentType: $from.parent.type.name,
64
+ parentEmpty: $from.parent.content.size === 0,
65
+ });
66
+
67
+ if (!isAtEnd) {
68
+ return;
69
+ }
70
+
71
+ const { tr, schema } = state;
72
+ const SelectionClass = state.selection.constructor as any;
73
+
74
+ // Enter on empty paragraph
75
+ if (event.key === 'Enter') {
76
+ const isEmptyPara = $from.parent.type.name === 'paragraph' && $from.parent.content.size === 0;
77
+
78
+ if (!isEmptyPara) {
79
+ return;
80
+ }
81
+
82
+ // Check previous node to see if it's also an empty paragraph
83
+ // $from.index(0) gives the index in the parent details node
84
+ const index = $from.index(detailsDepth);
85
+ const prevNode = detailsNode.child(index - 1);
86
+ const isPrevEmptyPara = prevNode && prevNode.type.name === 'paragraph' && prevNode.content.size === 0;
87
+
88
+ if (!isPrevEmptyPara) {
89
+ console.log('[Details Plugin] Allowing one empty line. Not exiting yet.');
90
+ return;
91
+ }
92
+
93
+ console.log('[Details Plugin] EXITING via Double Enter (Two empty paras)!');
94
+ event.preventDefault();
95
+
96
+ const beforePara = $from.before();
97
+
98
+ // delete the current empty para and the previous one?
99
+ // Usually "Exit" means: Lift the current empty para out.
100
+ // But if we have TWO empty paras, maybe we want to keep one inside?
101
+ // Let's stick to: If two empty paras, we treat it as an exit signal.
102
+ // We should probably remove the *current* empty para and move the cursor out.
103
+
104
+ tr.delete(beforePara, afterPara);
105
+
106
+ const p = schema.nodes.paragraph.createAndFill()!;
107
+ const insertPos = nodeEnd - (afterPara - beforePara);
108
+
109
+ tr.insert(insertPos, p);
110
+
111
+ if (SelectionClass.near) {
112
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(insertPos + 1)));
113
+ }
114
+
115
+ view.dispatch(tr);
116
+ }
117
+
118
+ // ArrowDown at end of content
119
+ if (event.key === 'ArrowDown') {
120
+ const atEndOfParent = $from.parentOffset === $from.parent.content.size;
121
+
122
+ if (!atEndOfParent) {
123
+ return;
124
+ }
125
+
126
+ console.log('[Details Plugin] EXITING via ArrowDown!');
127
+ event.preventDefault();
128
+
129
+ if (nodeEnd < state.doc.content.size) {
130
+ if (SelectionClass.near) {
131
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(nodeEnd)));
132
+ view.dispatch(tr);
133
+ }
134
+ } else {
135
+ const p = schema.nodes.paragraph.createAndFill()!;
136
+
137
+ tr.insert(nodeEnd, p);
138
+
139
+ if (SelectionClass.near) {
140
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(nodeEnd + 1)));
141
+ }
142
+
143
+ view.dispatch(tr);
144
+ }
145
+ }
146
+ }
147
+
16
148
  export default function detailsPlugin(
17
149
  context: PluginContext,
18
150
  options: PluginOptions = {}
@@ -23,10 +155,38 @@ export default function detailsPlugin(
23
155
 
24
156
  const toolbarItem = createToolbarItemOption(i18n);
25
157
 
158
+ // Hook into the global keydown event for exit behavior
159
+ eventEmitter.listen('keydown', (editorType: string, event: KeyboardEvent) => {
160
+ console.log('[Details Plugin] keydown via eventEmitter!', { editorType, key: event.key });
161
+
162
+ if (editorType !== 'wysiwyg') {
163
+ return;
164
+ }
165
+
166
+ if (event.key !== 'Enter' && event.key !== 'ArrowDown') {
167
+ return;
168
+ }
169
+
170
+ handleDetailsExit(context, event);
171
+ });
172
+
26
173
  return {
174
+ toHTMLRenderers: {
175
+ htmlBlock: {
176
+ renderer: (node) => {
177
+ const isDetails = /<(\/)?(details|summary)/i.test(node.literal || '');
178
+ return isDetails
179
+ ? [{ type: 'html', content: node.literal || '' }]
180
+ : [
181
+ { type: 'openTag', tagName: 'div', outerNewLine: true },
182
+ { type: 'html', content: node.literal || '' },
183
+ { type: 'closeTag', tagName: 'div', outerNewLine: true },
184
+ ];
185
+ },
186
+ },
187
+ },
27
188
  markdownCommands: {
28
189
  details: (payload, { tr, selection, schema }, dispatch) => {
29
- const { from, to } = selection;
30
190
  const slice = selection.content();
31
191
  const textContent =
32
192
  slice.content.textBetween(0, slice.content.size, '\n') || i18n.get('content');
@@ -41,26 +201,18 @@ export default function detailsPlugin(
41
201
  return true;
42
202
  },
43
203
  },
44
- toHTMLRenderers: {
45
- htmlBlock: {
46
- details: (node) => [
47
- { type: 'openTag', tagName: 'details', outerNewLine: true },
48
- { type: 'html', content: node.childrenHTML || '' },
49
- { type: 'closeTag', tagName: 'details', outerNewLine: true },
50
- ],
51
- summary: (node) => [
52
- { type: 'openTag', tagName: 'summary', outerNewLine: true },
53
- { type: 'html', content: node.childrenHTML || '' },
54
- { type: 'closeTag', tagName: 'summary', outerNewLine: true },
55
- ],
56
- },
57
- },
204
+
205
+
58
206
  wysiwygNodeViews: {
59
207
  details: (node, view, getPos) => {
60
208
  const dom = document.createElement('details');
61
- const { htmlAttrs } = node.attrs;
209
+ const htmlAttrs = (node.attrs && node.attrs.htmlAttrs) || {};
210
+ const isOpenAttr = node.attrs.open; // Core definition uses direct attribute
62
211
 
63
- if (htmlAttrs.open) {
212
+ // Check both direct attribute (priority) and htmlAttrs (legacy/plugin)
213
+ const isOpen = isOpenAttr === true || (htmlAttrs && htmlAttrs.open !== null && typeof htmlAttrs.open !== 'undefined');
214
+
215
+ if (isOpen) {
64
216
  dom.open = true;
65
217
  }
66
218
 
@@ -73,9 +225,18 @@ export default function detailsPlugin(
73
225
  const pos = getPos();
74
226
 
75
227
  if (typeof pos === 'number') {
228
+ const isOpen = htmlAttrs.open !== null && typeof htmlAttrs.open !== 'undefined';
229
+ const newHtmlAttrs = { ...htmlAttrs };
230
+
231
+ if (isOpen) {
232
+ delete newHtmlAttrs.open;
233
+ } else {
234
+ newHtmlAttrs.open = '';
235
+ }
236
+
76
237
  const newAttrs = {
77
238
  ...node.attrs,
78
- htmlAttrs: { ...htmlAttrs, open: !htmlAttrs.open },
239
+ htmlAttrs: newHtmlAttrs,
79
240
  };
80
241
 
81
242
  view.dispatch(view.state.tr.setNodeMarkup(pos, null, newAttrs));
@@ -83,115 +244,11 @@ export default function detailsPlugin(
83
244
  }
84
245
  });
85
246
 
86
- // Simplified keydown handler with better logic
87
- dom.addEventListener('keydown', (e) => {
88
- const { state } = view;
89
- const { selection } = state;
90
- const pos = getPos();
91
-
92
- if (typeof pos !== 'number') {
93
- return;
94
- }
95
-
96
- // Get fresh node
97
- const currentNode = state.doc.nodeAt(pos);
98
-
99
- if (!currentNode || currentNode.type.name !== 'details') {
100
- return;
101
- }
102
-
103
- // Calculate end position
104
- const endPos = pos + currentNode.nodeSize;
105
-
106
- // Get TextSelection constructor
107
- const { constructor: SelectionCtor } = state.selection as any;
108
-
109
- // Handle Enter key
110
- if (e.key === 'Enter') {
111
- const { $from } = selection;
112
-
113
- // Check if we're in an empty paragraph
114
- if ($from.parent.type.name === 'paragraph' && $from.parent.content.size === 0) {
115
- // Get position after this paragraph
116
- const afterPara = $from.after();
117
-
118
- // Check if this empty paragraph is the last thing in the details block
119
- // endPos - 1 is the position just before the closing tag
120
- if (afterPara >= endPos - 1) {
121
- e.preventDefault();
122
- e.stopPropagation();
123
-
124
- const { tr, schema } = state;
125
-
126
- // Delete the empty paragraph
127
- const beforePara = $from.before();
128
-
129
- tr.delete(beforePara, afterPara);
130
-
131
- // Insert new paragraph after the details block
132
- const p = schema.nodes.paragraph.createAndFill()!;
133
- const insertPos = endPos - (afterPara - beforePara);
134
-
135
- tr.insert(insertPos, p);
136
-
137
- // Move cursor into new paragraph
138
- if (SelectionCtor && SelectionCtor.near) {
139
- tr.setSelection(SelectionCtor.near(tr.doc.resolve(insertPos + 1)));
140
- }
141
-
142
- view.dispatch(tr);
143
- }
144
- }
145
- }
146
-
147
- // Handle ArrowDown key
148
- if (e.key === 'ArrowDown') {
149
- const { $from } = selection;
150
-
151
- // Check if cursor is at the end of its parent node
152
- const atEndOfParent = $from.parentOffset === $from.parent.content.size;
153
-
154
- if (!atEndOfParent) {
155
- return;
156
- }
157
-
158
- // Check if parent is the last child in details
159
- const afterPara = $from.after();
160
-
161
- if (afterPara >= endPos - 1) {
162
- e.preventDefault();
163
- e.stopPropagation();
164
-
165
- const { tr, schema } = state;
166
-
167
- // Try to move to position after details block
168
- if (endPos < state.doc.content.size) {
169
- // There's content after, just move there
170
- if (SelectionCtor && SelectionCtor.near) {
171
- tr.setSelection(SelectionCtor.near(tr.doc.resolve(endPos)));
172
- view.dispatch(tr);
173
- }
174
- } else {
175
- // No content after, create a new paragraph
176
- const p = schema.nodes.paragraph.createAndFill()!;
177
-
178
- tr.insert(endPos, p);
179
-
180
- if (SelectionCtor && SelectionCtor.near) {
181
- tr.setSelection(SelectionCtor.near(tr.doc.resolve(endPos + 1)));
182
- }
183
-
184
- view.dispatch(tr);
185
- }
186
- }
187
- }
188
- });
189
-
190
247
  return { dom, contentDOM: dom };
191
248
  },
192
249
  },
193
250
  wysiwygCommands: {
194
- details: (payload, { tr, selection, schema }, dispatch) => {
251
+ details: (payload, { tr, schema }, dispatch) => {
195
252
  const summaryText = i18n.get('summary');
196
253
  const contentText = i18n.get('content');
197
254
 
@@ -200,9 +257,9 @@ export default function detailsPlugin(
200
257
  if (details && summary && paragraph) {
201
258
  const summaryNode = summary.create({}, schema.text(summaryText));
202
259
  const contentNode = paragraph.create({}, schema.text(contentText));
203
- const node = details.create({}, [summaryNode, contentNode]);
260
+ const detailsNode = details.create({}, [summaryNode, contentNode]);
204
261
 
205
- tr.replaceSelectionWith(node);
262
+ tr.replaceSelectionWith(detailsNode);
206
263
  dispatch!(tr);
207
264
  return true;
208
265
  }
@@ -212,8 +269,8 @@ export default function detailsPlugin(
212
269
  },
213
270
  toolbarItems: [
214
271
  {
215
- groupIndex: 0,
216
- itemIndex: 4,
272
+ groupIndex: 4,
273
+ itemIndex: 3,
217
274
  item: toolbarItem,
218
275
  },
219
276
  ],