@licium/editor-plugin-details 1.0.1 → 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.0 | 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,92 +623,20 @@ function detailsPlugin(context, options) {
527
623
  }
528
624
  }
529
625
  });
530
- // Add Keydown listener for exit behavior
531
- dom.addEventListener('keydown', function (e) {
532
- var state = view.state;
533
- var selection = state.selection, tr = state.tr, schema = state.schema;
534
- var $from = selection.$from;
535
- var pos = getPos();
536
- if (typeof pos !== 'number') {
537
- return;
538
- }
539
- // Fetch current node to ensure nodeSize is fresh!
540
- var currentNode = state.doc.nodeAt(pos);
541
- if (!currentNode || currentNode.type.name !== 'details') {
542
- return;
543
- }
544
- var nodeSize = currentNode.nodeSize;
545
- var endOfDetails = pos + nodeSize;
546
- // Workaround for Dual Package Hazard:
547
- // Get the TextSelection constructor from the current selection instance
548
- // assuming the current selection is a TextSelection (which it is while typing).
549
- var SelectionHeader = state.selection.constructor;
550
- if (e.key === 'Enter') {
551
- var parent = $from.parent;
552
- // Check if we are in an empty paragraph that is the last child of details
553
- if (parent.type.name === 'paragraph' && parent.content.size === 0) {
554
- var parentEnd = $from.after();
555
- if (parentEnd === endOfDetails - 1) {
556
- // Empty last paragraph -> Exit logic
557
- e.preventDefault();
558
- e.stopPropagation();
559
- var parentPos = $from.before();
560
- tr.delete(parentPos, parentEnd);
561
- var p = schema.nodes.paragraph.createAndFill();
562
- // After deletion, the position shifts back
563
- var newPos = endOfDetails - (parentEnd - parentPos);
564
- tr.insert(newPos, p);
565
- if (SelectionHeader && SelectionHeader.near) {
566
- tr.setSelection(SelectionHeader.near(tr.doc.resolve(newPos + 1)));
567
- view.dispatch(tr);
568
- }
569
- }
570
- }
571
- }
572
- else if (e.key === 'ArrowDown') {
573
- var parentEnd = $from.after();
574
- if (parentEnd !== endOfDetails - 1) {
575
- return;
576
- }
577
- // We are in the last child
578
- var atEnd = $from.parentOffset === $from.parent.content.size;
579
- if (!atEnd) {
580
- return;
581
- }
582
- e.preventDefault();
583
- e.stopPropagation();
584
- if (endOfDetails < state.doc.content.size) {
585
- // Move cursor out to existing content
586
- if (SelectionHeader && SelectionHeader.near) {
587
- tr.setSelection(SelectionHeader.near(tr.doc.resolve(endOfDetails)));
588
- view.dispatch(tr);
589
- }
590
- }
591
- else {
592
- // Create new paragraph below
593
- var p = schema.nodes.paragraph.createAndFill();
594
- tr.insert(endOfDetails, p);
595
- if (SelectionHeader && SelectionHeader.near) {
596
- tr.setSelection(SelectionHeader.near(tr.doc.resolve(endOfDetails + 1)));
597
- view.dispatch(tr);
598
- }
599
- }
600
- }
601
- });
602
626
  return { dom: dom, contentDOM: dom };
603
627
  },
604
628
  },
