@licium/editor-plugin-details 1.0.2 → 1.0.3

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.
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * TOAST UI Editor : Text Align Plugin
3
- * @version 1.0.1 | Sat Jan 03 2026
3
+ * @version 1.0.2 | Sun Jan 04 2026
4
4
  * @author NHN Cloud FE Development Lab <dl_javascript@nhn.com>
5
5
  * @license MIT
6
6
  */
@@ -474,16 +474,112 @@ function createToolbarItemOption(i18n) {
474
474
  command: 'details',
475
475
  };
476
476
  }
477
+ function handleDetailsExit(context, event) {
478
+ var instance = context.instance;
479
+ if (!instance) {
480
+ console.log('[Details Plugin] No instance found');
481
+ return;
482
+ }
483
+ var editor = instance.getCurrentModeEditor();
484
+ if (!editor || !editor.view) {
485
+ console.log('[Details Plugin] No editor view found');
486
+ return;
487
+ }
488
+ var view = editor.view;
489
+ var state = view.state;
490
+ var selection = state.selection;
491
+ var $from = selection.$from;
492
+ // Find details node in ancestry
493
+ var detailsDepth = -1;
494
+ for (var d = $from.depth; d > 0; d -= 1) {
495
+ if ($from.node(d).type.name === 'details') {
496
+ detailsDepth = d;
497
+ break;
498
+ }
499
+ }
500
+ if (detailsDepth < 0) {
501
+ return;
502
+ }
503
+ var detailsNode = $from.node(detailsDepth);
504
+ var nodeStart = $from.before(detailsDepth);
505
+ var nodeEnd = nodeStart + detailsNode.nodeSize;
506
+ var afterPara = $from.after();
507
+ var isAtEnd = afterPara >= nodeEnd - 1;
508
+ console.log('[Details Plugin] Inside details:', {
509
+ key: event.key,
510
+ isAtEnd: isAtEnd,
511
+ afterPara: afterPara,
512
+ nodeEnd: nodeEnd,
513
+ parentType: $from.parent.type.name,
514
+ parentEmpty: $from.parent.content.size === 0,
515
+ });
516
+ if (!isAtEnd) {
517
+ return;
518
+ }
519
+ var tr = state.tr, schema = state.schema;
520
+ var SelectionClass = state.selection.constructor;
521
+ // Enter on empty paragraph
522
+ if (event.key === 'Enter') {
523
+ var isEmptyPara = $from.parent.type.name === 'paragraph' && $from.parent.content.size === 0;
524
+ if (!isEmptyPara) {
525
+ return;
526
+ }
527
+ console.log('[Details Plugin] EXITING via Enter!');
528
+ event.preventDefault();
529
+ var beforePara = $from.before();
530
+ tr.delete(beforePara, afterPara);
531
+ var p = schema.nodes.paragraph.createAndFill();
532
+ var insertPos = nodeEnd - (afterPara - beforePara);
533
+ tr.insert(insertPos, p);
534
+ if (SelectionClass.near) {
535
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(insertPos + 1)));
536
+ }
537
+ view.dispatch(tr);
538
+ }
539
+ // ArrowDown at end of content
540
+ if (event.key === 'ArrowDown') {
541
+ var atEndOfParent = $from.parentOffset === $from.parent.content.size;
542
+ if (!atEndOfParent) {
543
+ return;
544
+ }
545
+ console.log('[Details Plugin] EXITING via ArrowDown!');
546
+ event.preventDefault();
547
+ if (nodeEnd < state.doc.content.size) {
548
+ if (SelectionClass.near) {
549
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(nodeEnd)));
550
+ view.dispatch(tr);
551
+ }
552
+ }
553
+ else {
554
+ var p = schema.nodes.paragraph.createAndFill();
555
+ tr.insert(nodeEnd, p);
556
+ if (SelectionClass.near) {
557
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(nodeEnd + 1)));
558
+ }
559
+ view.dispatch(tr);
560
+ }
561
+ }
562
+ }
477
563
  function detailsPlugin(context, options) {
478
564
  if (options === void 0) { options = {}; }
479
565
  var i18n = context.i18n, eventEmitter = context.eventEmitter;
480
566
  addLangs(i18n);
481
567
  var toolbarItem = createToolbarItemOption(i18n);
568
+ // Hook into the global keydown event for exit behavior
569
+ eventEmitter.listen('keydown', function (editorType, event) {
570
+ console.log('[Details Plugin] keydown via eventEmitter!', { editorType: editorType, key: event.key });
571
+ if (editorType !== 'wysiwyg') {
572
+ return;
573
+ }
574
+ if (event.key !== 'Enter' && event.key !== 'ArrowDown') {
575
+ return;
576
+ }
577
+ handleDetailsExit(context, event);
578
+ });
482
579
  return {
483
580
  markdownCommands: {
484
581
  details: function (payload, _a, dispatch) {
485
582
  var tr = _a.tr, selection = _a.selection, schema = _a.schema;
486
- var from = selection.from, to = selection.to;
487
583
  var slice = selection.content();
488
584
  var textContent = slice.content.textBetween(0, slice.content.size, '\n') || i18n.get('content');
489
585
  var summaryText = i18n.get('summary');
@@ -527,99 +623,20 @@ function detailsPlugin(context, options) {
527
623
  }
528
624
  }
529
625
  });
530
- // Simplified keydown handler with better logic
531
- dom.addEventListener('keydown', function (e) {
532
- var state = view.state;
533
- var selection = state.selection;
534
- var pos = getPos();
535
- if (typeof pos !== 'number') {
536
- return;
537
- }
538
- // Get fresh node
539
- var currentNode = state.doc.nodeAt(pos);
540
- if (!currentNode || currentNode.type.name !== 'details') {
541
- return;
542
- }
543
- // Calculate end position
544
- var endPos = pos + currentNode.nodeSize;
545
- // Get TextSelection constructor
546
- var SelectionCtor = state.selection.constructor;
547
- // Handle Enter key
548
- if (e.key === 'Enter') {
549
- var $from = selection.$from;
550
- // Check if we're in an empty paragraph
551
- if ($from.parent.type.name === 'paragraph' && $from.parent.content.size === 0) {
552
- // Get position after this paragraph
553
- var afterPara = $from.after();
554
- // Check if this empty paragraph is the last thing in the details block
555
- // endPos - 1 is the position just before the closing tag
556
- if (afterPara >= endPos - 1) {
557
- e.preventDefault();
558
- e.stopPropagation();
559
- var tr = state.tr, schema = state.schema;
560
- // Delete the empty paragraph
561
- var beforePara = $from.before();
562
- tr.delete(beforePara, afterPara);
563
- // Insert new paragraph after the details block
564
- var p = schema.nodes.paragraph.createAndFill();
565
- var insertPos = endPos - (afterPara - beforePara);
566
- tr.insert(insertPos, p);
567
- // Move cursor into new paragraph
568
- if (SelectionCtor && SelectionCtor.near) {
569
- tr.setSelection(SelectionCtor.near(tr.doc.resolve(insertPos + 1)));
570
- }
571
- view.dispatch(tr);
572
- }
573
- }
574
- }
575
- // Handle ArrowDown key
576
- if (e.key === 'ArrowDown') {
577
- var $from = selection.$from;
578
- // Check if cursor is at the end of its parent node
579
- var atEndOfParent = $from.parentOffset === $from.parent.content.size;
580
- if (!atEndOfParent) {
581
- return;
582
- }
583
- // Check if parent is the last child in details
584
- var afterPara = $from.after();
585
- if (afterPara >= endPos - 1) {
586
- e.preventDefault();
587
- e.stopPropagation();
588
- var tr = state.tr, schema = state.schema;
589
- // Try to move to position after details block
590
- if (endPos < state.doc.content.size) {
591
- // There's content after, just move there
592
- if (SelectionCtor && SelectionCtor.near) {
593
- tr.setSelection(SelectionCtor.near(tr.doc.resolve(endPos)));
594
- view.dispatch(tr);
595
- }
596
- }
597
- else {
598
- // No content after, create a new paragraph
599
- var p = schema.nodes.paragraph.createAndFill();
600
- tr.insert(endPos, p);
601
- if (SelectionCtor && SelectionCtor.near) {
602
- tr.setSelection(SelectionCtor.near(tr.doc.resolve(endPos + 1)));
603
- }
604
- view.dispatch(tr);
605
- }
606
- }
607
- }
608
- });
609
626
  return { dom: dom, contentDOM: dom };
610
627
  },
