autumnnote 2.0.0 → 2.2.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.
- package/README.md +72 -5
- package/dist/autumnnote.cjs +20 -20
- package/dist/autumnnote.css +1 -1
- package/dist/autumnnote.es.js +661 -798
- package/dist/autumnnote.es.js.map +1 -1
- package/dist/autumnnote.min.js +20 -20
- package/dist/autumnnote.umd.js +20 -20
- package/dist/autumnnote.umd.js.map +1 -1
- package/dist/icon-data-V0Xqv-wX.js +255 -0
- package/dist/icon-data-V0Xqv-wX.js.map +1 -0
- package/package.json +13 -4
- package/types/index.d.ts +8 -0
- package/src/js/Context.js +0 -854
- package/src/js/core/detectLang.js +0 -98
- package/src/js/core/dom.js +0 -372
- package/src/js/core/env.js +0 -25
- package/src/js/core/key.js +0 -66
- package/src/js/core/lists.js +0 -121
- package/src/js/core/markdown.js +0 -695
- package/src/js/core/range.js +0 -194
- package/src/js/core/sanitise.js +0 -231
- package/src/js/editing/History.js +0 -266
- package/src/js/editing/Style.js +0 -812
- package/src/js/editing/Table.js +0 -105
- package/src/js/editing/Typing.js +0 -397
- package/src/js/index.js +0 -193
- package/src/js/index.umd.js +0 -17
- package/src/js/module/AutoSaveRestore.js +0 -125
- package/src/js/module/BaseDialog.js +0 -133
- package/src/js/module/BaseMediaTooltip.js +0 -142
- package/src/js/module/BaseResizer.js +0 -312
- package/src/js/module/BubbleToolbar.js +0 -483
- package/src/js/module/Buttons.js +0 -399
- package/src/js/module/Clipboard.js +0 -579
- package/src/js/module/CodeTooltip.js +0 -493
- package/src/js/module/Codeview.js +0 -125
- package/src/js/module/ContextMenu.js +0 -621
- package/src/js/module/Editor.js +0 -747
- package/src/js/module/EmojiDialog.js +0 -254
- package/src/js/module/FindReplace.js +0 -512
- package/src/js/module/Fullscreen.js +0 -80
- package/src/js/module/IconDialog.js +0 -618
- package/src/js/module/ImageCropOverlay.js +0 -586
- package/src/js/module/ImageDialog.js +0 -193
- package/src/js/module/ImageResizer.js +0 -42
- package/src/js/module/ImageTooltip.js +0 -285
- package/src/js/module/LinkDialog.js +0 -145
- package/src/js/module/LinkTooltip.js +0 -250
- package/src/js/module/MarkdownShortcuts.js +0 -250
- package/src/js/module/Mention.js +0 -365
- package/src/js/module/Placeholder.js +0 -51
- package/src/js/module/ShortcutsDialog.js +0 -111
- package/src/js/module/SlashMenu.js +0 -376
- package/src/js/module/Statusbar.js +0 -246
- package/src/js/module/TableTooltip.js +0 -1521
- package/src/js/module/Toolbar.js +0 -750
- package/src/js/module/VideoDialog.js +0 -193
- package/src/js/module/VideoResizer.js +0 -66
- package/src/js/module/VideoTooltip.js +0 -248
- package/src/js/module/emoji-data.js +0 -496
- package/src/js/renderer.js +0 -120
- package/src/js/settings.js +0 -214
- package/src/styles/_variables.scss +0 -48
- package/src/styles/autumnnote.scss +0 -2866
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
// LinkTooltip.js - Hover tooltip for links inside the editor
|
|
2
|
-
// Displays a small action bar with: visit link, edit link, unlink
|
|
3
|
-
import { createElement, on } from '../core/dom.js';
|
|
4
|
-
|
|
5
|
-
const ICONS = {
|
|
6
|
-
open: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`,
|
|
7
|
-
edit: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
|
|
8
|
-
unlink: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18.84 12.25l1.72-1.71h-.02a5.004 5.004 0 0 0-.12-7.07 5.006 5.006 0 0 0-6.95 0l-1.72 1.71"/><path d="M5.17 11.75l-1.71 1.71a5.004 5.004 0 0 0 .12 7.07 5.006 5.006 0 0 0 6.95 0l1.71-1.71"/><line x1="8" y1="2" x2="8" y2="5"/><line x1="2" y1="8" x2="5" y2="8"/><line x1="16" y1="19" x2="16" y2="22"/><line x1="19" y1="16" x2="22" y2="16"/></svg>`,
|
|
9
|
-
copy: `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/** Delay in ms before showing / after hiding to avoid flicker */
|
|
13
|
-
const SHOW_DELAY = 120;
|
|
14
|
-
const HIDE_DELAY = 200;
|
|
15
|
-
|
|
16
|
-
export class LinkTooltip {
|
|
17
|
-
/** @param {import('../Context.js').Context} context */
|
|
18
|
-
constructor(context) {
|
|
19
|
-
this.context = context;
|
|
20
|
-
this._el = null;
|
|
21
|
-
this._activeAnchor = null;
|
|
22
|
-
this._showTimer = null;
|
|
23
|
-
this._hideTimer = null;
|
|
24
|
-
this._disposers = [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
initialize() {
|
|
28
|
-
this._el = this._buildTooltip();
|
|
29
|
-
document.body.appendChild(this._el);
|
|
30
|
-
|
|
31
|
-
const editable = this.context.layoutInfo.editable;
|
|
32
|
-
|
|
33
|
-
this._disposers.push(
|
|
34
|
-
// Detect when pointer enters a link
|
|
35
|
-
on(editable, 'mouseover', (e) => {
|
|
36
|
-
const anchor = /** @type {Element} */ (e.target)?.closest('a[href]');
|
|
37
|
-
if (anchor && editable.contains(anchor)) {
|
|
38
|
-
this._scheduleShow(anchor);
|
|
39
|
-
}
|
|
40
|
-
}),
|
|
41
|
-
// Detect when pointer leaves the editable area entirely
|
|
42
|
-
on(editable, 'mouseout', (e) => {
|
|
43
|
-
const to = /** @type {Node|null} */ (/** @type {MouseEvent} */ (e).relatedTarget);
|
|
44
|
-
if (!to || (!editable.contains(to) && !this._el.contains(to))) {
|
|
45
|
-
this._scheduleHide();
|
|
46
|
-
}
|
|
47
|
-
}),
|
|
48
|
-
// Hide when the page scrolls or resizes — the tooltip position becomes stale
|
|
49
|
-
on(globalThis, 'scroll', () => this._hide(), { passive: true }),
|
|
50
|
-
on(globalThis, 'resize', () => this._hide(), { passive: true }),
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
return this;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
destroy() {
|
|
57
|
-
this._clearTimers();
|
|
58
|
-
this._disposers.forEach((d) => d());
|
|
59
|
-
this._disposers = [];
|
|
60
|
-
if (this._el?.parentNode) this._el.remove();
|
|
61
|
-
this._el = null;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// Build
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
_buildTooltip() {
|
|
69
|
-
const L = this.context.locale.tooltips.link;
|
|
70
|
-
const el = createElement('div', { class: 'an-link-tooltip', role: 'toolbar', 'aria-label': L.ariaLabel });
|
|
71
|
-
el.style.display = 'none';
|
|
72
|
-
|
|
73
|
-
// URL preview text (truncated)
|
|
74
|
-
this._urlLabel = createElement('span', { class: 'an-link-tooltip-url' });
|
|
75
|
-
el.appendChild(this._urlLabel);
|
|
76
|
-
|
|
77
|
-
// Separator
|
|
78
|
-
el.appendChild(createElement('div', { class: 'an-link-tooltip-sep' }));
|
|
79
|
-
|
|
80
|
-
// Action buttons
|
|
81
|
-
this._openBtn = this._makeBtn(ICONS.open, L.openLink, () => this._openLink());
|
|
82
|
-
this._copyBtn = this._makeBtn(ICONS.copy, L.copyUrl, () => this._copyLink());
|
|
83
|
-
this._editBtn = this._makeBtn(ICONS.edit, L.editLink, () => this._editLink());
|
|
84
|
-
this._unlinkBtn = this._makeBtn(ICONS.unlink, L.removeLink, () => this._unlink());
|
|
85
|
-
|
|
86
|
-
el.appendChild(this._openBtn);
|
|
87
|
-
el.appendChild(this._copyBtn);
|
|
88
|
-
el.appendChild(this._editBtn);
|
|
89
|
-
el.appendChild(this._unlinkBtn);
|
|
90
|
-
|
|
91
|
-
// Keep tooltip alive while hovering it
|
|
92
|
-
this._disposers.push(
|
|
93
|
-
on(el, 'mouseenter', () => this._clearTimers()),
|
|
94
|
-
on(el, 'mouseleave', () => this._scheduleHide()),
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
return el;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
_makeBtn(icon, title, handler) {
|
|
101
|
-
const btn = createElement('button', { type: 'button', class: 'an-link-tooltip-btn', title });
|
|
102
|
-
btn.innerHTML = icon;
|
|
103
|
-
this._disposers.push(on(btn, 'click', (e) => { e.preventDefault(); e.stopPropagation(); handler(); }));
|
|
104
|
-
return btn;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Show / Hide logic
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
|
|
111
|
-
_scheduleShow(anchor) {
|
|
112
|
-
if (this._activeAnchor === anchor && this._el.style.display !== 'none') return;
|
|
113
|
-
clearTimeout(this._hideTimer);
|
|
114
|
-
this._hideTimer = null;
|
|
115
|
-
clearTimeout(this._showTimer);
|
|
116
|
-
this._showTimer = setTimeout(() => {
|
|
117
|
-
this._activeAnchor = anchor;
|
|
118
|
-
this._show(anchor);
|
|
119
|
-
}, SHOW_DELAY);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
_scheduleHide() {
|
|
123
|
-
clearTimeout(this._showTimer);
|
|
124
|
-
this._showTimer = null;
|
|
125
|
-
if (this._hideTimer) return;
|
|
126
|
-
this._hideTimer = setTimeout(() => {
|
|
127
|
-
this._hide();
|
|
128
|
-
}, HIDE_DELAY);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
_show(anchor) {
|
|
132
|
-
const isReadOnly = this.context.layoutInfo.container.classList.contains('an-disabled');
|
|
133
|
-
const url = anchor.getAttribute('href') || '';
|
|
134
|
-
this._urlLabel.textContent = this._truncateUrl(url);
|
|
135
|
-
this._urlLabel.title = url;
|
|
136
|
-
// In read-only mode keep only open + copy; hide edit and unlink
|
|
137
|
-
this._editBtn.style.display = isReadOnly ? 'none' : '';
|
|
138
|
-
this._unlinkBtn.style.display = isReadOnly ? 'none' : '';
|
|
139
|
-
this._el.style.display = 'flex';
|
|
140
|
-
this._positionNear(anchor);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
_hide() {
|
|
144
|
-
this._el.style.display = 'none';
|
|
145
|
-
this._activeAnchor = null;
|
|
146
|
-
this._clearTimers();
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
_clearTimers() {
|
|
150
|
-
clearTimeout(this._showTimer);
|
|
151
|
-
clearTimeout(this._hideTimer);
|
|
152
|
-
this._showTimer = null;
|
|
153
|
-
this._hideTimer = null;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
_positionNear(anchor) {
|
|
157
|
-
const rect = anchor.getBoundingClientRect();
|
|
158
|
-
const tipW = this._el.offsetWidth || 260;
|
|
159
|
-
const tipH = this._el.offsetHeight || 34;
|
|
160
|
-
const margin = 6;
|
|
161
|
-
|
|
162
|
-
// Prefer below the link; if no room, put above
|
|
163
|
-
let top = rect.bottom + margin;
|
|
164
|
-
let left = rect.left;
|
|
165
|
-
|
|
166
|
-
if (top + tipH > globalThis.innerHeight - margin) {
|
|
167
|
-
top = rect.top - tipH - margin;
|
|
168
|
-
}
|
|
169
|
-
if (left + tipW > globalThis.innerWidth - margin) {
|
|
170
|
-
left = globalThis.innerWidth - tipW - margin;
|
|
171
|
-
}
|
|
172
|
-
if (left < margin) left = margin;
|
|
173
|
-
|
|
174
|
-
this._el.style.top = `${top}px`;
|
|
175
|
-
this._el.style.left = `${left}px`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
_truncateUrl(url) {
|
|
179
|
-
try {
|
|
180
|
-
const u = new URL(url);
|
|
181
|
-
const display = u.host + (u.pathname === '/' ? '' : u.pathname);
|
|
182
|
-
return display.length > 48 ? display.slice(0, 48) + '…' : display;
|
|
183
|
-
} catch {
|
|
184
|
-
return url.length > 48 ? url.slice(0, 48) + '…' : url;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
// Actions
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
|
|
192
|
-
_openLink() {
|
|
193
|
-
const url = this._activeAnchor?.getAttribute('href');
|
|
194
|
-
if (url) globalThis.open(url, '_blank', 'noopener,noreferrer');
|
|
195
|
-
this._hide();
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
_copyLink() {
|
|
199
|
-
const url = this._activeAnchor?.getAttribute('href');
|
|
200
|
-
if (url) {
|
|
201
|
-
navigator.clipboard.writeText(url).catch(() => {
|
|
202
|
-
// Fallback for environments where clipboard API is unavailable
|
|
203
|
-
const ta = document.createElement('textarea');
|
|
204
|
-
ta.value = url;
|
|
205
|
-
ta.style.position = 'fixed';
|
|
206
|
-
ta.style.opacity = '0';
|
|
207
|
-
document.body.appendChild(ta);
|
|
208
|
-
ta.select();
|
|
209
|
-
document.execCommand('copy');
|
|
210
|
-
ta.remove();
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
// Brief visual feedback
|
|
214
|
-
if (this._copyBtn) {
|
|
215
|
-
this._copyBtn.classList.add('an-link-tooltip-btn--copied');
|
|
216
|
-
setTimeout(() => this._copyBtn?.classList.remove('an-link-tooltip-btn--copied'), 1000);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
_editLink() {
|
|
221
|
-
const anchor = this._activeAnchor;
|
|
222
|
-
if (!anchor) return;
|
|
223
|
-
this._hide();
|
|
224
|
-
|
|
225
|
-
// Place cursor inside the anchor, then open the link dialog (which pre-fills from selection)
|
|
226
|
-
const sel = globalThis.getSelection();
|
|
227
|
-
const range = document.createRange();
|
|
228
|
-
range.selectNodeContents(anchor);
|
|
229
|
-
sel.removeAllRanges();
|
|
230
|
-
sel.addRange(range);
|
|
231
|
-
|
|
232
|
-
this.context.invoke('linkDialog.show');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
_unlink() {
|
|
236
|
-
const anchor = this._activeAnchor;
|
|
237
|
-
if (!anchor) return;
|
|
238
|
-
this._hide();
|
|
239
|
-
|
|
240
|
-
// Select the anchor text, then remove the link
|
|
241
|
-
const sel = globalThis.getSelection();
|
|
242
|
-
const range = document.createRange();
|
|
243
|
-
range.selectNode(anchor);
|
|
244
|
-
sel.removeAllRanges();
|
|
245
|
-
sel.addRange(range);
|
|
246
|
-
|
|
247
|
-
document.execCommand('unlink');
|
|
248
|
-
this.context.invoke('editor.afterCommand');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MarkdownShortcuts.js — Convert Markdown-style syntax typed directly in the
|
|
3
|
-
* editor into rich HTML elements (input rules / auto-format).
|
|
4
|
-
*
|
|
5
|
-
* Activated when `markdownShortcuts: true` (the default).
|
|
6
|
-
*
|
|
7
|
-
* Block rules — triggered by Space or Enter at the start of a line:
|
|
8
|
-
* #[#[#]]· → H1 / H2 / H3
|
|
9
|
-
* >· → blockquote
|
|
10
|
-
* -· or *· → unordered list item
|
|
11
|
-
* 1.· → ordered list item
|
|
12
|
-
* [ ]· → checklist item
|
|
13
|
-
* --- → horizontal rule (on Enter)
|
|
14
|
-
* ``` → code block (on Enter)
|
|
15
|
-
*
|
|
16
|
-
* Inline rules — triggered when the closing marker is typed:
|
|
17
|
-
* **text** → <strong>
|
|
18
|
-
* *text* → <em>
|
|
19
|
-
* ~~text~~ → <s>
|
|
20
|
-
* `code` → <code>
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { on } from '../core/dom.js';
|
|
24
|
-
|
|
25
|
-
export class MarkdownShortcuts {
|
|
26
|
-
/** @param {import('../Context.js').Context} context */
|
|
27
|
-
constructor(context) {
|
|
28
|
-
this.context = context;
|
|
29
|
-
this.options = context.options;
|
|
30
|
-
this._disposers = [];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
initialize() {
|
|
34
|
-
if (!this.options.markdownShortcuts) return this;
|
|
35
|
-
const editable = this.context.layoutInfo.editable;
|
|
36
|
-
const d1 = on(editable, 'keydown', /** @param {KeyboardEvent} e */ (e) => this._onKeydown(e));
|
|
37
|
-
const d2 = on(editable, 'input', () => this._onInput());
|
|
38
|
-
this._disposers.push(d1, d2);
|
|
39
|
-
return this;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
destroy() {
|
|
43
|
-
this._disposers.forEach((d) => d());
|
|
44
|
-
this._disposers = [];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// Block rules (Space / Enter)
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
_onKeydown(e) {
|
|
52
|
-
if (e.key === ' ') {
|
|
53
|
-
if (this._applyBlockRule()) e.preventDefault();
|
|
54
|
-
} else if (e.key === 'Enter') {
|
|
55
|
-
if (this._applyEnterRule()) e.preventDefault();
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Returns the plain text of the line the cursor is on, up to the cursor.
|
|
61
|
-
* @returns {{ text: string, range: Range, lineNode: Node } | null}
|
|
62
|
-
*/
|
|
63
|
-
_getLineContext() {
|
|
64
|
-
const sel = globalThis.getSelection();
|
|
65
|
-
if (!sel?.rangeCount) return null;
|
|
66
|
-
const range = sel.getRangeAt(0);
|
|
67
|
-
if (!range.collapsed) return null;
|
|
68
|
-
|
|
69
|
-
const editable = this.context.layoutInfo.editable;
|
|
70
|
-
if (!editable.contains(range.startContainer)) return null;
|
|
71
|
-
|
|
72
|
-
// Walk up to find the block-level ancestor inside the editable
|
|
73
|
-
let node = range.startContainer;
|
|
74
|
-
while (node && node !== editable && !this._isBlock(node)) {
|
|
75
|
-
node = node.parentNode;
|
|
76
|
-
}
|
|
77
|
-
if (!node || node === editable) node = range.startContainer;
|
|
78
|
-
|
|
79
|
-
// Collect all text content before the cursor within that block
|
|
80
|
-
const tmpRange = document.createRange();
|
|
81
|
-
tmpRange.setStart(node, 0);
|
|
82
|
-
tmpRange.setEnd(range.startContainer, range.startOffset);
|
|
83
|
-
const text = tmpRange.toString();
|
|
84
|
-
|
|
85
|
-
return { text, range, lineNode: node };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
_isBlock(node) {
|
|
89
|
-
if (node.nodeType !== Node.ELEMENT_NODE) return false;
|
|
90
|
-
const display = globalThis.getComputedStyle(node).display;
|
|
91
|
-
return display === 'block' || display === 'list-item' || display === 'table-cell';
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Applies block rule on Space key. Returns true if a rule fired. */
|
|
95
|
-
_applyBlockRule() {
|
|
96
|
-
const ctx = this._getLineContext();
|
|
97
|
-
if (!ctx) return false;
|
|
98
|
-
const { text } = ctx;
|
|
99
|
-
|
|
100
|
-
const blockPatterns = [
|
|
101
|
-
{ re: /^(#{1,3})$/, handler: (m) => this._convertToHeading(m[1].length) },
|
|
102
|
-
{ re: /^>$/, handler: () => this._convertToBlockquote() },
|
|
103
|
-
{ re: /^[-*]$/, handler: () => this._convertToList('ul') },
|
|
104
|
-
{ re: /^1\.$/, handler: () => this._convertToList('ol') },
|
|
105
|
-
{ re: /^\[ \]$/, handler: () => this._convertToChecklist() },
|
|
106
|
-
];
|
|
107
|
-
|
|
108
|
-
for (const { re, handler } of blockPatterns) {
|
|
109
|
-
const m = re.exec(text);
|
|
110
|
-
if (m) {
|
|
111
|
-
handler(m);
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Applies block rule on Enter key (---, ```). Returns true if a rule fired. */
|
|
119
|
-
_applyEnterRule() {
|
|
120
|
-
const ctx = this._getLineContext();
|
|
121
|
-
if (!ctx) return false;
|
|
122
|
-
const { text } = ctx;
|
|
123
|
-
|
|
124
|
-
if (/^-{3,}$/.test(text)) {
|
|
125
|
-
this._convertToHr();
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
if (/^`{3}/.test(text)) {
|
|
129
|
-
this._convertToCodeBlock();
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// Block converters
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
_selectLineAndDelete() {
|
|
140
|
-
const sel = globalThis.getSelection();
|
|
141
|
-
if (!sel?.rangeCount) return;
|
|
142
|
-
const range = sel.getRangeAt(0);
|
|
143
|
-
// Select from the start of the block to the cursor and delete
|
|
144
|
-
const startRange = document.createRange();
|
|
145
|
-
startRange.setStart(range.startContainer.parentNode || range.startContainer, 0);
|
|
146
|
-
startRange.setEnd(range.startContainer, range.startOffset);
|
|
147
|
-
startRange.deleteContents();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
_convertToHeading(level) {
|
|
151
|
-
this._selectLineAndDelete();
|
|
152
|
-
document.execCommand('formatBlock', false, `h${level}`);
|
|
153
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
_convertToBlockquote() {
|
|
157
|
-
this._selectLineAndDelete();
|
|
158
|
-
document.execCommand('formatBlock', false, 'blockquote');
|
|
159
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
_convertToList(type) {
|
|
163
|
-
this._selectLineAndDelete();
|
|
164
|
-
document.execCommand(type === 'ul' ? 'insertUnorderedList' : 'insertOrderedList');
|
|
165
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
_convertToChecklist() {
|
|
169
|
-
this._selectLineAndDelete();
|
|
170
|
-
this.context.invoke('editor.toggleChecklist');
|
|
171
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
_convertToHr() {
|
|
175
|
-
this._selectLineAndDelete();
|
|
176
|
-
this.context.invoke('editor.insertHr');
|
|
177
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
_convertToCodeBlock() {
|
|
181
|
-
this._selectLineAndDelete();
|
|
182
|
-
document.execCommand('formatBlock', false, 'pre');
|
|
183
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// ---------------------------------------------------------------------------
|
|
187
|
-
// Inline rules (input event)
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
|
|
190
|
-
_onInput() {
|
|
191
|
-
const sel = globalThis.getSelection();
|
|
192
|
-
if (!sel?.rangeCount) return;
|
|
193
|
-
const range = sel.getRangeAt(0);
|
|
194
|
-
if (!range.collapsed) return;
|
|
195
|
-
|
|
196
|
-
const editable = this.context.layoutInfo.editable;
|
|
197
|
-
if (!editable.contains(range.startContainer)) return;
|
|
198
|
-
|
|
199
|
-
const node = range.startContainer;
|
|
200
|
-
if (node.nodeType !== Node.TEXT_NODE) return;
|
|
201
|
-
|
|
202
|
-
const text = node.textContent;
|
|
203
|
-
const offset = range.startOffset;
|
|
204
|
-
|
|
205
|
-
const inlineRules = [
|
|
206
|
-
// **bold**
|
|
207
|
-
{ re: /\*\*(.+?)\*\*$/, tag: 'strong' },
|
|
208
|
-
// *italic* (not part of **)
|
|
209
|
-
{ re: /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)$/, tag: 'em' },
|
|
210
|
-
// ~~strike~~
|
|
211
|
-
{ re: /~~(.+?)~~$/, tag: 's' },
|
|
212
|
-
// `code`
|
|
213
|
-
{ re: /`([^`]+)`$/, tag: 'code' },
|
|
214
|
-
];
|
|
215
|
-
|
|
216
|
-
const upToCursor = text.slice(0, offset);
|
|
217
|
-
for (const { re, tag } of inlineRules) {
|
|
218
|
-
const m = re.exec(upToCursor);
|
|
219
|
-
if (!m) continue;
|
|
220
|
-
|
|
221
|
-
const matchStart = upToCursor.length - m[0].length;
|
|
222
|
-
const matchEnd = offset;
|
|
223
|
-
const innerText = m[1];
|
|
224
|
-
|
|
225
|
-
// Replace matched text with formatted element
|
|
226
|
-
const before = text.slice(0, matchStart);
|
|
227
|
-
const after = text.slice(matchEnd);
|
|
228
|
-
|
|
229
|
-
const el = document.createElement(tag);
|
|
230
|
-
el.textContent = innerText;
|
|
231
|
-
|
|
232
|
-
// Rebuild the text node and insert the element
|
|
233
|
-
const beforeNode = document.createTextNode(before);
|
|
234
|
-
const afterNode = document.createTextNode('' + after);
|
|
235
|
-
|
|
236
|
-
/** @type {ChildNode} */ (node).before(beforeNode, el, afterNode);
|
|
237
|
-
/** @type {ChildNode} */ (node).remove();
|
|
238
|
-
|
|
239
|
-
// Place cursor after the element (after the ZWS)
|
|
240
|
-
const newRange = document.createRange();
|
|
241
|
-
newRange.setStart(afterNode, 1);
|
|
242
|
-
newRange.collapse(true);
|
|
243
|
-
sel.removeAllRanges();
|
|
244
|
-
sel.addRange(newRange);
|
|
245
|
-
|
|
246
|
-
this.context.triggerEvent('change', this.context.getHTML());
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|