@sex-editor/slash 0.0.1

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,23 @@
1
+ import { SexEditor } from '@sex-editor/core';
2
+
3
+ declare class SlashMenuPlugin {
4
+ private editor;
5
+ private menuElement;
6
+ private selectedIndex;
7
+ private queryString;
8
+ private filteredItems;
9
+ private active;
10
+ private items;
11
+ constructor(editor: SexEditor);
12
+ private getMenuItems;
13
+ register(): void;
14
+ private filterItems;
15
+ private showMenu;
16
+ private hideMenu;
17
+ private moveSelection;
18
+ private updateMenuUI;
19
+ private executeSelection;
20
+ }
21
+ declare function registerSlashPlugin(editor: SexEditor): SlashMenuPlugin;
22
+
23
+ export { SlashMenuPlugin, registerSlashPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,1293 @@
1
+ // src/index.ts
2
+ import {
3
+ $createParagraphNode,
4
+ $getSelection,
5
+ $isRangeSelection,
6
+ COMMAND_PRIORITY_LOW,
7
+ KEY_ARROW_DOWN_COMMAND,
8
+ KEY_ARROW_UP_COMMAND,
9
+ KEY_ENTER_COMMAND,
10
+ KEY_ESCAPE_COMMAND,
11
+ TextNode,
12
+ $insertNodes
13
+ } from "lexical";
14
+ import {
15
+ $createHeadingNode,
16
+ $createQuoteNode
17
+ } from "@lexical/rich-text";
18
+ import {
19
+ INSERT_ORDERED_LIST_COMMAND,
20
+ INSERT_UNORDERED_LIST_COMMAND
21
+ } from "@lexical/list";
22
+ import { $createCodeNode } from "@lexical/code";
23
+ import {
24
+ $createHorizontalRuleNode,
25
+ $createPageBreakNode,
26
+ $createTableNode,
27
+ $createImageNode,
28
+ $createInlineImageNode,
29
+ $createAnchorNode
30
+ } from "@sex-editor/core";
31
+
32
+ // ../ui/dist/index.js
33
+ var Modal = class {
34
+ constructor(options) {
35
+ this.options = options;
36
+ this.container = document.createElement("div");
37
+ this.container.className = "sex-modal-overlay";
38
+ this.render();
39
+ }
40
+ setContent(content) {
41
+ this.options.content = content;
42
+ const body = this.container.querySelector(".sex-modal-body");
43
+ if (body) {
44
+ body.innerHTML = "";
45
+ body.appendChild(content);
46
+ }
47
+ if (this.options.footer) {
48
+ const footerEl = this.container.querySelector(".sex-modal-footer");
49
+ if (footerEl) {
50
+ footerEl.innerHTML = "";
51
+ footerEl.appendChild(this.options.footer);
52
+ footerEl.style.display = "block";
53
+ }
54
+ }
55
+ }
56
+ render() {
57
+ this.container.innerHTML = `
58
+ <div class="sex-modal" style="width: ${this.options.width || "400px"}">
59
+ <div class="sex-modal-header">
60
+ <h3>${this.options.title}</h3>
61
+ <button class="sex-modal-close">&times;</button>
62
+ </div>
63
+ <div class="sex-modal-body"></div>
64
+ <div class="sex-modal-footer"></div>
65
+ </div>
66
+ `;
67
+ const body = this.container.querySelector(".sex-modal-body");
68
+ if (this.options.content) {
69
+ body.appendChild(this.options.content);
70
+ }
71
+ const footer = this.container.querySelector(".sex-modal-footer");
72
+ if (this.options.footer) {
73
+ footer.appendChild(this.options.footer);
74
+ } else {
75
+ footer.style.display = "none";
76
+ }
77
+ const closeBtn = this.container.querySelector(".sex-modal-close");
78
+ closeBtn?.addEventListener("click", () => this.close());
79
+ if (this.options.closeOnClickOutside !== false) {
80
+ this.container.addEventListener("click", (e) => {
81
+ if (e.target === this.container) {
82
+ this.close();
83
+ }
84
+ });
85
+ }
86
+ }
87
+ show() {
88
+ document.body.appendChild(this.container);
89
+ }
90
+ close() {
91
+ this.container.remove();
92
+ if (this.options.onClose) {
93
+ this.options.onClose();
94
+ }
95
+ }
96
+ getElement() {
97
+ return this.container;
98
+ }
99
+ };
100
+ var ImageInsertDialog = class {
101
+ constructor(config) {
102
+ this.modal = null;
103
+ this.config = config;
104
+ this.t = config.t;
105
+ }
106
+ show() {
107
+ const content = document.createElement("div");
108
+ content.innerHTML = `
109
+ <div class="sex-tabs">
110
+ <button class="sex-tab active" data-tab="url">${this.t("toolbar.imageSrc")}</button>
111
+ <button class="sex-tab" data-tab="upload">${this.t("toolbar.imageUpload")}</button>
112
+ </div>
113
+
114
+ <div class="sex-tab-content active" id="tab-url">
115
+ <input type="text" class="sex-input sex-input-url" placeholder="https://example.com/image.png">
116
+ </div>
117
+
118
+ <div class="sex-tab-content" id="tab-upload">
119
+ <div class="sex-upload-area">
120
+ <span>${this.t("toolbar.browse")}</span>
121
+ <input type="file" accept="image/*" style="display:none">
122
+ </div>
123
+ <div class="sex-preview-area" style="display:none; margin-top: 10px; text-align: center;">
124
+ <img src="" style="max-width: 100%; max-height: 200px; object-fit: contain;">
125
+ </div>
126
+ </div>
127
+
128
+ <div style="margin-top: 16px;">
129
+ <label style="display:block; margin-bottom: 6px; color: #666; font-size: 13px;">${this.t("toolbar.imageAlt") || "Alt Text"}</label>
130
+ <input type="text" class="sex-input sex-input-alt" placeholder="${this.t("toolbar.imageAltPlaceholder") || "Image description"}">
131
+ </div>
132
+ `;
133
+ const footer = document.createElement("div");
134
+ footer.innerHTML = `
135
+ <button class="sex-btn sex-btn-cancel">${this.t("toolbar.cancel")}</button>
136
+ <button class="sex-btn sex-btn-primary">${this.t("toolbar.confirm")}</button>
137
+ `;
138
+ const tabs = content.querySelectorAll(".sex-tab");
139
+ const urlInput = content.querySelector(".sex-input-url");
140
+ const altInput = content.querySelector(".sex-input-alt");
141
+ const uploadArea = content.querySelector(".sex-upload-area");
142
+ const fileInput = content.querySelector("input[type='file']");
143
+ const previewArea = content.querySelector(".sex-preview-area");
144
+ const previewImg = previewArea.querySelector("img");
145
+ const cancelBtn = footer.querySelector(".sex-btn-cancel");
146
+ const confirmBtn = footer.querySelector(".sex-btn-primary");
147
+ let activeTab = "url";
148
+ let selectedFile = null;
149
+ tabs.forEach((tab) => {
150
+ tab.addEventListener("click", () => {
151
+ tabs.forEach((t) => t.classList.remove("active"));
152
+ content.querySelectorAll(".sex-tab-content").forEach((c) => c.classList.remove("active"));
153
+ tab.classList.add("active");
154
+ const tabId = tab.dataset.tab;
155
+ content.querySelector(`#tab-${tabId}`)?.classList.add("active");
156
+ activeTab = tabId || "url";
157
+ });
158
+ });
159
+ uploadArea.addEventListener("click", () => fileInput.click());
160
+ fileInput.addEventListener("change", (e) => {
161
+ const file = e.target.files?.[0];
162
+ if (file) {
163
+ selectedFile = file;
164
+ const url = URL.createObjectURL(file);
165
+ previewImg.src = url;
166
+ previewArea.style.display = "block";
167
+ uploadArea.style.display = "none";
168
+ }
169
+ });
170
+ cancelBtn.addEventListener("click", () => {
171
+ this.modal?.close();
172
+ if (this.config.onCancel) this.config.onCancel();
173
+ });
174
+ confirmBtn.addEventListener("click", () => {
175
+ const alt = altInput.value.trim();
176
+ if (activeTab === "url") {
177
+ const url = urlInput.value.trim();
178
+ if (url) {
179
+ this.config.onSubmit({
180
+ type: "url",
181
+ src: url,
182
+ alt
183
+ });
184
+ this.modal?.close();
185
+ }
186
+ } else {
187
+ if (selectedFile) {
188
+ this.config.onSubmit({
189
+ type: "file",
190
+ file: selectedFile,
191
+ alt: alt || selectedFile.name
192
+ });
193
+ this.modal?.close();
194
+ }
195
+ }
196
+ });
197
+ this.modal = new Modal({
198
+ title: this.t("toolbar.insertImage"),
199
+ content,
200
+ footer,
201
+ width: "400px",
202
+ onClose: this.config.onCancel
203
+ });
204
+ this.modal.show();
205
+ }
206
+ };
207
+ var Button = class {
208
+ constructor(options = {}) {
209
+ this.options = options;
210
+ this.element = document.createElement("button");
211
+ this.element.type = "button";
212
+ this.element.className = `sex-ui-button ${options.className || ""}`;
213
+ this.render();
214
+ this.bindEvents();
215
+ }
216
+ render() {
217
+ const { title, icon, active, disabled, dataset, children } = this.options;
218
+ if (title) this.element.title = title;
219
+ if (active) this.element.classList.add("active");
220
+ if (disabled) this.element.disabled = true;
221
+ if (dataset) {
222
+ Object.entries(dataset).forEach(([key, value]) => {
223
+ this.element.dataset[key] = value;
224
+ });
225
+ }
226
+ this.element.innerHTML = "";
227
+ if (icon) {
228
+ if (typeof icon === "string") {
229
+ if (icon.trim().startsWith("<svg")) {
230
+ const temp = document.createElement("div");
231
+ temp.innerHTML = icon;
232
+ if (temp.firstChild) {
233
+ this.element.appendChild(temp.firstChild);
234
+ }
235
+ } else {
236
+ this.element.textContent = icon;
237
+ }
238
+ } else if (icon instanceof HTMLElement) {
239
+ this.element.appendChild(icon);
240
+ }
241
+ }
242
+ if (children) {
243
+ if (typeof children === "string") {
244
+ const span = document.createElement("span");
245
+ span.textContent = children;
246
+ this.element.appendChild(span);
247
+ } else {
248
+ this.element.appendChild(children);
249
+ }
250
+ }
251
+ }
252
+ bindEvents() {
253
+ if (this.options.onClick) {
254
+ this.element.addEventListener("click", (e) => {
255
+ if (!this.element.disabled && this.options.onClick) {
256
+ this.options.onClick(e);
257
+ }
258
+ });
259
+ }
260
+ }
261
+ getElement() {
262
+ return this.element;
263
+ }
264
+ setActive(active) {
265
+ this.element.classList.toggle("active", active);
266
+ }
267
+ setDisabled(disabled) {
268
+ this.element.disabled = disabled;
269
+ }
270
+ };
271
+ var NumberInput = class {
272
+ constructor(options = {}) {
273
+ this.options = {
274
+ value: 14,
275
+ min: 0,
276
+ max: 100,
277
+ step: 1,
278
+ ...options
279
+ };
280
+ this.container = document.createElement("div");
281
+ this.container.className = "sex-number-input";
282
+ this.render();
283
+ this.bindEvents();
284
+ }
285
+ render() {
286
+ const { value, width, titleMinus, titlePlus } = this.options;
287
+ if (width) this.container.style.width = width;
288
+ const minusBtn = document.createElement("button");
289
+ minusBtn.className = "sex-number-btn sex-number-minus";
290
+ minusBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M5 11h14v2H5z"/></svg>`;
291
+ if (titleMinus) minusBtn.title = titleMinus;
292
+ this.input = document.createElement("input");
293
+ this.input.type = "text";
294
+ this.input.inputMode = "numeric";
295
+ this.input.className = "sex-number-field";
296
+ this.input.value = String(value);
297
+ const plusBtn = document.createElement("button");
298
+ plusBtn.className = "sex-number-btn sex-number-plus";
299
+ plusBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2h6z"/></svg>`;
300
+ if (titlePlus) plusBtn.title = titlePlus;
301
+ this.container.append(minusBtn, this.input, plusBtn);
302
+ minusBtn.addEventListener("click", () => this.changeValue(-(this.options.step || 1)));
303
+ plusBtn.addEventListener("click", () => this.changeValue(this.options.step || 1));
304
+ }
305
+ bindEvents() {
306
+ this.input.addEventListener("keydown", (e) => {
307
+ const step = this.options.step || 1;
308
+ if (e.key === "Enter") {
309
+ this.commitValue();
310
+ this.input.blur();
311
+ } else if (e.key === "ArrowUp") {
312
+ e.preventDefault();
313
+ this.changeValue(step);
314
+ } else if (e.key === "ArrowDown") {
315
+ e.preventDefault();
316
+ this.changeValue(-step);
317
+ }
318
+ });
319
+ this.input.addEventListener("blur", () => {
320
+ this.commitValue();
321
+ });
322
+ this.input.addEventListener("input", () => {
323
+ this.input.value = this.input.value.replace(/[^0-9]/g, "");
324
+ });
325
+ }
326
+ changeValue(delta) {
327
+ let val = parseInt(this.input.value, 10);
328
+ if (isNaN(val)) val = this.options.value || 0;
329
+ this.setValue(val + delta);
330
+ }
331
+ commitValue() {
332
+ let val = parseInt(this.input.value, 10);
333
+ if (isNaN(val)) val = this.options.value || 0;
334
+ this.setValue(val);
335
+ }
336
+ clamp(val) {
337
+ const { min, max } = this.options;
338
+ if (min !== void 0) val = Math.max(min, val);
339
+ if (max !== void 0) val = Math.min(max, val);
340
+ return val;
341
+ }
342
+ setValue(val, emit = true) {
343
+ val = this.clamp(val);
344
+ this.input.value = String(val);
345
+ this.options.value = val;
346
+ if (emit && this.options.onChange) {
347
+ this.options.onChange(val);
348
+ }
349
+ }
350
+ getElement() {
351
+ return this.container;
352
+ }
353
+ getValue() {
354
+ return this.options.value || 0;
355
+ }
356
+ destroy() {
357
+ this.container.remove();
358
+ }
359
+ };
360
+ var TableInsertDialog = class {
361
+ constructor(config) {
362
+ this.modal = null;
363
+ this.config = config;
364
+ this.rowsInput = new NumberInput({
365
+ value: 3,
366
+ min: 1,
367
+ max: 20,
368
+ step: 1,
369
+ width: "100%"
370
+ });
371
+ this.colsInput = new NumberInput({
372
+ value: 3,
373
+ min: 1,
374
+ max: 10,
375
+ step: 1,
376
+ width: "100%"
377
+ });
378
+ }
379
+ show() {
380
+ const content = document.createElement("div");
381
+ content.style.padding = "16px";
382
+ content.innerHTML = `
383
+ <div style="margin-bottom:12px">
384
+ <label style="display:block;margin-bottom:4px;font-size:13px;color:#666">${this.config.t("toolbar.table.rows") || "Rows"}</label>
385
+ <div id="sex-table-rows-input"></div>
386
+ </div>
387
+ <div style="margin-bottom:12px">
388
+ <label style="display:block;margin-bottom:4px;font-size:13px;color:#666">${this.config.t("toolbar.table.cols") || "Columns"}</label>
389
+ <div id="sex-table-cols-input"></div>
390
+ </div>
391
+ `;
392
+ content.querySelector("#sex-table-rows-input")?.appendChild(this.rowsInput.getElement());
393
+ content.querySelector("#sex-table-cols-input")?.appendChild(this.colsInput.getElement());
394
+ const footer = document.createElement("div");
395
+ footer.innerHTML = `
396
+ <button class="sex-btn sex-btn-cancel">${this.config.t("toolbar.cancel")}</button>
397
+ <button class="sex-btn sex-btn-primary">${this.config.t("toolbar.confirm")}</button>
398
+ `;
399
+ this.modal = new Modal({
400
+ title: this.config.t("toolbar.insertTable") || "Insert Table",
401
+ content,
402
+ footer,
403
+ width: "300px",
404
+ onClose: () => {
405
+ this.config.onCancel?.();
406
+ }
407
+ });
408
+ footer.querySelector(".sex-btn-cancel")?.addEventListener("click", () => {
409
+ this.modal?.close();
410
+ });
411
+ footer.querySelector(".sex-btn-primary")?.addEventListener("click", () => {
412
+ const rows = this.rowsInput.getValue();
413
+ const cols = this.colsInput.getValue();
414
+ this.config.onSubmit(rows, cols);
415
+ this.modal?.close();
416
+ });
417
+ this.modal.show();
418
+ }
419
+ };
420
+ var AnchorInsertDialog = class {
421
+ constructor({ t, defaultId = "", onSubmit, onCancel }) {
422
+ this.modal = new Modal({
423
+ title: t("dialog.anchor.insert"),
424
+ closeOnClickOutside: true,
425
+ onClose: onCancel
426
+ });
427
+ const content = document.createElement("div");
428
+ content.style.cssText = "display: flex; flex-direction: column; gap: 12px; padding: 4px 0;";
429
+ const idGroup = document.createElement("div");
430
+ idGroup.style.cssText = "display: flex; flex-direction: column; gap: 4px;";
431
+ const idLabel = document.createElement("label");
432
+ idLabel.textContent = "ID";
433
+ idLabel.style.cssText = "font-size: 13px; font-weight: 500; color: #374151;";
434
+ this.idInput = document.createElement("input");
435
+ this.idInput.type = "text";
436
+ this.idInput.value = defaultId;
437
+ this.idInput.placeholder = t("toolbar.anchorIdPlaceholder") || "my-anchor-id";
438
+ this.idInput.style.cssText = "padding: 8px 12px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 14px; outline: none; transition: border-color 0.2s;";
439
+ this.idInput.onfocus = () => this.idInput.style.borderColor = "#2563eb";
440
+ this.idInput.onblur = () => this.idInput.style.borderColor = "#d1d5db";
441
+ setTimeout(() => this.idInput.focus(), 0);
442
+ idGroup.appendChild(idLabel);
443
+ idGroup.appendChild(this.idInput);
444
+ content.appendChild(idGroup);
445
+ const footer = document.createElement("div");
446
+ footer.style.cssText = "display: flex; justify-content: flex-end; gap: 8px; margin-top: 8px;";
447
+ const cancelBtn = new Button({
448
+ children: t("dialog.cancel"),
449
+ // variant: 'secondary', // Button class doesn't support variant yet, fallback to class or styles if needed
450
+ onClick: () => this.modal.close()
451
+ });
452
+ const confirmBtn = new Button({
453
+ children: t("dialog.confirm"),
454
+ // variant: 'primary',
455
+ onClick: () => {
456
+ const id = this.idInput.value.trim();
457
+ if (id) {
458
+ onSubmit({ id });
459
+ this.modal.close();
460
+ } else {
461
+ this.idInput.style.borderColor = "red";
462
+ }
463
+ }
464
+ });
465
+ this.idInput.onkeydown = (e) => {
466
+ if (e.key === "Enter") {
467
+ e.preventDefault();
468
+ confirmBtn.getElement().click();
469
+ }
470
+ };
471
+ footer.appendChild(cancelBtn.getElement());
472
+ footer.appendChild(confirmBtn.getElement());
473
+ content.appendChild(footer);
474
+ this.modal.setContent(content);
475
+ }
476
+ show() {
477
+ this.modal.show();
478
+ }
479
+ };
480
+ var ensureStyle = (id, cssText) => {
481
+ if (typeof document === "undefined") return;
482
+ if (document.getElementById(id)) return;
483
+ const style = document.createElement("style");
484
+ style.id = id;
485
+ style.textContent = cssText;
486
+ (document.head ?? document.documentElement).appendChild(style);
487
+ };
488
+ ensureStyle(
489
+ "sex-ui-styles",
490
+ `/* Button */
491
+ .sex-ui-button {
492
+ display: flex;
493
+ align-items: center;
494
+ justify-content: center;
495
+ min-width: 32px;
496
+ height: 32px;
497
+ padding: 0 4px;
498
+ border: 1px solid transparent;
499
+ border-radius: 4px;
500
+ background: transparent;
501
+ cursor: pointer;
502
+ transition: all .2s ease;
503
+ color: #495057;
504
+ font-size: 13px;
505
+ }
506
+ .sex-ui-button:hover:not(:disabled) {
507
+ background: #e9ecef;
508
+ border-color: #dee2e6;
509
+ }
510
+ .sex-ui-button.active {
511
+ background: #e9ecef;
512
+ border-color: #dee2e6;
513
+ }
514
+ .sex-ui-button:disabled {
515
+ opacity: 0.5;
516
+ cursor: not-allowed;
517
+ }
518
+ .sex-ui-button svg {
519
+ display: block;
520
+ }
521
+
522
+ /* NumberInput */
523
+ .sex-number-input {
524
+ display: flex;
525
+ align-items: center;
526
+ background: white;
527
+ border: 1px solid transparent;
528
+ border-radius: 4px;
529
+ overflow: hidden;
530
+ height: 32px;
531
+ transition: all .2s ease;
532
+ }
533
+ .sex-number-input:hover {
534
+ border-color: #dee2e6;
535
+ }
536
+ .sex-number-input:focus-within {
537
+ border-color: #dee2e6;
538
+ }
539
+ .sex-number-btn {
540
+ width: 28px;
541
+ height: 100%;
542
+ display: flex;
543
+ align-items: center;
544
+ justify-content: center;
545
+ background: transparent;
546
+ border: none;
547
+ cursor: pointer;
548
+ color: #495057;
549
+ transition: background .2s;
550
+ }
551
+ .sex-number-btn:hover {
552
+ background: #f8f9fa;
553
+ }
554
+ .sex-number-field {
555
+ width: 36px;
556
+ text-align: center;
557
+ border: none;
558
+ outline: none;
559
+ font-size: 13px;
560
+ color: #495057;
561
+ background: transparent;
562
+ }
563
+ /* Hide arrows */
564
+ .sex-number-field::-webkit-outer-spin-button,
565
+ .sex-number-field::-webkit-inner-spin-button {
566
+ -webkit-appearance: none;
567
+ margin: 0;
568
+ }
569
+
570
+ /* Dropdown */
571
+ .sex-dropdown { position: relative; display: inline-flex; }
572
+ .sex-dropdown-button {
573
+ display: flex;
574
+ align-items: center;
575
+ justify-content: space-between;
576
+ height: 32px;
577
+ padding: 0 8px;
578
+ border: 1px solid transparent;
579
+ border-radius: 4px;
580
+ background: transparent;
581
+ cursor: pointer;
582
+ transition: all .2s ease;
583
+ color: #495057;
584
+ font-size: 13px;
585
+ }
586
+ .sex-dropdown-button:hover, .sex-dropdown-button.active {
587
+ background: #e9ecef;
588
+ border-color: #dee2e6;
589
+ }
590
+ .sex-dropdown-menu {
591
+ position: absolute;
592
+ top: 100%;
593
+ left: 0;
594
+ min-width: 100%;
595
+ z-index: 1000;
596
+ background: white;
597
+ border: 1px solid #e9ecef;
598
+ border-radius: 4px;
599
+ box-shadow: 0 4px 12px rgba(0,0,0,.1);
600
+ margin-top: 4px;
601
+ max-height: 300px;
602
+ overflow-y: auto;
603
+ display: none;
604
+ }
605
+ .sex-dropdown-menu.show { display: block; }
606
+ .sex-menu-item {
607
+ padding: 8px 12px;
608
+ cursor: pointer;
609
+ font-size: 13px;
610
+ transition: background .2s;
611
+ display: flex;
612
+ align-items: center;
613
+ }
614
+ .sex-menu-item:hover, .sex-menu-item.active { background: #f8f9fa; }
615
+ .sex-menu-divider {
616
+ height: 1px;
617
+ background: #eee;
618
+ margin: 4px 0;
619
+ }
620
+
621
+ /* Modal */
622
+ .sex-modal-overlay {
623
+ position: fixed;
624
+ top: 0;
625
+ left: 0;
626
+ width: 100%;
627
+ height: 100%;
628
+ background: rgba(0, 0, 0, 0.5);
629
+ display: flex;
630
+ align-items: center;
631
+ justify-content: center;
632
+ z-index: 2000;
633
+ }
634
+ .sex-modal {
635
+ background: white;
636
+ border-radius: 8px;
637
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
638
+ overflow: hidden;
639
+ display: flex;
640
+ flex-direction: column;
641
+ max-height: 90vh;
642
+ }
643
+ .sex-modal-header {
644
+ padding: 16px;
645
+ border-bottom: 1px solid #eee;
646
+ display: flex;
647
+ justify-content: space-between;
648
+ align-items: center;
649
+ }
650
+ .sex-modal-header h3 { margin: 0; font-size: 16px; font-weight: 600; }
651
+ .sex-modal-close { background: none; border: none; font-size: 20px; cursor: pointer; color: #999; }
652
+ .sex-modal-body { padding: 16px; overflow-y: auto; }
653
+ .sex-modal-footer {
654
+ padding: 16px;
655
+ border-top: 1px solid #eee;
656
+ display: flex;
657
+ justify-content: flex-end;
658
+ gap: 8px;
659
+ }
660
+ .sex-btn {
661
+ padding: 6px 16px;
662
+ border-radius: 4px;
663
+ border: none;
664
+ cursor: pointer;
665
+ font-size: 14px;
666
+ }
667
+ .sex-btn-cancel { background: #f1f1f1; color: #333; }
668
+ .sex-btn-primary { background: #007bff; color: white; }
669
+ .sex-btn-primary:disabled { background: #ccc; cursor: not-allowed; }
670
+ .sex-btn-preset { background: #f8f9fa; border: 1px solid #dee2e6; color: #495057; }
671
+ .sex-btn-preset:hover { background: #e9ecef; border-color: #ced4da; }
672
+
673
+ /* Input Styles */
674
+ .sex-input {
675
+ width: 100%;
676
+ padding: 8px;
677
+ border: 1px solid #ddd;
678
+ border-radius: 4px;
679
+ box-sizing: border-box;
680
+ }
681
+
682
+ /* ImageInsertDialog */
683
+ .sex-tabs { display: flex; border-bottom: 1px solid #eee; margin-bottom: 16px; }
684
+ .sex-tab {
685
+ padding: 8px 16px;
686
+ background: none;
687
+ border: none;
688
+ border-bottom: 2px solid transparent;
689
+ cursor: pointer;
690
+ color: #666;
691
+ }
692
+ .sex-tab.active { color: #007bff; border-bottom-color: #007bff; }
693
+ .sex-tab-content { display: none; }
694
+ .sex-tab-content.active { display: block; }
695
+ .sex-upload-area {
696
+ border: 2px dashed #ddd;
697
+ border-radius: 4px;
698
+ padding: 32px;
699
+ text-align: center;
700
+ cursor: pointer;
701
+ color: #666;
702
+ transition: border-color 0.2s;
703
+ }
704
+ .sex-upload-area:hover { border-color: #007bff; color: #007bff; }
705
+
706
+ /* FloatingToolbar */
707
+ .sex-floating-toolbar {
708
+ position: absolute;
709
+ top: 0;
710
+ left: 0;
711
+ z-index: 1000;
712
+ background: white;
713
+ border: 1px solid #e9ecef;
714
+ border-radius: 6px;
715
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
716
+ display: none;
717
+ padding: 4px;
718
+ gap: 4px;
719
+ align-items: center;
720
+ transition: opacity 0.2s;
721
+ }
722
+ .sex-floating-toolbar.show {
723
+ display: flex;
724
+ }
725
+ .sex-floating-separator {
726
+ width: 1px;
727
+ height: 20px;
728
+ background: #e9ecef;
729
+ margin: 0 2px;
730
+ }
731
+ `
732
+ );
733
+
734
+ // src/index.ts
735
+ var ensureStyle2 = (id, cssText) => {
736
+ if (typeof document === "undefined") return;
737
+ if (document.getElementById(id)) return;
738
+ const style = document.createElement("style");
739
+ style.id = id;
740
+ style.textContent = cssText;
741
+ (document.head ?? document.documentElement).appendChild(style);
742
+ };
743
+ ensureStyle2(
744
+ "sex-slash-styles",
745
+ `.sex-slash-menu {
746
+ position: absolute;
747
+ z-index: 100;
748
+ background: white;
749
+ border: 1px solid #e5e7eb;
750
+ border-radius: 8px;
751
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
752
+ width: 280px;
753
+ max-height: 300px;
754
+ overflow-y: auto;
755
+ padding: 4px;
756
+ }
757
+
758
+ .sex-slash-menu-item {
759
+ display: flex;
760
+ align-items: center;
761
+ padding: 8px 12px;
762
+ cursor: pointer;
763
+ border-radius: 4px;
764
+ font-size: 14px;
765
+ color: #374151;
766
+ transition: background-color 0.2s;
767
+ }
768
+
769
+ .sex-slash-menu-item:hover,
770
+ .sex-slash-menu-item.selected {
771
+ background-color: #f3f4f6;
772
+ }
773
+
774
+ .sex-slash-menu-icon {
775
+ margin-right: 12px;
776
+ width: 20px;
777
+ height: 20px;
778
+ display: flex;
779
+ align-items: center;
780
+ justify-content: center;
781
+ color: #6b7280;
782
+ }
783
+
784
+ .sex-slash-menu-label {
785
+ flex: 1;
786
+ }
787
+
788
+ .sex-slash-menu-hidden {
789
+ display: none;
790
+ }
791
+ `
792
+ );
793
+ var SlashMenuPlugin = class {
794
+ constructor(editor) {
795
+ this.menuElement = null;
796
+ this.selectedIndex = 0;
797
+ this.queryString = null;
798
+ this.filteredItems = [];
799
+ this.active = false;
800
+ this.editor = editor;
801
+ this.items = this.getMenuItems();
802
+ }
803
+ getMenuItems() {
804
+ const t = this.editor.t;
805
+ return [
806
+ {
807
+ key: "paragraph",
808
+ label: t("toolbar.block.paragraph") || "Paragraph",
809
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M14 17H4v2h10v-2zm0-8H4v2h10V9zM4 15h16v-2H4v2zM4 5v2h16V5H4z"/></svg>`,
810
+ keywords: ["p", "paragraph", "text"],
811
+ execute: (editor) => {
812
+ editor.update(() => {
813
+ const selection = $getSelection();
814
+ if ($isRangeSelection(selection)) {
815
+ const paragraph = $createParagraphNode();
816
+ $insertNodes([paragraph]);
817
+ }
818
+ });
819
+ }
820
+ },
821
+ {
822
+ key: "anchor",
823
+ label: t("toolbar.anchor") || "Anchor",
824
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>`,
825
+ keywords: ["anchor", "bookmark", "id"],
826
+ execute: (editor) => {
827
+ const dialog = new AnchorInsertDialog({
828
+ t: (key) => editor.t ? editor.t(key) : key,
829
+ onSubmit: (data) => {
830
+ editor.update(() => {
831
+ const node = $createAnchorNode(data.id);
832
+ $insertNodes([node]);
833
+ });
834
+ }
835
+ });
836
+ dialog.show();
837
+ }
838
+ },
839
+ {
840
+ key: "h1",
841
+ label: t("toolbar.block.h1") || "Heading 1",
842
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
843
+ keywords: ["h1", "heading1", "title"],
844
+ execute: (editor) => {
845
+ editor.update(() => {
846
+ const selection = $getSelection();
847
+ if ($isRangeSelection(selection)) {
848
+ $insertNodes([$createHeadingNode("h1")]);
849
+ }
850
+ });
851
+ }
852
+ },
853
+ {
854
+ key: "h2",
855
+ label: t("toolbar.block.h2") || "Heading 2",
856
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
857
+ keywords: ["h2", "heading2", "subtitle"],
858
+ execute: (editor) => {
859
+ editor.update(() => {
860
+ const selection = $getSelection();
861
+ if ($isRangeSelection(selection)) {
862
+ $insertNodes([$createHeadingNode("h2")]);
863
+ }
864
+ });
865
+ }
866
+ },
867
+ {
868
+ key: "h3",
869
+ label: t("toolbar.block.h3") || "Heading 3",
870
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
871
+ keywords: ["h3", "heading3"],
872
+ execute: (editor) => {
873
+ editor.update(() => {
874
+ const selection = $getSelection();
875
+ if ($isRangeSelection(selection)) {
876
+ $insertNodes([$createHeadingNode("h3")]);
877
+ }
878
+ });
879
+ }
880
+ },
881
+ {
882
+ key: "h4",
883
+ label: t("toolbar.block.h4") || "Heading 4",
884
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
885
+ keywords: ["h4", "heading4"],
886
+ execute: (editor) => {
887
+ editor.update(() => {
888
+ const selection = $getSelection();
889
+ if ($isRangeSelection(selection)) {
890
+ $insertNodes([$createHeadingNode("h4")]);
891
+ }
892
+ });
893
+ }
894
+ },
895
+ {
896
+ key: "h5",
897
+ label: t("toolbar.block.h5") || "Heading 5",
898
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
899
+ keywords: ["h5", "heading5"],
900
+ execute: (editor) => {
901
+ editor.update(() => {
902
+ const selection = $getSelection();
903
+ if ($isRangeSelection(selection)) {
904
+ $insertNodes([$createHeadingNode("h5")]);
905
+ }
906
+ });
907
+ }
908
+ },
909
+ {
910
+ key: "h6",
911
+ label: t("toolbar.block.h6") || "Heading 6",
912
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M5 4v3h5.5v12h3V7H19V4z"/></svg>`,
913
+ keywords: ["h6", "heading6"],
914
+ execute: (editor) => {
915
+ editor.update(() => {
916
+ const selection = $getSelection();
917
+ if ($isRangeSelection(selection)) {
918
+ $insertNodes([$createHeadingNode("h6")]);
919
+ }
920
+ });
921
+ }
922
+ },
923
+ {
924
+ key: "ul",
925
+ label: t("toolbar.block.bullet") || "Bullet List",
926
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm0-6c-.83 0-1.5.67-1.5 1.5S3.17 7.5 4 7.5 5.5 6.83 5.5 6 4.83 4.5 4 4.5zm0 12c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zM7 19h14v-2H7v2zm0-6h14v-2H7v2zm0-8v2h14V5H7z"/></svg>`,
927
+ keywords: ["ul", "list", "bullet"],
928
+ execute: (editor) => {
929
+ editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
930
+ }
931
+ },
932
+ {
933
+ key: "ol",
934
+ label: t("toolbar.block.number") || "Numbered List",
935
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1v.9h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z"/></svg>`,
936
+ keywords: ["ol", "list", "number", "ordered"],
937
+ execute: (editor) => {
938
+ editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
939
+ }
940
+ },
941
+ {
942
+ key: "code",
943
+ label: t("toolbar.block.code") || "Code Block",
944
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>`,
945
+ keywords: ["code", "block", "javascript", "ts"],
946
+ execute: (editor) => {
947
+ editor.update(() => {
948
+ const selection = $getSelection();
949
+ if ($isRangeSelection(selection)) {
950
+ $insertNodes([$createCodeNode()]);
951
+ }
952
+ });
953
+ }
954
+ },
955
+ {
956
+ key: "quote",
957
+ label: t("toolbar.block.quote") || "Quote",
958
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>`,
959
+ keywords: ["quote", "blockquote"],
960
+ execute: (editor) => {
961
+ editor.update(() => {
962
+ const selection = $getSelection();
963
+ if ($isRangeSelection(selection)) {
964
+ $insertNodes([$createQuoteNode()]);
965
+ }
966
+ });
967
+ }
968
+ },
969
+ {
970
+ key: "image",
971
+ label: t("toolbar.image") || "Image",
972
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`,
973
+ keywords: ["image", "photo", "picture"],
974
+ execute: (editor) => {
975
+ const dialog = new ImageInsertDialog({
976
+ t: editor.t,
977
+ onSubmit: async (data) => {
978
+ try {
979
+ let src = data.src || "";
980
+ const meta = data.type === "file" && data.file ? await Promise.resolve(editor._imageMetaResolver?.(data.file)).catch(() => void 0) : void 0;
981
+ if (data.type === "file" && data.file) {
982
+ const file = data.file;
983
+ const strategy = editor._imageStorageStrategy ?? "auto";
984
+ const shouldBase64 = strategy === "base64" || strategy === "auto" && !editor._imageUploadHandler;
985
+ if (!shouldBase64) {
986
+ if (!editor._imageUploadHandler) {
987
+ throw new Error("imageUploadHandler is required when imageStorage is 'upload'");
988
+ }
989
+ src = await editor._imageUploadHandler(file);
990
+ } else {
991
+ src = await new Promise((resolve) => {
992
+ const reader = new FileReader();
993
+ reader.onload = () => resolve(reader.result);
994
+ reader.readAsDataURL(file);
995
+ });
996
+ }
997
+ }
998
+ if (src) {
999
+ editor.update(() => {
1000
+ const node = $createImageNode({
1001
+ src,
1002
+ altText: data.alt,
1003
+ maxWidth: 500,
1004
+ meta: meta || void 0
1005
+ });
1006
+ $insertNodes([node]);
1007
+ });
1008
+ }
1009
+ } catch (e) {
1010
+ console.error("Image insert failed", e);
1011
+ }
1012
+ }
1013
+ });
1014
+ dialog.show();
1015
+ }
1016
+ },
1017
+ {
1018
+ key: "inline-image",
1019
+ label: t("toolbar.inlineImage") || "Inline Image",
1020
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`,
1021
+ keywords: ["inline-image", "inline", "image", "img"],
1022
+ execute: (editor) => {
1023
+ const dialog = new ImageInsertDialog({
1024
+ t: editor.t,
1025
+ onSubmit: async (data) => {
1026
+ try {
1027
+ let src = data.src || "";
1028
+ const meta = data.type === "file" && data.file ? await Promise.resolve(editor._imageMetaResolver?.(data.file)).catch(() => void 0) : void 0;
1029
+ if (data.type === "file" && data.file) {
1030
+ const file = data.file;
1031
+ const strategy = editor._imageStorageStrategy ?? "auto";
1032
+ const shouldBase64 = strategy === "base64" || strategy === "auto" && !editor._imageUploadHandler;
1033
+ if (!shouldBase64) {
1034
+ if (!editor._imageUploadHandler) {
1035
+ throw new Error("imageUploadHandler is required when imageStorage is 'upload'");
1036
+ }
1037
+ src = await editor._imageUploadHandler(file);
1038
+ } else {
1039
+ src = await new Promise((resolve) => {
1040
+ const reader = new FileReader();
1041
+ reader.onload = () => resolve(reader.result);
1042
+ reader.readAsDataURL(file);
1043
+ });
1044
+ }
1045
+ }
1046
+ if (src) {
1047
+ editor.update(() => {
1048
+ const node = $createInlineImageNode({
1049
+ src,
1050
+ altText: data.alt,
1051
+ maxWidth: 500,
1052
+ width: 100,
1053
+ meta: meta || void 0
1054
+ });
1055
+ $insertNodes([node]);
1056
+ });
1057
+ }
1058
+ } catch (e) {
1059
+ console.error("Inline image insert failed", e);
1060
+ }
1061
+ }
1062
+ });
1063
+ dialog.show();
1064
+ }
1065
+ },
1066
+ {
1067
+ key: "table",
1068
+ label: t("toolbar.insertTable") || "Table",
1069
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20 2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM8 20H4v-4h4v4zm0-6H4v-4h4v4zm0-6H4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4zm6 12h-4v-4h4v4zm0-6h-4v-4h4v4zm0-6h-4V4h4v4z"/></svg>`,
1070
+ keywords: ["table", "grid"],
1071
+ execute: (editor) => {
1072
+ const dialog = new TableInsertDialog({
1073
+ t,
1074
+ onSubmit: (rows, cols) => {
1075
+ editor.update(() => {
1076
+ const node = $createTableNode(rows, cols);
1077
+ $insertNodes([node]);
1078
+ });
1079
+ }
1080
+ });
1081
+ dialog.show();
1082
+ }
1083
+ },
1084
+ {
1085
+ key: "hr",
1086
+ label: t("toolbar.horizontalRule") || "Horizontal Rule",
1087
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4 11h16v2H4z"/></svg>`,
1088
+ keywords: ["hr", "line", "separator"],
1089
+ execute: (editor) => {
1090
+ editor.update(() => {
1091
+ const selection = $getSelection();
1092
+ if ($isRangeSelection(selection)) {
1093
+ $insertNodes([$createHorizontalRuleNode()]);
1094
+ }
1095
+ });
1096
+ }
1097
+ },
1098
+ {
1099
+ key: "page-break",
1100
+ label: t("toolbar.pageBreak") || "Page Break",
1101
+ icon: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9 13h6v-2h-6v2zm-2 2H5v-2h2v2zm-2-4H3v2h2v-2zm0 8h2v-2H5v2zm14-2v-2h-2v2h2zm-2 4h2v-2h-2v2zM6 7H4v2h2V7zm14 0v2h-2V7h2zm-8-2h6v2h-6V5zM8 7H6v2h2V7zm8 2v2h2V9h-2zm-4 4h2v-2h-2v2zm-4 0h2v-2H8v2zm4 4h2v-2h-2v2z"/></svg>`,
1102
+ keywords: ["page", "break", "split"],
1103
+ execute: (editor) => {
1104
+ editor.update(() => {
1105
+ const selection = $getSelection();
1106
+ if ($isRangeSelection(selection)) {
1107
+ $insertNodes([$createPageBreakNode()]);
1108
+ }
1109
+ });
1110
+ }
1111
+ }
1112
+ ];
1113
+ }
1114
+ register() {
1115
+ this.editor.registerUpdateListener(({ editorState }) => {
1116
+ editorState.read(() => {
1117
+ const selection = $getSelection();
1118
+ if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
1119
+ this.hideMenu();
1120
+ return;
1121
+ }
1122
+ const anchor = selection.anchor;
1123
+ const node = anchor.getNode();
1124
+ if (node instanceof TextNode) {
1125
+ const textContent = node.getTextContent();
1126
+ const block = node.getTopLevelElementOrThrow();
1127
+ const blockText = block.getTextContent();
1128
+ if (blockText.startsWith("/") && selection.anchor.offset <= blockText.length) {
1129
+ const query = blockText.substring(1);
1130
+ this.queryString = query;
1131
+ this.showMenu(selection);
1132
+ return;
1133
+ }
1134
+ }
1135
+ this.hideMenu();
1136
+ });
1137
+ });
1138
+ this.editor.registerCommand(
1139
+ KEY_ARROW_UP_COMMAND,
1140
+ (event) => {
1141
+ if (this.active) {
1142
+ event.preventDefault();
1143
+ this.moveSelection(-1);
1144
+ return true;
1145
+ }
1146
+ return false;
1147
+ },
1148
+ COMMAND_PRIORITY_LOW
1149
+ );
1150
+ this.editor.registerCommand(
1151
+ KEY_ARROW_DOWN_COMMAND,
1152
+ (event) => {
1153
+ if (this.active) {
1154
+ event.preventDefault();
1155
+ this.moveSelection(1);
1156
+ return true;
1157
+ }
1158
+ return false;
1159
+ },
1160
+ COMMAND_PRIORITY_LOW
1161
+ );
1162
+ this.editor.registerCommand(
1163
+ KEY_ENTER_COMMAND,
1164
+ (event) => {
1165
+ if (this.active) {
1166
+ event?.preventDefault();
1167
+ this.executeSelection();
1168
+ return true;
1169
+ }
1170
+ return false;
1171
+ },
1172
+ COMMAND_PRIORITY_LOW
1173
+ );
1174
+ this.editor.registerCommand(
1175
+ KEY_ESCAPE_COMMAND,
1176
+ (event) => {
1177
+ if (this.active) {
1178
+ event.preventDefault();
1179
+ this.hideMenu();
1180
+ return true;
1181
+ }
1182
+ return false;
1183
+ },
1184
+ COMMAND_PRIORITY_LOW
1185
+ );
1186
+ }
1187
+ filterItems() {
1188
+ if (!this.queryString) {
1189
+ this.filteredItems = this.items;
1190
+ return;
1191
+ }
1192
+ const q = this.queryString.toLowerCase();
1193
+ this.filteredItems = this.items.filter(
1194
+ (item) => item.label.toLowerCase().includes(q) || item.keywords.some((k) => k.toLowerCase().includes(q))
1195
+ );
1196
+ }
1197
+ showMenu(selection) {
1198
+ this.active = true;
1199
+ this.filterItems();
1200
+ if (this.filteredItems.length === 0) {
1201
+ this.hideMenu();
1202
+ return;
1203
+ }
1204
+ if (!this.menuElement) {
1205
+ this.menuElement = document.createElement("div");
1206
+ this.menuElement.className = "sex-slash-menu";
1207
+ document.body.appendChild(this.menuElement);
1208
+ }
1209
+ this.menuElement.innerHTML = "";
1210
+ this.menuElement.style.display = "block";
1211
+ this.filteredItems.forEach((item, index) => {
1212
+ const el = document.createElement("div");
1213
+ el.className = `sex-slash-menu-item ${index === this.selectedIndex ? "selected" : ""}`;
1214
+ el.innerHTML = `
1215
+ <span class="sex-slash-menu-icon">${item.icon}</span>
1216
+ <span class="sex-slash-menu-label">${item.label}</span>
1217
+ `;
1218
+ el.onclick = () => {
1219
+ this.selectedIndex = index;
1220
+ this.executeSelection();
1221
+ };
1222
+ this.menuElement?.appendChild(el);
1223
+ });
1224
+ const domSelection = window.getSelection();
1225
+ if (domSelection && domSelection.rangeCount > 0) {
1226
+ const range = domSelection.getRangeAt(0);
1227
+ const rect = range.getBoundingClientRect();
1228
+ if (this.menuElement) {
1229
+ this.menuElement.style.top = `${rect.bottom + window.scrollY + 5}px`;
1230
+ this.menuElement.style.left = `${rect.left + window.scrollX}px`;
1231
+ }
1232
+ }
1233
+ }
1234
+ hideMenu() {
1235
+ this.active = false;
1236
+ this.queryString = null;
1237
+ this.selectedIndex = 0;
1238
+ if (this.menuElement) {
1239
+ this.menuElement.style.display = "none";
1240
+ }
1241
+ }
1242
+ moveSelection(delta) {
1243
+ this.selectedIndex += delta;
1244
+ if (this.selectedIndex < 0) {
1245
+ this.selectedIndex = this.filteredItems.length - 1;
1246
+ } else if (this.selectedIndex >= this.filteredItems.length) {
1247
+ this.selectedIndex = 0;
1248
+ }
1249
+ this.updateMenuUI();
1250
+ }
1251
+ updateMenuUI() {
1252
+ if (!this.menuElement) return;
1253
+ const items = this.menuElement.querySelectorAll(".sex-slash-menu-item");
1254
+ items.forEach((item, index) => {
1255
+ if (index === this.selectedIndex) {
1256
+ item.classList.add("selected");
1257
+ item.scrollIntoView({ block: "nearest" });
1258
+ } else {
1259
+ item.classList.remove("selected");
1260
+ }
1261
+ });
1262
+ }
1263
+ executeSelection() {
1264
+ const item = this.filteredItems[this.selectedIndex];
1265
+ if (item) {
1266
+ const deleteCount = (this.queryString?.length || 0) + 1;
1267
+ this.hideMenu();
1268
+ this.editor.update(() => {
1269
+ const selection = $getSelection();
1270
+ if ($isRangeSelection(selection)) {
1271
+ const anchor = selection.anchor;
1272
+ const node = anchor.getNode();
1273
+ if (node instanceof TextNode) {
1274
+ node.spliceText(0, deleteCount, "", true);
1275
+ }
1276
+ }
1277
+ }, {
1278
+ onUpdate: () => {
1279
+ item.execute(this.editor);
1280
+ }
1281
+ });
1282
+ }
1283
+ }
1284
+ };
1285
+ function registerSlashPlugin(editor) {
1286
+ const plugin = new SlashMenuPlugin(editor);
1287
+ plugin.register();
1288
+ return plugin;
1289
+ }
1290
+ export {
1291
+ SlashMenuPlugin,
1292
+ registerSlashPlugin
1293
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@sex-editor/slash",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "tsup": "8.3.5",
17
+ "typescript": "^5.6.3",
18
+ "@sex-editor/core": "0.0.2",
19
+ "@sex-editor/ui": "0.0.1"
20
+ },
21
+ "peerDependencies": {
22
+ "@sex-editor/core": ">=0.0.2 <0.0.3",
23
+ "lexical": "^0.39.0",
24
+ "@lexical/link": "^0.39.0",
25
+ "@lexical/list": "^0.39.0",
26
+ "@lexical/rich-text": "^0.39.0",
27
+ "@lexical/selection": "^0.39.0",
28
+ "@lexical/code": "^0.39.0"
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "sideEffects": [
34
+ "**/*.css"
35
+ ],
36
+ "keywords": [],
37
+ "author": "",
38
+ "license": "ISC",
39
+ "scripts": {
40
+ "dev": "tsup --watch",
41
+ "build": "tsup"
42
+ }
43
+ }