605
629
  wysiwygCommands: {
606
630
  details: function (payload, _a, dispatch) {
607
- var tr = _a.tr, selection = _a.selection, schema = _a.schema;
631
+ var tr = _a.tr, schema = _a.schema;
608
632
  var summaryText = i18n.get('summary');
609
633
  var contentText = i18n.get('content');
610
634
  var _b = schema.nodes, details = _b.details, summary = _b.summary, paragraph = _b.paragraph;
611
635
  if (details && summary && paragraph) {
612
636
  var summaryNode = summary.create({}, schema.text(summaryText));
613
637
  var contentNode = paragraph.create({}, schema.text(contentText));
614
- var node = details.create({}, [summaryNode, contentNode]);
615
- tr.replaceSelectionWith(node);
638
+ var detailsNode = details.create({}, [summaryNode, contentNode]);
639
+ tr.replaceSelectionWith(detailsNode);
616
640
  dispatch(tr);
617
641
  return true;
618
642
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@licium/editor-plugin-details",
3
- "version": "1.0.1",
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,101 +210,11 @@ export default function detailsPlugin(
83
210
  }
84
211
  });
85
212
 
86
- // Add Keydown listener for exit behavior
87
- dom.addEventListener('keydown', (e) => {
88
- const { state } = view;
89
- const { selection, tr, schema } = state;
90
- const { $from } = selection;
91
- const pos = getPos();
92
-
93
- if (typeof pos !== 'number') {
94
- return;
95
- }
96
-
97
- // Fetch current node to ensure nodeSize is fresh!
98
- const currentNode = state.doc.nodeAt(pos);
99
-
100
- if (!currentNode || currentNode.type.name !== 'details') {
101
- return;
102
- }
103
-
104
- const { nodeSize } = currentNode;
105
- const endOfDetails = pos + nodeSize;
106
-
107
- // Workaround for Dual Package Hazard:
108
- // Get the TextSelection constructor from the current selection instance
109
- // assuming the current selection is a TextSelection (which it is while typing).
110
- const { constructor: SelectionHeader } = state.selection as any;
111
-
112
- if (e.key === 'Enter') {
113
- const { parent } = $from;
114
-
115
- // Check if we are in an empty paragraph that is the last child of details
116
- if (parent.type.name === 'paragraph' && parent.content.size === 0) {
117
- const parentEnd = $from.after();
118
-
119
- if (parentEnd === endOfDetails - 1) {
120
- // Empty last paragraph -> Exit logic
121
- e.preventDefault();
122
- e.stopPropagation();
123
-
124
- const parentPos = $from.before();
125
-
126
- tr.delete(parentPos, parentEnd);
127
- const p = schema.nodes.paragraph.createAndFill()!;
128
- // After deletion, the position shifts back
129
- const newPos = endOfDetails - (parentEnd - parentPos);
130
-
131
- tr.insert(newPos, p);
132
-
133
- if (SelectionHeader && SelectionHeader.near) {
134
- tr.setSelection(SelectionHeader.near(tr.doc.resolve(newPos + 1)));
135
- view.dispatch(tr);
136
- }
137
- }
138
- }
139
- } else if (e.key === 'ArrowDown') {
140
- const parentEnd = $from.after();
141
-
142
- if (parentEnd !== endOfDetails - 1) {
143
- return;
144
- }
145
-
146
- // We are in the last child
147
- const atEnd = $from.parentOffset === $from.parent.content.size;
148
-
149
- if (!atEnd) {
150
- return;
151
- }
152
-
153
- e.preventDefault();
154
- e.stopPropagation();
155
-
156
- if (endOfDetails < state.doc.content.size) {
157
- // Move cursor out to existing content
158
- if (SelectionHeader && SelectionHeader.near) {
159
- tr.setSelection(SelectionHeader.near(tr.doc.resolve(endOfDetails)));
160
- view.dispatch(tr);
161
- }
162
- } else {
163
- // Create new paragraph below
164
- const p = schema.nodes.paragraph.createAndFill()!;
165
-
166
- tr.insert(endOfDetails, p);
167
-
168
- if (SelectionHeader && SelectionHeader.near) {
169
- tr.setSelection(SelectionHeader.near(tr.doc.resolve(endOfDetails + 1)));
170
- view.dispatch(tr);
171
- }
172
- }
173
- }
174
- });
175
-
176
213
  return { dom, contentDOM: dom };
177
214
  },
178
215
  },
179
216
  wysiwygCommands: {
180
- details: (payload, { tr, selection, schema }, dispatch) => {
217
+ details: (payload, { tr, schema }, dispatch) => {
181
218
  const summaryText = i18n.get('summary');
182
219
  const contentText = i18n.get('content');
183
220
 
@@ -186,9 +223,9 @@ export default function detailsPlugin(
186
223
  if (details && summary && paragraph) {
187
224
  const summaryNode = summary.create({}, schema.text(summaryText));
188
225
  const contentNode = paragraph.create({}, schema.text(contentText));
189
- const node = details.create({}, [summaryNode, contentNode]);
226
+ const detailsNode = details.create({}, [summaryNode, contentNode]);
190
227
 
191
- tr.replaceSelectionWith(node);
228
+ tr.replaceSelectionWith(detailsNode);
192
229
  dispatch!(tr);
193
230
  return true;
194
231
  }
@@ -199,7 +236,7 @@ export default function detailsPlugin(
199
236
  toolbarItems: [
200
237
  {
201
238
  groupIndex: 0,
202
- itemIndex: 4, // Append after CodeBlock
239
+ itemIndex: 4,
203
240
  item: toolbarItem,
204
241
  },
205
242
  ],