611
628
  },
612
629
  wysiwygCommands: {
613
630
  details: function (payload, _a, dispatch) {
614
- var tr = _a.tr, selection = _a.selection, schema = _a.schema;
631
+ var tr = _a.tr, schema = _a.schema;
615
632
  var summaryText = i18n.get('summary');
616
633
  var contentText = i18n.get('content');
617
634
  var _b = schema.nodes, details = _b.details, summary = _b.summary, paragraph = _b.paragraph;
618
635
  if (details && summary && paragraph) {
619
636
  var summaryNode = summary.create({}, schema.text(summaryText));
620
637
  var contentNode = paragraph.create({}, schema.text(contentText));
621
- var node = details.create({}, [summaryNode, contentNode]);
622
- tr.replaceSelectionWith(node);
638
+ var detailsNode = details.create({}, [summaryNode, contentNode]);
639
+ tr.replaceSelectionWith(detailsNode);
623
640
  dispatch(tr);
624
641
  return true;
625
642
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@licium/editor-plugin-details",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Details/Summary plugin for Toast UI Editor",
5
5
  "keywords": [
6
6
  "nhn",
@@ -7,7 +7,30 @@
7
7
  text-indent: -9999px !important;
8
8
  }
9
9
 
10
+ details {
11
+ display: block;
12
+ background-color: #f7f7f7;
13
+ border: 1px solid #e1e1e1;
14
+ border-radius: 4px;
15
+ padding: 10px;
16
+ margin: 10px 0;
17
+ }
18
+
19
+ summary {
20
+ cursor: pointer;
21
+ font-weight: bold;
22
+ margin-bottom: 5px;
23
+ outline: none;
24
+ }
25
+
10
26
  /* Dark Mode */
11
27
  .toastui-editor-dark .toastui-editor-toolbar-icons.details {
12
28
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23eee' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z'/%3E%3Cpath d='M9 10L12 13 15 10' stroke='%23eee'/%3E%3C/svg%3E") !important;
29
+ }
30
+
31
+ .toastui-editor-dark details {
32
+ background-color: #282a36;
33
+ /* Darker background for contrast against editor bg */
34
+ border-color: #44475a;
35
+ color: #f8f8f2;
13
36
  }
package/src/index.ts CHANGED
@@ -13,6 +13,119 @@ function createToolbarItemOption(i18n: I18n) {
13
13
  };
14
14
  }
15
15
 
16
+ function handleDetailsExit(context: PluginContext, event: KeyboardEvent): void {
17
+ const { instance } = context as any;
18
+
19
+ if (!instance) {
20
+ console.log('[Details Plugin] No instance found');
21
+ return;
22
+ }
23
+
24
+ const editor = instance.getCurrentModeEditor();
25
+
26
+ if (!editor || !editor.view) {
27
+ console.log('[Details Plugin] No editor view found');
28
+ return;
29
+ }
30
+
31
+ const { view } = editor;
32
+ const { state } = view;
33
+ const { selection } = state;
34
+ const { $from } = selection;
35
+
36
+ // Find details node in ancestry
37
+ let detailsDepth = -1;
38
+
39
+ for (let d = $from.depth; d > 0; d -= 1) {
40
+ if ($from.node(d).type.name === 'details') {
41
+ detailsDepth = d;
42
+ break;
43
+ }
44
+ }
45
+
46
+ if (detailsDepth < 0) {
47
+ return;
48
+ }
49
+
50
+ const detailsNode = $from.node(detailsDepth);
51
+ const nodeStart = $from.before(detailsDepth);
52
+ const nodeEnd = nodeStart + detailsNode.nodeSize;
53
+ const afterPara = $from.after();
54
+ const isAtEnd = afterPara >= nodeEnd - 1;
55
+
56
+ console.log('[Details Plugin] Inside details:', {
57
+ key: event.key,
58
+ isAtEnd,
59
+ afterPara,
60
+ nodeEnd,
61
+ parentType: $from.parent.type.name,
62
+ parentEmpty: $from.parent.content.size === 0,
63
+ });
64
+
65
+ if (!isAtEnd) {
66
+ return;
67
+ }
68
+
69
+ const { tr, schema } = state;
70
+ const SelectionClass = state.selection.constructor as any;
71
+
72
+ // Enter on empty paragraph
73
+ if (event.key === 'Enter') {
74
+ const isEmptyPara = $from.parent.type.name === 'paragraph' && $from.parent.content.size === 0;
75
+
76
+ if (!isEmptyPara) {
77
+ return;
78
+ }
79
+
80
+ console.log('[Details Plugin] EXITING via Enter!');
81
+ event.preventDefault();
82
+
83
+ const beforePara = $from.before();
84
+
85
+ tr.delete(beforePara, afterPara);
86
+
87
+ const p = schema.nodes.paragraph.createAndFill()!;
88
+ const insertPos = nodeEnd - (afterPara - beforePara);
89
+
90
+ tr.insert(insertPos, p);
91
+
92
+ if (SelectionClass.near) {
93
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(insertPos + 1)));
94
+ }
95
+
96
+ view.dispatch(tr);
97
+ }
98
+
99
+ // ArrowDown at end of content
100
+ if (event.key === 'ArrowDown') {
101
+ const atEndOfParent = $from.parentOffset === $from.parent.content.size;
102
+
103
+ if (!atEndOfParent) {
104
+ return;
105
+ }
106
+
107
+ console.log('[Details Plugin] EXITING via ArrowDown!');
108
+ event.preventDefault();
109
+
110
+ if (nodeEnd < state.doc.content.size) {
111
+ if (SelectionClass.near) {
112
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(nodeEnd)));
113
+ view.dispatch(tr);
114
+ }
115
+ } else {
116
+ const p = schema.nodes.paragraph.createAndFill()!;
117
+
118
+ tr.insert(nodeEnd, p);
119
+
120
+ if (SelectionClass.near) {
121
+ tr.setSelection(SelectionClass.near(tr.doc.resolve(nodeEnd + 1)));
122
+ }
123
+
124
+ view.dispatch(tr);
125
+ }
126
+ }
127
+ }
128
+
16
129
  export default function detailsPlugin(
17
130
  context: PluginContext,
18
131
  options: PluginOptions = {}
@@ -23,10 +136,24 @@ export default function detailsPlugin(
23
136
 
24
137
  const toolbarItem = createToolbarItemOption(i18n);
25
138
 
139
+ // Hook into the global keydown event for exit behavior
140
+ eventEmitter.listen('keydown', (editorType: string, event: KeyboardEvent) => {
141
+ console.log('[Details Plugin] keydown via eventEmitter!', { editorType, key: event.key });
142
+
143
+ if (editorType !== 'wysiwyg') {
144
+ return;
145
+ }
146
+
147
+ if (event.key !== 'Enter' && event.key !== 'ArrowDown') {
148
+ return;
149
+ }
150
+
151
+ handleDetailsExit(context, event);
152
+ });
153
+
26
154
  return {
27
155
  markdownCommands: {
28
156
  details: (payload, { tr, selection, schema }, dispatch) => {
29
- const { from, to } = selection;
30
157
  const slice = selection.content();
31
158
  const textContent =
32
159
  slice.content.textBetween(0, slice.content.size, '\n') || i18n.get('content');
@@ -83,115 +210,11 @@ export default function detailsPlugin(
83
210
  }
84
211
  });
85
212
 
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
213
  return { dom, contentDOM: dom };
191
214
  },
192
215
  },
193
216
  wysiwygCommands: {
194
- details: (payload, { tr, selection, schema }, dispatch) => {
217
+ details: (payload, { tr, schema }, dispatch) => {
195
218
  const summaryText = i18n.get('summary');
196
219
  const contentText = i18n.get('content');
197
220
 
@@ -200,9 +223,9 @@ export default function detailsPlugin(
200
223
  if (details && summary && paragraph) {
201
224
  const summaryNode = summary.create({}, schema.text(summaryText));
202
225
  const contentNode = paragraph.create({}, schema.text(contentText));
203
- const node = details.create({}, [summaryNode, contentNode]);
226
+ const detailsNode = details.create({}, [summaryNode, contentNode]);
204
227
 
205
- tr.replaceSelectionWith(node);
228
+ tr.replaceSelectionWith(detailsNode);
206
229
  dispatch!(tr);
207
230
  return true;
208
231
  }