autumnnote 1.0.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/LICENSE +21 -0
- package/README.md +874 -0
- package/dist/autumnnote.css +1 -0
- package/dist/autumnnote.es.js +5888 -0
- package/dist/autumnnote.es.js.map +1 -0
- package/dist/autumnnote.umd.js +74 -0
- package/dist/autumnnote.umd.js.map +1 -0
- package/package.json +55 -0
- package/src/js/Context.js +497 -0
- package/src/js/core/dom.js +315 -0
- package/src/js/core/env.js +25 -0
- package/src/js/core/func.js +153 -0
- package/src/js/core/key.js +66 -0
- package/src/js/core/lists.js +121 -0
- package/src/js/core/markdown.js +294 -0
- package/src/js/core/range.js +194 -0
- package/src/js/core/sanitise.js +78 -0
- package/src/js/editing/History.js +205 -0
- package/src/js/editing/Style.js +329 -0
- package/src/js/editing/Table.js +59 -0
- package/src/js/editing/Typing.js +142 -0
- package/src/js/index.js +126 -0
- package/src/js/module/Buttons.js +300 -0
- package/src/js/module/Clipboard.js +460 -0
- package/src/js/module/CodeTooltip.js +428 -0
- package/src/js/module/Codeview.js +122 -0
- package/src/js/module/ContextMenu.js +470 -0
- package/src/js/module/Editor.js +528 -0
- package/src/js/module/EmojiDialog.js +726 -0
- package/src/js/module/FindReplace.js +440 -0
- package/src/js/module/Fullscreen.js +80 -0
- package/src/js/module/IconDialog.js +620 -0
- package/src/js/module/ImageDialog.js +208 -0
- package/src/js/module/ImageResizer.js +216 -0
- package/src/js/module/ImageTooltip.js +286 -0
- package/src/js/module/LinkDialog.js +204 -0
- package/src/js/module/LinkTooltip.js +242 -0
- package/src/js/module/Placeholder.js +44 -0
- package/src/js/module/ShortcutsDialog.js +141 -0
- package/src/js/module/Statusbar.js +238 -0
- package/src/js/module/TableTooltip.js +568 -0
- package/src/js/module/Toolbar.js +562 -0
- package/src/js/module/VideoDialog.js +263 -0
- package/src/js/module/VideoResizer.js +227 -0
- package/src/js/module/VideoTooltip.js +252 -0
- package/src/js/renderer.js +107 -0
- package/src/js/settings.js +134 -0
- package/src/styles/_variables.scss +48 -0
- package/src/styles/autumnnote.scss +1740 -0
- package/types/index.d.ts +324 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History.js - Undo / redo stack for editor content
|
|
3
|
+
* Inspired by Summernote's History module, rewritten without jQuery
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class History {
|
|
7
|
+
/**
|
|
8
|
+
* @param {HTMLElement} editable - the contenteditable element
|
|
9
|
+
* @param {number} [limit=100] - maximum number of undo/redo states
|
|
10
|
+
*/
|
|
11
|
+
constructor(editable, limit = 100) {
|
|
12
|
+
this.editable = editable;
|
|
13
|
+
this._limit = limit;
|
|
14
|
+
/** @type {Array<{html: string, range: {sc: string, so: number, ec: string, eo: number}|null}>} */
|
|
15
|
+
this.stack = [];
|
|
16
|
+
this.stackOffset = -1;
|
|
17
|
+
this._savePoint();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Private helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
_serialize() {
|
|
25
|
+
return this.editable.innerHTML;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Serializes the current selection as character offsets from the start of
|
|
30
|
+
* the editable element, so it can be restored after innerHTML replacement.
|
|
31
|
+
* @returns {{ start: number, end: number }|null}
|
|
32
|
+
*/
|
|
33
|
+
_serializeSelection() {
|
|
34
|
+
const sel = window.getSelection();
|
|
35
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
36
|
+
const range = sel.getRangeAt(0);
|
|
37
|
+
if (!this.editable.contains(range.startContainer)) return null;
|
|
38
|
+
return {
|
|
39
|
+
start: this._charOffset(range.startContainer, range.startOffset),
|
|
40
|
+
end: this._charOffset(range.endContainer, range.endOffset),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns the character offset of (node, offset) from the beginning of
|
|
46
|
+
* the editable's text content.
|
|
47
|
+
* @param {Node} node
|
|
48
|
+
* @param {number} offset
|
|
49
|
+
* @returns {number}
|
|
50
|
+
*/
|
|
51
|
+
_charOffset(node, offset) {
|
|
52
|
+
let count = 0;
|
|
53
|
+
const walker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
|
|
54
|
+
let cur;
|
|
55
|
+
while ((cur = walker.nextNode())) {
|
|
56
|
+
if (cur === node) return count + offset;
|
|
57
|
+
count += cur.length;
|
|
58
|
+
}
|
|
59
|
+
return count;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Restores a previously serialized selection inside the editable.
|
|
64
|
+
* @param {{ start: number, end: number }|null} saved
|
|
65
|
+
*/
|
|
66
|
+
_restoreSelection(saved) {
|
|
67
|
+
if (!saved) return;
|
|
68
|
+
let startNode = null, startOff = 0;
|
|
69
|
+
let endNode = null, endOff = 0;
|
|
70
|
+
let count = 0;
|
|
71
|
+
const walker = document.createTreeWalker(this.editable, NodeFilter.SHOW_TEXT, null);
|
|
72
|
+
let cur;
|
|
73
|
+
while ((cur = walker.nextNode())) {
|
|
74
|
+
const len = cur.length;
|
|
75
|
+
if (!startNode && count + len >= saved.start) {
|
|
76
|
+
startNode = cur;
|
|
77
|
+
startOff = saved.start - count;
|
|
78
|
+
}
|
|
79
|
+
if (!endNode && count + len >= saved.end) {
|
|
80
|
+
endNode = cur;
|
|
81
|
+
endOff = saved.end - count;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
count += len;
|
|
85
|
+
}
|
|
86
|
+
if (!startNode) return;
|
|
87
|
+
if (!endNode) { endNode = startNode; endOff = startOff; }
|
|
88
|
+
try {
|
|
89
|
+
const range = document.createRange();
|
|
90
|
+
range.setStart(startNode, startOff);
|
|
91
|
+
range.setEnd(endNode, endOff);
|
|
92
|
+
const sel = window.getSelection();
|
|
93
|
+
sel.removeAllRanges();
|
|
94
|
+
sel.addRange(range);
|
|
95
|
+
} catch (_) { /* detached node — ignore */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_savePoint() {
|
|
99
|
+
// Trim future history if we're mid-stack
|
|
100
|
+
if (this.stackOffset < this.stack.length - 1) {
|
|
101
|
+
this.stack = this.stack.slice(0, this.stackOffset + 1);
|
|
102
|
+
}
|
|
103
|
+
const raw = this._serialize();
|
|
104
|
+
const { html, images } = this._tokenizeImages(raw);
|
|
105
|
+
this.stack.push({ html, images, sel: this._serializeSelection() });
|
|
106
|
+
if (this.stack.length > this._limit) {
|
|
107
|
+
this.stack.shift();
|
|
108
|
+
} else {
|
|
109
|
+
this.stackOffset++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_restore(point) {
|
|
114
|
+
if (!point) return;
|
|
115
|
+
this.editable.innerHTML = this._detokenizeImages(point);
|
|
116
|
+
this._restoreSelection(point.sel);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Base64 tokenisation — keeps snapshot strings small so that the
|
|
121
|
+
// per-keystroke `recordUndo` string comparison stays fast even when the
|
|
122
|
+
// editor contains large embedded images.
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Replaces every `data:…;base64,…` occurrence in `html` with a compact
|
|
127
|
+
* token `__asn_img_0__`, `__asn_img_1__`, … and returns the tokenized
|
|
128
|
+
* string together with a map from token → original data URL.
|
|
129
|
+
* @param {string} html
|
|
130
|
+
* @returns {{ html: string, images: Object<string,string> }}
|
|
131
|
+
*/
|
|
132
|
+
_tokenizeImages(html) {
|
|
133
|
+
const images = {};
|
|
134
|
+
let index = 0;
|
|
135
|
+
const tokenized = html.replace(/data:[^;]+;base64,[^"' >]*/g, (match) => {
|
|
136
|
+
const token = `__asn_img_${index}__`;
|
|
137
|
+
images[token] = match;
|
|
138
|
+
index++;
|
|
139
|
+
return token;
|
|
140
|
+
});
|
|
141
|
+
return { html: tokenized, images };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Restores a snapshot by replacing tokens back with their data URLs.
|
|
146
|
+
* @param {{ html: string, images: Object<string,string> }} point
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
_detokenizeImages(point) {
|
|
150
|
+
if (!point.images || Object.keys(point.images).length === 0) return point.html;
|
|
151
|
+
return point.html.replace(/__asn_img_\d+__/g, (token) => point.images[token] || token);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Public API
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Records the current editor state as a history checkpoint.
|
|
160
|
+
*/
|
|
161
|
+
recordUndo() {
|
|
162
|
+
const current = this._serialize();
|
|
163
|
+
const { html: tokenized } = this._tokenizeImages(current);
|
|
164
|
+
const prev = this.stack[this.stackOffset];
|
|
165
|
+
if (prev && prev.html === tokenized) return; // No change
|
|
166
|
+
this._savePoint();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Undo to the previous state.
|
|
171
|
+
*/
|
|
172
|
+
undo() {
|
|
173
|
+
if (this.stackOffset <= 0) return;
|
|
174
|
+
this.stackOffset--;
|
|
175
|
+
this._restore(this.stack[this.stackOffset]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Redo to the next state.
|
|
180
|
+
*/
|
|
181
|
+
redo() {
|
|
182
|
+
if (this.stackOffset >= this.stack.length - 1) return;
|
|
183
|
+
this.stackOffset++;
|
|
184
|
+
this._restore(this.stack[this.stackOffset]);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resets the history stack (e.g. on editor destroy or full content replace).
|
|
189
|
+
*/
|
|
190
|
+
reset() {
|
|
191
|
+
this.stack = [];
|
|
192
|
+
this.stackOffset = -1;
|
|
193
|
+
this._savePoint();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @returns {boolean} */
|
|
197
|
+
canUndo() {
|
|
198
|
+
return this.stackOffset > 0;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** @returns {boolean} */
|
|
202
|
+
canRedo() {
|
|
203
|
+
return this.stackOffset < this.stack.length - 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style.js - Inline / block style detection and application utilities
|
|
3
|
+
* Rewritten from Summernote's approach using vanilla JS + execCommand fallback
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { closest, isElement, isPara } from '../core/dom.js';
|
|
7
|
+
import { currentRange } from '../core/range.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// execCommand wrappers (still the most compatible way in contenteditable)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies a document execCommand.
|
|
15
|
+
* @param {string} cmd
|
|
16
|
+
* @param {string} [value]
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
export function execCommand(cmd, value = null) {
|
|
20
|
+
return document.execCommand(cmd, false, value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Inline style helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Bolds / unbolds the selection.
|
|
29
|
+
*/
|
|
30
|
+
export const bold = () => execCommand('bold');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Italicises / un-italicises the selection.
|
|
34
|
+
*/
|
|
35
|
+
export const italic = () => execCommand('italic');
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Underlines / un-underlines the selection.
|
|
39
|
+
*/
|
|
40
|
+
export const underline = () => execCommand('underline');
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Strikethrough / removes strikethrough.
|
|
44
|
+
*/
|
|
45
|
+
export const strikethrough = () => execCommand('strikeThrough');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Superscript toggle.
|
|
49
|
+
*/
|
|
50
|
+
export const superscript = () => execCommand('superscript');
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Subscript toggle.
|
|
54
|
+
*/
|
|
55
|
+
export const subscript = () => execCommand('subscript');
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sets the foreground colour of the selected text.
|
|
59
|
+
* @param {string} color - CSS colour string
|
|
60
|
+
*/
|
|
61
|
+
export const foreColor = (color) => execCommand('foreColor', color);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sets the background (highlight) colour of the selected text.
|
|
65
|
+
* @param {string} color - CSS colour string
|
|
66
|
+
*/
|
|
67
|
+
export const backColor = (color) => execCommand('hiliteColor', color);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sets the font name for the selection.
|
|
71
|
+
* @param {string} name
|
|
72
|
+
*/
|
|
73
|
+
export const fontName = (name) => execCommand('fontName', name);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Sets the font size (in pt or with unit) for the selection.
|
|
77
|
+
* Uses a span-based approach to set px sizes precisely.
|
|
78
|
+
* @param {string} size - e.g. '14px'
|
|
79
|
+
* @param {HTMLElement} [editable] - scoping element to avoid touching nodes outside this editor
|
|
80
|
+
*/
|
|
81
|
+
export function fontSize(size, editable = document) {
|
|
82
|
+
execCommand('fontSize', '7'); // placeholder
|
|
83
|
+
// Replace font elements with spans, scoped to the active editable
|
|
84
|
+
editable.querySelectorAll('font[size="7"]').forEach((el) => {
|
|
85
|
+
const span = document.createElement('span');
|
|
86
|
+
span.style.fontSize = size;
|
|
87
|
+
el.parentNode.insertBefore(span, el);
|
|
88
|
+
while (el.firstChild) span.appendChild(el.firstChild);
|
|
89
|
+
el.parentNode.removeChild(el);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Block style helpers
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Wraps the selection in the given block tag (p, h1-h6, blockquote, pre).
|
|
99
|
+
* @param {string} tagName
|
|
100
|
+
*/
|
|
101
|
+
export const formatBlock = (tagName) => execCommand('formatBlock', `<${tagName}>`);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Left-aligns the current block.
|
|
105
|
+
*/
|
|
106
|
+
export const justifyLeft = () => execCommand('justifyLeft');
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Center-aligns the current block.
|
|
110
|
+
*/
|
|
111
|
+
export const justifyCenter = () => execCommand('justifyCenter');
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Right-aligns the current block.
|
|
115
|
+
*/
|
|
116
|
+
export const justifyRight = () => execCommand('justifyRight');
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Fully justifies the current block.
|
|
120
|
+
*/
|
|
121
|
+
export const justifyFull = () => execCommand('justifyFull');
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Indents the list or block.
|
|
125
|
+
*/
|
|
126
|
+
export const indent = () => execCommand('indent');
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Outdents the list or block.
|
|
130
|
+
*/
|
|
131
|
+
export const outdent = () => execCommand('outdent');
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Inserts an unordered list or converts selection.
|
|
135
|
+
*/
|
|
136
|
+
export const insertUnorderedList = () => execCommand('insertUnorderedList');
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Inserts an ordered list or converts selection.
|
|
140
|
+
*/
|
|
141
|
+
export const insertOrderedList = () => execCommand('insertOrderedList');
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// Line-height helper
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Applies a line-height value to every block-level element that intersects
|
|
149
|
+
* the current selection.
|
|
150
|
+
* @param {string} value - unitless multiplier, e.g. '1.5'
|
|
151
|
+
*/
|
|
152
|
+
export function lineHeight(value) {
|
|
153
|
+
const sel = window.getSelection();
|
|
154
|
+
if (!sel || sel.rangeCount === 0) return;
|
|
155
|
+
|
|
156
|
+
const range = sel.getRangeAt(0);
|
|
157
|
+
const BLOCK_TAGS = new Set(['P', 'DIV', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE', 'PRE', 'TD', 'TH']);
|
|
158
|
+
|
|
159
|
+
const nearestBlock = (node) => {
|
|
160
|
+
let el = node instanceof Element ? node : node.parentElement;
|
|
161
|
+
while (el) {
|
|
162
|
+
if (BLOCK_TAGS.has(el.tagName)) return el;
|
|
163
|
+
el = el.parentElement;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (range.collapsed) {
|
|
169
|
+
const block = nearestBlock(range.startContainer);
|
|
170
|
+
if (block) block.style.lineHeight = value;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// For a range selection, collect all unique block ancestors of text nodes
|
|
175
|
+
const blocks = new Set();
|
|
176
|
+
const iter = document.createNodeIterator(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null);
|
|
177
|
+
let textNode;
|
|
178
|
+
while ((textNode = iter.nextNode())) {
|
|
179
|
+
if (range.intersectsNode(textNode)) {
|
|
180
|
+
const block = nearestBlock(textNode);
|
|
181
|
+
if (block) blocks.add(block);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (blocks.size === 0) {
|
|
185
|
+
const block = nearestBlock(range.commonAncestorContainer);
|
|
186
|
+
if (block) blocks.add(block);
|
|
187
|
+
}
|
|
188
|
+
blocks.forEach((block) => { block.style.lineHeight = value; });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Style query helpers
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns the computed styles relevant to the current cursor position.
|
|
197
|
+
* @param {HTMLElement} editable
|
|
198
|
+
* @returns {object} styleMap
|
|
199
|
+
*/
|
|
200
|
+
export function currentStyle(editable) {
|
|
201
|
+
const range = currentRange(editable);
|
|
202
|
+
if (!range) return {};
|
|
203
|
+
|
|
204
|
+
const container = range.isCollapsed()
|
|
205
|
+
? range.sc
|
|
206
|
+
: range.commonAncestor();
|
|
207
|
+
|
|
208
|
+
const el = isElement(container) ? container : container.parentElement;
|
|
209
|
+
if (!el) return {};
|
|
210
|
+
|
|
211
|
+
const computed = window.getComputedStyle(el);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
bold: document.queryCommandState('bold'),
|
|
215
|
+
italic: document.queryCommandState('italic'),
|
|
216
|
+
underline: document.queryCommandState('underline'),
|
|
217
|
+
strikethrough: document.queryCommandState('strikeThrough'),
|
|
218
|
+
superscript: document.queryCommandState('superscript'),
|
|
219
|
+
subscript: document.queryCommandState('subscript'),
|
|
220
|
+
fontSize: computed.fontSize,
|
|
221
|
+
fontFamily: computed.fontFamily,
|
|
222
|
+
color: computed.color,
|
|
223
|
+
backgroundColor: computed.backgroundColor,
|
|
224
|
+
textAlign: computed.textAlign,
|
|
225
|
+
lineHeight: computed.lineHeight,
|
|
226
|
+
formatBlock: (closest(el, isPara, editable) || { nodeName: 'p' }).nodeName.toLowerCase(),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Inline code toggle
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Wraps the selection in an inline <code> element, or unwraps it if the
|
|
236
|
+
* cursor is already inside a <code> that is not inside a <pre>.
|
|
237
|
+
* @param {HTMLElement} [editable]
|
|
238
|
+
*/
|
|
239
|
+
export function toggleInlineCode(editable) {
|
|
240
|
+
const sel = window.getSelection();
|
|
241
|
+
if (!sel || !sel.rangeCount) return;
|
|
242
|
+
const range = sel.getRangeAt(0);
|
|
243
|
+
let container = range.commonAncestorContainer;
|
|
244
|
+
if (container.nodeType === 3) container = container.parentElement;
|
|
245
|
+
const codeEl = container && container.closest ? container.closest('code') : null;
|
|
246
|
+
if (codeEl && !codeEl.closest('pre')) {
|
|
247
|
+
// Unwrap
|
|
248
|
+
const parent = codeEl.parentNode;
|
|
249
|
+
while (codeEl.firstChild) parent.insertBefore(codeEl.firstChild, codeEl);
|
|
250
|
+
parent.removeChild(codeEl);
|
|
251
|
+
if (editable) editable.normalize();
|
|
252
|
+
} else {
|
|
253
|
+
if (range.collapsed) return;
|
|
254
|
+
try {
|
|
255
|
+
const code = document.createElement('code');
|
|
256
|
+
range.surroundContents(code);
|
|
257
|
+
} catch {
|
|
258
|
+
// surroundContents fails across element boundaries — extract and rewrap
|
|
259
|
+
const frag = range.extractContents();
|
|
260
|
+
const code = document.createElement('code');
|
|
261
|
+
code.appendChild(frag);
|
|
262
|
+
range.insertNode(code);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Returns true when the cursor / selection is inside an inline <code>
|
|
269
|
+
* (not nested in a <pre>).
|
|
270
|
+
* @returns {boolean}
|
|
271
|
+
*/
|
|
272
|
+
export function isInlineCode() {
|
|
273
|
+
const sel = window.getSelection();
|
|
274
|
+
if (!sel || !sel.rangeCount) return false;
|
|
275
|
+
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
276
|
+
if (container.nodeType === 3) container = container.parentElement;
|
|
277
|
+
const code = container && container.closest ? container.closest('code') : null;
|
|
278
|
+
return !!(code && !code.closest('pre'));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Checklist (task list)
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Toggles a task-list at the cursor.
|
|
287
|
+
* If inside a checklist <li>, converts it back to a <p>.
|
|
288
|
+
* Otherwise inserts a new <ul class="an-checklist"> with one item.
|
|
289
|
+
*/
|
|
290
|
+
export function toggleChecklist() {
|
|
291
|
+
const sel = window.getSelection();
|
|
292
|
+
if (!sel || !sel.rangeCount) return;
|
|
293
|
+
const range = sel.getRangeAt(0);
|
|
294
|
+
let container = range.commonAncestorContainer;
|
|
295
|
+
if (container.nodeType === 3) container = container.parentElement;
|
|
296
|
+
const li = container && container.closest ? container.closest('.an-checklist li') : null;
|
|
297
|
+
if (li) {
|
|
298
|
+
// Exit checklist — convert item to a <p>
|
|
299
|
+
const ul = li.closest('.an-checklist');
|
|
300
|
+
const text = Array.from(li.childNodes)
|
|
301
|
+
.filter((n) => !(n.nodeType === 1 && n.tagName === 'INPUT'))
|
|
302
|
+
.map((n) => n.textContent).join('').replace(/\u00a0/g, ' ').trim();
|
|
303
|
+
const p = document.createElement('p');
|
|
304
|
+
p.textContent = text || '\u00a0';
|
|
305
|
+
ul.parentNode.insertBefore(p, ul.nextSibling);
|
|
306
|
+
ul.removeChild(li);
|
|
307
|
+
if (ul.children.length === 0) ul.remove();
|
|
308
|
+
const nr = document.createRange();
|
|
309
|
+
nr.setStart(p, 0);
|
|
310
|
+
nr.collapse(true);
|
|
311
|
+
sel.removeAllRanges();
|
|
312
|
+
sel.addRange(nr);
|
|
313
|
+
} else {
|
|
314
|
+
const cb = '<input type="checkbox" contenteditable="false">';
|
|
315
|
+
execCommand('insertHTML', `<ul class="an-checklist"><li>${cb}\u00a0</li></ul>`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Returns true when the cursor is inside a checklist item.
|
|
321
|
+
* @returns {boolean}
|
|
322
|
+
*/
|
|
323
|
+
export function isInChecklist() {
|
|
324
|
+
const sel = window.getSelection();
|
|
325
|
+
if (!sel || !sel.rangeCount) return false;
|
|
326
|
+
let container = sel.getRangeAt(0).commonAncestorContainer;
|
|
327
|
+
if (container.nodeType === 3) container = container.parentElement;
|
|
328
|
+
return !!(container && container.closest && container.closest('.an-checklist li'));
|
|
329
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table.js - Table creation and manipulation utilities
|
|
3
|
+
* Inspired by Summernote's table handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createElement } from '../core/dom.js';
|
|
7
|
+
import { execCommand } from './Style.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Table creation
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a table element with the specified dimensions.
|
|
15
|
+
* @param {number} cols
|
|
16
|
+
* @param {number} rows
|
|
17
|
+
* @param {{ headerRow?: boolean }} [opts]
|
|
18
|
+
* @returns {HTMLTableElement}
|
|
19
|
+
*/
|
|
20
|
+
export function createTable(cols, rows, opts = {}) {
|
|
21
|
+
const { headerRow = false } = opts;
|
|
22
|
+
const table = createElement('table', { class: 'an-table' });
|
|
23
|
+
|
|
24
|
+
if (headerRow && rows > 0) {
|
|
25
|
+
const thead = createElement('thead');
|
|
26
|
+
const tr = createElement('tr');
|
|
27
|
+
for (let c = 0; c < cols; c++) {
|
|
28
|
+
const th = createElement('th', {}, ['\u00a0']);
|
|
29
|
+
tr.appendChild(th);
|
|
30
|
+
}
|
|
31
|
+
thead.appendChild(tr);
|
|
32
|
+
table.appendChild(thead);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const bodyRows = headerRow ? Math.max(rows - 1, 1) : rows;
|
|
36
|
+
const tbody = createElement('tbody');
|
|
37
|
+
table.appendChild(tbody);
|
|
38
|
+
|
|
39
|
+
for (let r = 0; r < bodyRows; r++) {
|
|
40
|
+
const tr = createElement('tr');
|
|
41
|
+
for (let c = 0; c < cols; c++) {
|
|
42
|
+
const td = createElement('td', {}, ['\u00a0']); //
|
|
43
|
+
tr.appendChild(td);
|
|
44
|
+
}
|
|
45
|
+
tbody.appendChild(tr);
|
|
46
|
+
}
|
|
47
|
+
return table;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Inserts a table at the current cursor position.
|
|
52
|
+
* @param {number} cols
|
|
53
|
+
* @param {number} rows
|
|
54
|
+
* @param {{ headerRow?: boolean }} [opts]
|
|
55
|
+
*/
|
|
56
|
+
export function insertTable(cols, rows, opts = {}) {
|
|
57
|
+
const table = createTable(cols, rows, opts);
|
|
58
|
+
execCommand('insertHTML', table.outerHTML);
|
|
59
|
+
}
|