tulih-editor 0.1.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 +331 -0
- package/dist/tulih-editor.css +1 -0
- package/dist/tulih-editor.es.js +3051 -0
- package/dist/tulih-editor.es.js.map +1 -0
- package/dist/tulih-editor.umd.js +8 -0
- package/dist/tulih-editor.umd.js.map +1 -0
- package/dist/types/core/Editor.d.ts +20 -0
- package/dist/types/core/PluginManager.d.ts +22 -0
- package/dist/types/core/helpers.d.ts +22 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/plugins/align.d.ts +3 -0
- package/dist/types/plugins/autoLinkify.d.ts +3 -0
- package/dist/types/plugins/autosave.d.ts +3 -0
- package/dist/types/plugins/block.d.ts +3 -0
- package/dist/types/plugins/caseTransform.d.ts +3 -0
- package/dist/types/plugins/codeBlock.d.ts +3 -0
- package/dist/types/plugins/colors.d.ts +3 -0
- package/dist/types/plugins/darkMode.d.ts +3 -0
- package/dist/types/plugins/direction.d.ts +3 -0
- package/dist/types/plugins/dragDrop.d.ts +3 -0
- package/dist/types/plugins/emoji.d.ts +3 -0
- package/dist/types/plugins/emojiAutocomplete.d.ts +3 -0
- package/dist/types/plugins/findReplace.d.ts +3 -0
- package/dist/types/plugins/floatingToolbar.d.ts +3 -0
- package/dist/types/plugins/fontFamily.d.ts +3 -0
- package/dist/types/plugins/fontSize.d.ts +3 -0
- package/dist/types/plugins/fullscreen.d.ts +3 -0
- package/dist/types/plugins/history.d.ts +3 -0
- package/dist/types/plugins/hr.d.ts +3 -0
- package/dist/types/plugins/iframe.d.ts +3 -0
- package/dist/types/plugins/image.d.ts +3 -0
- package/dist/types/plugins/imageProps.d.ts +3 -0
- package/dist/types/plugins/imageTools.d.ts +3 -0
- package/dist/types/plugins/indent.d.ts +3 -0
- package/dist/types/plugins/index.d.ts +2 -0
- package/dist/types/plugins/inline.d.ts +3 -0
- package/dist/types/plugins/inlineCode.d.ts +3 -0
- package/dist/types/plugins/keyboardShortcuts.d.ts +3 -0
- package/dist/types/plugins/lineHeight.d.ts +3 -0
- package/dist/types/plugins/link.d.ts +3 -0
- package/dist/types/plugins/linkTooltip.d.ts +3 -0
- package/dist/types/plugins/list.d.ts +3 -0
- package/dist/types/plugins/markdown.d.ts +3 -0
- package/dist/types/plugins/mediaEmbed.d.ts +3 -0
- package/dist/types/plugins/pasteImage.d.ts +3 -0
- package/dist/types/plugins/pastePlain.d.ts +3 -0
- package/dist/types/plugins/pre.d.ts +3 -0
- package/dist/types/plugins/readOnly.d.ts +3 -0
- package/dist/types/plugins/shortcutCustomizer.d.ts +3 -0
- package/dist/types/plugins/shortcutsHelp.d.ts +3 -0
- package/dist/types/plugins/source.d.ts +3 -0
- package/dist/types/plugins/specialChars.d.ts +3 -0
- package/dist/types/plugins/statusBar.d.ts +3 -0
- package/dist/types/plugins/subSuper.d.ts +3 -0
- package/dist/types/plugins/table.d.ts +3 -0
- package/dist/types/plugins/tableBg.d.ts +3 -0
- package/dist/types/plugins/tableTools.d.ts +3 -0
- package/dist/types/plugins/toolbarCollapse.d.ts +3 -0
- package/dist/types/plugins/unlink.d.ts +3 -0
- package/dist/types/plugins/wordCount.d.ts +3 -0
- package/dist/types/types.d.ts +226 -0
- package/package.json +66 -0
- package/src/core/Editor.ts +460 -0
- package/src/core/PluginManager.ts +140 -0
- package/src/core/helpers.ts +209 -0
- package/src/css.d.ts +2 -0
- package/src/index.ts +87 -0
- package/src/plugins/align.ts +72 -0
- package/src/plugins/autoLinkify.ts +34 -0
- package/src/plugins/autosave.ts +69 -0
- package/src/plugins/block.ts +32 -0
- package/src/plugins/caseTransform.ts +54 -0
- package/src/plugins/codeBlock.ts +93 -0
- package/src/plugins/colors.ts +68 -0
- package/src/plugins/darkMode.ts +123 -0
- package/src/plugins/direction.ts +30 -0
- package/src/plugins/dragDrop.ts +68 -0
- package/src/plugins/emoji.ts +188 -0
- package/src/plugins/emojiAutocomplete.ts +183 -0
- package/src/plugins/findReplace.ts +229 -0
- package/src/plugins/floatingToolbar.ts +258 -0
- package/src/plugins/fontFamily.ts +41 -0
- package/src/plugins/fontSize.ts +32 -0
- package/src/plugins/fullscreen.ts +36 -0
- package/src/plugins/history.ts +14 -0
- package/src/plugins/hr.ts +118 -0
- package/src/plugins/iframe.ts +88 -0
- package/src/plugins/image.ts +107 -0
- package/src/plugins/imageProps.ts +119 -0
- package/src/plugins/imageTools.ts +344 -0
- package/src/plugins/indent.ts +29 -0
- package/src/plugins/index.ts +101 -0
- package/src/plugins/inline.ts +17 -0
- package/src/plugins/inlineCode.ts +21 -0
- package/src/plugins/keyboardShortcuts.ts +92 -0
- package/src/plugins/lineHeight.ts +40 -0
- package/src/plugins/link.ts +344 -0
- package/src/plugins/linkTooltip.ts +63 -0
- package/src/plugins/list.ts +141 -0
- package/src/plugins/markdown.ts +61 -0
- package/src/plugins/mediaEmbed.ts +44 -0
- package/src/plugins/pasteImage.ts +61 -0
- package/src/plugins/pastePlain.ts +43 -0
- package/src/plugins/pre.ts +11 -0
- package/src/plugins/readOnly.ts +46 -0
- package/src/plugins/shortcutCustomizer.ts +125 -0
- package/src/plugins/shortcutsHelp.ts +51 -0
- package/src/plugins/source.ts +77 -0
- package/src/plugins/specialChars.ts +64 -0
- package/src/plugins/statusBar.ts +85 -0
- package/src/plugins/subSuper.ts +20 -0
- package/src/plugins/table.ts +166 -0
- package/src/plugins/tableBg.ts +11 -0
- package/src/plugins/tableTools.ts +475 -0
- package/src/plugins/toolbarCollapse.ts +14 -0
- package/src/plugins/unlink.ts +29 -0
- package/src/plugins/wordCount.ts +34 -0
- package/src/styles/base.css +258 -0
- package/src/styles/editor.css +309 -0
- package/src/styles/index.css +6 -0
- package/src/types.ts +278 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM utilities used by the editor core and plugins.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const blockSelector = 'p,h1,h2,h3,blockquote,pre,ul,ol,li,table,hr,iframe,.te-embed';
|
|
6
|
+
const inlineTags = ['A', 'B', 'I', 'U', 'S', 'STRONG', 'EM', 'SPAN', 'SMALL', 'MARK', 'CODE', 'KBD', 'SUB', 'SUP'];
|
|
7
|
+
|
|
8
|
+
function qs(selector: string, root?: Document | Element | null): Element | null {
|
|
9
|
+
return (root || document).querySelector(selector);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function qsa(selector: string, root?: Document | Element | null): Element[] {
|
|
13
|
+
return Array.prototype.slice.call((root || document).querySelectorAll(selector));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function exec(command: string, value?: string): void {
|
|
17
|
+
document.execCommand(command, false, value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getSelectionHtml(): string {
|
|
21
|
+
const selection = window.getSelection();
|
|
22
|
+
if (!selection || selection.rangeCount === 0) return '';
|
|
23
|
+
const range = selection.getRangeAt(0).cloneRange();
|
|
24
|
+
const div = document.createElement('div');
|
|
25
|
+
div.appendChild(range.cloneContents());
|
|
26
|
+
return div.innerHTML;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function hasBlockDescendant(el: Element): boolean {
|
|
30
|
+
return !!el.querySelector(blockSelector);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isInlineElement(el: Node): boolean {
|
|
34
|
+
if (el.nodeType !== 1) return false;
|
|
35
|
+
const tag = (el as Element).tagName;
|
|
36
|
+
if (tag === 'IMG' || tag === 'BR' || tag === 'IFRAME') return true;
|
|
37
|
+
return inlineTags.indexOf(tag) !== -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function wrapNodeInP(node: Node): HTMLParagraphElement {
|
|
41
|
+
const p = document.createElement('p');
|
|
42
|
+
node.parentNode!.insertBefore(p, node);
|
|
43
|
+
p.appendChild(node);
|
|
44
|
+
return p;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function wrapIframe(node: Element | null): HTMLDivElement | undefined {
|
|
48
|
+
if (!node || node.tagName !== 'IFRAME') return;
|
|
49
|
+
const wrapper = document.createElement('div');
|
|
50
|
+
wrapper.className = 'te-embed';
|
|
51
|
+
wrapper.setAttribute('contenteditable', 'false');
|
|
52
|
+
node.setAttribute('contenteditable', 'false');
|
|
53
|
+
node.setAttribute('draggable', 'false');
|
|
54
|
+
node.setAttribute('tabindex', '-1');
|
|
55
|
+
node.parentNode!.insertBefore(wrapper, node);
|
|
56
|
+
wrapper.appendChild(node);
|
|
57
|
+
return wrapper;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function normalizeParagraphs(root: Element | null): void {
|
|
61
|
+
if (!root) return;
|
|
62
|
+
let child = root.firstChild;
|
|
63
|
+
while (child) {
|
|
64
|
+
const next = child.nextSibling;
|
|
65
|
+
if (child.nodeType === 3) {
|
|
66
|
+
if ((child.textContent || '').trim() === '') {
|
|
67
|
+
root.removeChild(child);
|
|
68
|
+
} else {
|
|
69
|
+
wrapNodeInP(child);
|
|
70
|
+
}
|
|
71
|
+
} else if (child.nodeType === 1) {
|
|
72
|
+
const el = child as Element;
|
|
73
|
+
const tag = el.tagName;
|
|
74
|
+
if (tag === 'DIV') {
|
|
75
|
+
if (el.classList.contains('te-embed') || el.querySelector('iframe,video,audio,.te-embed')) {
|
|
76
|
+
// keep embed wrappers intact
|
|
77
|
+
} else if (!hasBlockDescendant(el)) {
|
|
78
|
+
const p = document.createElement('p');
|
|
79
|
+
while (el.firstChild) {
|
|
80
|
+
p.appendChild(el.firstChild);
|
|
81
|
+
}
|
|
82
|
+
root.replaceChild(p, el);
|
|
83
|
+
}
|
|
84
|
+
} else if (tag === 'IFRAME') {
|
|
85
|
+
wrapIframe(el);
|
|
86
|
+
} else if (isInlineElement(el)) {
|
|
87
|
+
wrapNodeInP(el);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
child = next;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function ensureInitialParagraph(root: Element | null): void {
|
|
95
|
+
if (!root) return;
|
|
96
|
+
if (root.querySelector(blockSelector)) return;
|
|
97
|
+
if (!root.firstChild) {
|
|
98
|
+
const p0 = document.createElement('p');
|
|
99
|
+
p0.innerHTML = '<br>';
|
|
100
|
+
root.appendChild(p0);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const p = document.createElement('p');
|
|
104
|
+
while (root.firstChild) {
|
|
105
|
+
p.appendChild(root.firstChild);
|
|
106
|
+
}
|
|
107
|
+
root.appendChild(p);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isInsideEditor(editor: Element, node: Node | null): boolean {
|
|
111
|
+
if (!node) return false;
|
|
112
|
+
if (node === editor) return true;
|
|
113
|
+
return editor.contains(node);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getBlockElement(editor: Element): Element | null {
|
|
117
|
+
const sel = window.getSelection();
|
|
118
|
+
if (!sel || sel.rangeCount === 0) return null;
|
|
119
|
+
let node: Node | null = sel.anchorNode;
|
|
120
|
+
if (node && node.nodeType === 3) node = node.parentNode;
|
|
121
|
+
if (!isInsideEditor(editor, node)) return null;
|
|
122
|
+
if (node && (node as Element).closest) {
|
|
123
|
+
return (node as Element).closest('p,h1,h2,h3,blockquote,pre,li,div');
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function setBtnActive(toolbar: Element | null, selector: string, active: boolean): void {
|
|
129
|
+
if (!toolbar) return;
|
|
130
|
+
const btn = toolbar.querySelector(selector);
|
|
131
|
+
if (!btn) return;
|
|
132
|
+
if (active) btn.classList.add('active');
|
|
133
|
+
else btn.classList.remove('active');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function updateToolbarState(
|
|
137
|
+
editor: HTMLElement,
|
|
138
|
+
blockSelect: HTMLSelectElement | null,
|
|
139
|
+
alignSelect: HTMLSelectElement | null,
|
|
140
|
+
toolbar: HTMLElement | null,
|
|
141
|
+
): void {
|
|
142
|
+
const blockEl = getBlockElement(editor);
|
|
143
|
+
if (blockSelect) {
|
|
144
|
+
let tag = '';
|
|
145
|
+
if (blockEl) {
|
|
146
|
+
let t = blockEl.tagName.toLowerCase();
|
|
147
|
+
if (t === 'div' || t === 'li') t = 'p';
|
|
148
|
+
if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre'].indexOf(t) !== -1) {
|
|
149
|
+
tag = t;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
blockSelect.value = tag || 'p';
|
|
153
|
+
}
|
|
154
|
+
if (alignSelect) {
|
|
155
|
+
let align = '';
|
|
156
|
+
if (blockEl) {
|
|
157
|
+
try {
|
|
158
|
+
const cs = window.getComputedStyle(blockEl);
|
|
159
|
+
let ta = cs && cs.textAlign ? cs.textAlign.toLowerCase() : '';
|
|
160
|
+
if (ta === 'start') ta = 'left';
|
|
161
|
+
if (ta === 'left' || ta === 'center' || ta === 'right' || ta === 'justify') {
|
|
162
|
+
align = ta;
|
|
163
|
+
}
|
|
164
|
+
} catch (_) { /* ignore */ }
|
|
165
|
+
}
|
|
166
|
+
alignSelect.value = align || '';
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
setBtnActive(toolbar, '[data-cmd="bold"]', document.queryCommandState('bold'));
|
|
170
|
+
setBtnActive(toolbar, '[data-cmd="italic"]', document.queryCommandState('italic'));
|
|
171
|
+
setBtnActive(toolbar, '[data-cmd="underline"]', document.queryCommandState('underline'));
|
|
172
|
+
setBtnActive(toolbar, '[data-cmd="strikeThrough"]', document.queryCommandState('strikeThrough'));
|
|
173
|
+
} catch (_) { /* ignore */ }
|
|
174
|
+
let inOL = false;
|
|
175
|
+
let inUL = false;
|
|
176
|
+
if (blockEl && blockEl.closest) {
|
|
177
|
+
inOL = !!blockEl.closest('ol');
|
|
178
|
+
inUL = !!blockEl.closest('ul');
|
|
179
|
+
}
|
|
180
|
+
setBtnActive(toolbar, '[data-cmd="insertOrderedList"]', inOL);
|
|
181
|
+
setBtnActive(toolbar, '[data-cmd="insertUnorderedList"]', inUL);
|
|
182
|
+
const isQuote = blockEl && blockEl.tagName === 'BLOCKQUOTE';
|
|
183
|
+
const isPre = blockEl && blockEl.tagName === 'PRE';
|
|
184
|
+
setBtnActive(toolbar, '[data-cmd="formatBlock"][data-val="blockquote"]', !!isQuote);
|
|
185
|
+
setBtnActive(toolbar, '[data-cmd="formatBlock"][data-val="pre"]', !!isPre);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function escapeHtml(str: string): string {
|
|
189
|
+
return String(str)
|
|
190
|
+
.replace(/&/g, '&')
|
|
191
|
+
.replace(/</g, '<')
|
|
192
|
+
.replace(/>/g, '>')
|
|
193
|
+
.replace(/"/g, '"')
|
|
194
|
+
.replace(/'/g, ''');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function removeNode(node: Node | null): void {
|
|
198
|
+
if (node && node.parentNode) {
|
|
199
|
+
node.parentNode.removeChild(node);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
qs, qsa, exec, getSelectionHtml,
|
|
205
|
+
blockSelector, inlineTags, hasBlockDescendant, isInlineElement,
|
|
206
|
+
wrapNodeInP, wrapIframe, normalizeParagraphs, ensureInitialParagraph,
|
|
207
|
+
isInsideEditor, getBlockElement, setBtnActive, updateToolbarState,
|
|
208
|
+
escapeHtml, removeNode,
|
|
209
|
+
};
|
package/src/css.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import './styles/index.css';
|
|
2
|
+
|
|
3
|
+
import PluginManager from './core/PluginManager';
|
|
4
|
+
import Editor from './core/Editor';
|
|
5
|
+
import registerDefaultPlugins from './plugins/index';
|
|
6
|
+
import type {
|
|
7
|
+
EditorOptions,
|
|
8
|
+
EditorInstance,
|
|
9
|
+
BrowseImageHandler,
|
|
10
|
+
TulihEditorStatic,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
const pm = new PluginManager();
|
|
14
|
+
registerDefaultPlugins(pm);
|
|
15
|
+
pm.injectAllCSS();
|
|
16
|
+
|
|
17
|
+
// Auto-load the Tabler Icons webfont (used by toolbar buttons) unless it is
|
|
18
|
+
// already present on the page. Tabler Icons is MIT licensed
|
|
19
|
+
// (https://github.com/tabler/tabler-icons). Pinned to the v3 line to avoid
|
|
20
|
+
// unexpected breaking changes from a future major release.
|
|
21
|
+
(function loadTablerIcons(): void {
|
|
22
|
+
if (typeof document === 'undefined') return;
|
|
23
|
+
if (document.querySelector('link[href*="tabler-icons"]')) return;
|
|
24
|
+
const l = document.createElement('link');
|
|
25
|
+
l.rel = 'stylesheet';
|
|
26
|
+
l.href = 'https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css';
|
|
27
|
+
document.head.appendChild(l);
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
const TulihEditor: TulihEditorStatic = {
|
|
31
|
+
pluginManager: pm,
|
|
32
|
+
instances: [],
|
|
33
|
+
onBrowseImage: null,
|
|
34
|
+
onUploadImage: null,
|
|
35
|
+
|
|
36
|
+
setBrowseImage(handler: BrowseImageHandler): void {
|
|
37
|
+
this.onBrowseImage = handler;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
attach(target: string | Element, options?: EditorOptions): EditorInstance | undefined {
|
|
41
|
+
const el = (typeof target === 'string' ? document.querySelector(target) : target) as HTMLElement | null;
|
|
42
|
+
if (!el) return;
|
|
43
|
+
if (String(el.tagName).toUpperCase() !== 'TEXTAREA' && !(el.matches && el.matches('textarea[data-tulih-editor]'))) return;
|
|
44
|
+
const sibling = el.nextElementSibling;
|
|
45
|
+
if (sibling && sibling.querySelector && sibling.querySelector('.te-editor')) return;
|
|
46
|
+
|
|
47
|
+
return new Editor(el, options, TulihEditor);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
create(target: string | Element, options?: EditorOptions): EditorInstance | undefined {
|
|
51
|
+
return this.attach(target, options);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (typeof document !== 'undefined') {
|
|
56
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
57
|
+
const allTA = document.querySelectorAll('textarea[data-tulih-editor]');
|
|
58
|
+
allTA.forEach((ta) => { TulihEditor.attach(ta); });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Expose the classes on the default object for advanced/UMD consumers without
|
|
63
|
+
// adding runtime *named* exports (those would turn the UMD global into
|
|
64
|
+
// `TulihEditor.default`, breaking the documented `<script>` usage).
|
|
65
|
+
(TulihEditor as TulihEditorStatic & { Editor: typeof Editor; PluginManager: typeof PluginManager }).Editor = Editor;
|
|
66
|
+
(TulihEditor as TulihEditorStatic & { Editor: typeof Editor; PluginManager: typeof PluginManager }).PluginManager = PluginManager;
|
|
67
|
+
|
|
68
|
+
export default TulihEditor;
|
|
69
|
+
export type {
|
|
70
|
+
Plugin,
|
|
71
|
+
PluginContext,
|
|
72
|
+
EditorOptions,
|
|
73
|
+
EditorInstance,
|
|
74
|
+
Features,
|
|
75
|
+
SanitizeOptions,
|
|
76
|
+
ResolvedOptions,
|
|
77
|
+
EditorUtils,
|
|
78
|
+
UploadImageHandler,
|
|
79
|
+
BrowseImageHandler,
|
|
80
|
+
TulihEditorStatic,
|
|
81
|
+
PluginManagerInstance,
|
|
82
|
+
HTMLProvider,
|
|
83
|
+
CSSProvider,
|
|
84
|
+
TeHistory,
|
|
85
|
+
TeEditorElement,
|
|
86
|
+
TeWrapperElement,
|
|
87
|
+
} from './types';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
var ALIGN_CMDS: Record<string, { exec: string; match: string }> = {
|
|
4
|
+
alignLeft: { exec: 'justifyLeft', match: 'left' },
|
|
5
|
+
alignCenter: { exec: 'justifyCenter', match: 'center' },
|
|
6
|
+
alignRight: { exec: 'justifyRight', match: 'right' },
|
|
7
|
+
alignJustify: { exec: 'justifyFull', match: 'justify' }
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getBlockAlignment(node: Node | null): string {
|
|
11
|
+
if(!node || node.nodeType === 3) node = node ? node.parentNode : null;
|
|
12
|
+
if(!node) return '';
|
|
13
|
+
var el = (node as Element).closest ? (node as Element).closest('p,h1,h2,h3,h4,h5,h6,li,div,blockquote') : null;
|
|
14
|
+
if(!el) return '';
|
|
15
|
+
try {
|
|
16
|
+
var cs = window.getComputedStyle(el);
|
|
17
|
+
var ta = cs.textAlign.toLowerCase();
|
|
18
|
+
if(ta === 'start') return 'left';
|
|
19
|
+
return ta;
|
|
20
|
+
} catch(_){ return ''; }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const align: Plugin = {
|
|
24
|
+
name: 'align',
|
|
25
|
+
order: 40,
|
|
26
|
+
css: '',
|
|
27
|
+
toolbarHTML: '' +
|
|
28
|
+
'<button type="button" class="btn btn-sm btn-light te-align-btn" title="Align left" data-cmd="alignLeft"><i class="ti ti-align-left"></i></button>' +
|
|
29
|
+
'<button type="button" class="btn btn-sm btn-light te-align-btn" title="Align center" data-cmd="alignCenter"><i class="ti ti-align-center"></i></button>' +
|
|
30
|
+
'<button type="button" class="btn btn-sm btn-light te-align-btn" title="Align right" data-cmd="alignRight"><i class="ti ti-align-right"></i></button>' +
|
|
31
|
+
'<button type="button" class="btn btn-sm btn-light te-align-btn" title="Justify" data-cmd="alignJustify"><i class="ti ti-align-justified"></i></button>',
|
|
32
|
+
init(ctx: PluginContext): void {
|
|
33
|
+
if(!ctx.features.align) return;
|
|
34
|
+
var names = Object.keys(ALIGN_CMDS);
|
|
35
|
+
|
|
36
|
+
names.forEach(function(name){
|
|
37
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="' + name + '"]') as HTMLElement | null;
|
|
38
|
+
if(!btn) return;
|
|
39
|
+
var cmd = ALIGN_CMDS[name];
|
|
40
|
+
btn.addEventListener('click', function(){
|
|
41
|
+
var align = getBlockAlignment(window.getSelection()!.anchorNode);
|
|
42
|
+
|
|
43
|
+
if(align === cmd.match){
|
|
44
|
+
document.execCommand('justifyLeft', false, undefined);
|
|
45
|
+
} else {
|
|
46
|
+
document.execCommand(cmd.exec, false, undefined);
|
|
47
|
+
}
|
|
48
|
+
ctx.editor.focus();
|
|
49
|
+
updateActive();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function updateActive(): void {
|
|
54
|
+
var node = window.getSelection()!.anchorNode;
|
|
55
|
+
if(!node || !ctx.wrapper.contains(node)) return;
|
|
56
|
+
var align = getBlockAlignment(node);
|
|
57
|
+
names.forEach(function(name){
|
|
58
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="' + name + '"]') as HTMLElement | null;
|
|
59
|
+
if(!btn) return;
|
|
60
|
+
btn.classList.toggle('active', align === ALIGN_CMDS[name].match);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
document.addEventListener('selectionchange', function(){
|
|
65
|
+
if(document.activeElement === ctx.editor || ctx.editor.contains(document.activeElement)){
|
|
66
|
+
updateActive();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default align;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
var URL_RE = /(?:https?:\/\/|www\.)[^\s<>"'()]+(?:\.[^\s<>"'()]+)*/gi;
|
|
4
|
+
|
|
5
|
+
const autoLinkify: Plugin = {
|
|
6
|
+
name: 'autoLinkify',
|
|
7
|
+
order: 1,
|
|
8
|
+
init(ctx: PluginContext): void {
|
|
9
|
+
if (ctx.features.autoLinkify === false) return;
|
|
10
|
+
var editor = ctx.editor;
|
|
11
|
+
|
|
12
|
+
editor.addEventListener('paste', function (e: ClipboardEvent) {
|
|
13
|
+
var text = (e.clipboardData && e.clipboardData.getData('text/plain')) || '';
|
|
14
|
+
if (!text) return;
|
|
15
|
+
|
|
16
|
+
text = text.trim();
|
|
17
|
+
|
|
18
|
+
if (URL_RE.test(text)) {
|
|
19
|
+
URL_RE.lastIndex = 0;
|
|
20
|
+
var url = text.match(URL_RE)![0];
|
|
21
|
+
var fullUrl = url.indexOf('://') === -1 ? 'https://' + url : url;
|
|
22
|
+
var isOk = ctx.utils.isSafeUrl ? ctx.utils.isSafeUrl(fullUrl, ctx.options.urlSchemes || ['http', 'https', 'mailto', 'tel']) : true;
|
|
23
|
+
if (isOk) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
e.stopImmediatePropagation();
|
|
26
|
+
var html = '<p><a href="' + fullUrl.replace(/"/g, '"') + '" target="_blank" rel="noopener noreferrer">' + url.replace(/</g, '<').replace(/>/g, '>') + '</a></p>';
|
|
27
|
+
document.execCommand('insertHTML', false, html);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}, true);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default autoLinkify;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
var STORAGE_PREFIX = 'te-autosave-';
|
|
4
|
+
|
|
5
|
+
function getStorageKey(textarea: HTMLTextAreaElement): string {
|
|
6
|
+
var id = textarea.id || textarea.name || textarea.className || '';
|
|
7
|
+
return STORAGE_PREFIX + (id || Math.random().toString(36).slice(2, 8));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const autosave: Plugin = {
|
|
11
|
+
name: 'autosave',
|
|
12
|
+
order: 210,
|
|
13
|
+
css: '' +
|
|
14
|
+
'.te-autosave-badge {' +
|
|
15
|
+
' font-size: .7rem; padding: 1px 6px; border-radius: 8px;' +
|
|
16
|
+
' line-height: 1.4; opacity: .6; margin-left: auto;' +
|
|
17
|
+
'}' +
|
|
18
|
+
'.te-autosave-badge.saved { background: #d1e7dd; color: #0f5132; }' +
|
|
19
|
+
'.te-autosave-badge.unsaved { background: #fff3cd; color: #664d03; }',
|
|
20
|
+
init(ctx: PluginContext): void {
|
|
21
|
+
if(!ctx.features.autosave) return;
|
|
22
|
+
|
|
23
|
+
var key = getStorageKey(ctx.textarea);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
var saved = localStorage.getItem(key);
|
|
27
|
+
if(saved && saved !== ctx.editor.innerHTML){
|
|
28
|
+
ctx.editor.innerHTML = saved;
|
|
29
|
+
}
|
|
30
|
+
} catch(_){}
|
|
31
|
+
|
|
32
|
+
var badge = document.createElement('span');
|
|
33
|
+
badge.className = 'te-autosave-badge saved';
|
|
34
|
+
badge.textContent = 'Saved';
|
|
35
|
+
var rightEl = ctx.wrapper.querySelector('.te-status-right') as HTMLElement | null;
|
|
36
|
+
if(rightEl){
|
|
37
|
+
rightEl.style.display = 'inline-flex';
|
|
38
|
+
rightEl.style.alignItems = 'center';
|
|
39
|
+
rightEl.style.gap = '8px';
|
|
40
|
+
rightEl.appendChild(badge);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setStatus(saved: boolean): void {
|
|
44
|
+
badge.textContent = saved ? 'Saved' : 'Unsaved';
|
|
45
|
+
badge.className = 'te-autosave-badge ' + (saved ? 'saved' : 'unsaved');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
var saveTimer: ReturnType<typeof setTimeout> | null = null;
|
|
49
|
+
function save(): void {
|
|
50
|
+
setStatus(false);
|
|
51
|
+
if(saveTimer) clearTimeout(saveTimer);
|
|
52
|
+
saveTimer = setTimeout(function(){
|
|
53
|
+
try {
|
|
54
|
+
localStorage.setItem(key, ctx.editor.innerHTML);
|
|
55
|
+
setStatus(true);
|
|
56
|
+
} catch(_){}
|
|
57
|
+
}, 500);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ctx.editor.addEventListener('input', save);
|
|
61
|
+
ctx.editor.addEventListener('paste', save);
|
|
62
|
+
|
|
63
|
+
ctx.editor.addEventListener('blur', function(){
|
|
64
|
+
try { localStorage.removeItem(key); } catch(_){}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default autosave;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const block: Plugin = {
|
|
4
|
+
name: 'block',
|
|
5
|
+
order: 10,
|
|
6
|
+
toolbarHTML: '' +
|
|
7
|
+
'<select class="te-block form-select form-select-sm">' +
|
|
8
|
+
' <option value="p">Paragraph</option>' +
|
|
9
|
+
' <option value="h1">Heading 1</option>' +
|
|
10
|
+
' <option value="h2">Heading 2</option>' +
|
|
11
|
+
' <option value="h3">Heading 3</option>' +
|
|
12
|
+
' <option value="h4">Heading 4</option>' +
|
|
13
|
+
' <option value="h5">Heading 5</option>' +
|
|
14
|
+
' <option value="h6">Heading 6</option>' +
|
|
15
|
+
' <option value="blockquote">Quote</option>' +
|
|
16
|
+
' <option value="pre">Code</option>' +
|
|
17
|
+
'</select>',
|
|
18
|
+
init(ctx: PluginContext): void {
|
|
19
|
+
if (!ctx.features.block) return;
|
|
20
|
+
const sel = ctx.wrapper.querySelector('.te-block') as HTMLSelectElement | null;
|
|
21
|
+
if (!sel) return;
|
|
22
|
+
sel.addEventListener('change', () => {
|
|
23
|
+
const v = sel.value;
|
|
24
|
+
if (!v) return;
|
|
25
|
+
document.execCommand('formatBlock', false, v);
|
|
26
|
+
ctx.editor.focus();
|
|
27
|
+
ctx.updateActiveStates();
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default block;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const caseTransform: Plugin = {
|
|
4
|
+
name: 'caseTransform',
|
|
5
|
+
order: 31,
|
|
6
|
+
toolbarHTML: '' +
|
|
7
|
+
'<select class="te-case form-select form-select-sm" title="Change case">' +
|
|
8
|
+
' <option value="">Case</option>' +
|
|
9
|
+
' <option value="upper">UPPERCASE</option>' +
|
|
10
|
+
' <option value="lower">lowercase</option>' +
|
|
11
|
+
' <option value="title">Title Case</option>' +
|
|
12
|
+
' <option value="sentence">Sentence case</option>' +
|
|
13
|
+
'</select>',
|
|
14
|
+
init(ctx: PluginContext): void {
|
|
15
|
+
if(ctx.features.caseTransform === false) return;
|
|
16
|
+
var sel = ctx.wrapper.querySelector('.te-case') as HTMLSelectElement | null;
|
|
17
|
+
if(!sel) return;
|
|
18
|
+
|
|
19
|
+
sel.addEventListener('mousedown', function(){
|
|
20
|
+
ctx.saveSel();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
sel.addEventListener('change', function(){
|
|
24
|
+
var v = sel!.value;
|
|
25
|
+
sel!.value = '';
|
|
26
|
+
if(!v) return;
|
|
27
|
+
|
|
28
|
+
ctx.restoreSel();
|
|
29
|
+
|
|
30
|
+
var s = window.getSelection();
|
|
31
|
+
if(!s || s.isCollapsed || !s.rangeCount) return;
|
|
32
|
+
var text = s.toString();
|
|
33
|
+
if(!text) return;
|
|
34
|
+
|
|
35
|
+
var transformed: string;
|
|
36
|
+
switch(v){
|
|
37
|
+
case 'upper': transformed = text.toUpperCase(); break;
|
|
38
|
+
case 'lower': transformed = text.toLowerCase(); break;
|
|
39
|
+
case 'title':
|
|
40
|
+
transformed = text.toLowerCase().replace(/\b\w/g, function(m){ return m.toUpperCase(); });
|
|
41
|
+
break;
|
|
42
|
+
case 'sentence':
|
|
43
|
+
transformed = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase().replace(/([.!?]\s+)(\w)/g, function(m){ return m.toUpperCase(); });
|
|
44
|
+
break;
|
|
45
|
+
default: return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
document.execCommand('insertText', false, transformed);
|
|
49
|
+
ctx.editor.focus();
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default caseTransform;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
var LANGUAGES = [
|
|
4
|
+
{ id: 'none', label: 'Plain text' },
|
|
5
|
+
{ id: 'markup', label: 'HTML' },
|
|
6
|
+
{ id: 'css', label: 'CSS' },
|
|
7
|
+
{ id: 'javascript', label: 'JavaScript' },
|
|
8
|
+
{ id: 'php', label: 'PHP' },
|
|
9
|
+
{ id: 'python', label: 'Python' },
|
|
10
|
+
{ id: 'java', label: 'Java' },
|
|
11
|
+
{ id: 'c', label: 'C' },
|
|
12
|
+
{ id: 'cpp', label: 'C++' },
|
|
13
|
+
{ id: 'sql', label: 'SQL' },
|
|
14
|
+
{ id: 'bash', label: 'Bash' },
|
|
15
|
+
{ id: 'json', label: 'JSON' },
|
|
16
|
+
{ id: 'typescript', label: 'TypeScript' },
|
|
17
|
+
{ id: 'go', label: 'Go' },
|
|
18
|
+
{ id: 'rust', label: 'Rust' },
|
|
19
|
+
{ id: 'ruby', label: 'Ruby' }
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const codeBlock: Plugin = {
|
|
23
|
+
name: 'codeBlock',
|
|
24
|
+
order: 23,
|
|
25
|
+
css: '' +
|
|
26
|
+
'.te-codeblock-popup {' +
|
|
27
|
+
' position: absolute; z-index: 3000;' +
|
|
28
|
+
' background: #fff; border: 1px solid #dee2e6;' +
|
|
29
|
+
' border-radius: .375rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
|
|
30
|
+
' padding: .5rem; min-width: 160px;' +
|
|
31
|
+
'}' +
|
|
32
|
+
'.te-codeblock-popup select {' +
|
|
33
|
+
' width: 100%; margin-bottom: .5rem;' +
|
|
34
|
+
'}' +
|
|
35
|
+
'.te-codeblock-popup .btn { width: 100%; }' +
|
|
36
|
+
'.te-editor pre {' +
|
|
37
|
+
' background: #1e1e2e; border: 1px solid #45475a;' +
|
|
38
|
+
' border-left: 4px solid #89b4fa;' +
|
|
39
|
+
' border-radius: .375rem; padding: .75rem;' +
|
|
40
|
+
' overflow-x: auto; margin: .5rem 0;' +
|
|
41
|
+
' font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", Consolas, monospace;' +
|
|
42
|
+
' font-size: .875rem; line-height: 1.6;' +
|
|
43
|
+
' color: #cdd6f4;' +
|
|
44
|
+
'}' +
|
|
45
|
+
'.te-editor pre code {' +
|
|
46
|
+
' background: none; padding: 0; font-size: inherit;' +
|
|
47
|
+
' line-height: inherit; color: inherit;' +
|
|
48
|
+
'}',
|
|
49
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Code block" data-cmd="codeBlock"><i class="ti ti-codeblock"></i></button>',
|
|
50
|
+
init(ctx: PluginContext): void {
|
|
51
|
+
if(ctx.features.codeBlock === false) return;
|
|
52
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="codeBlock"]') as HTMLButtonElement | null;
|
|
53
|
+
if(!btn) return;
|
|
54
|
+
|
|
55
|
+
var popup = document.createElement('div');
|
|
56
|
+
popup.className = 'te-codeblock-popup';
|
|
57
|
+
popup.style.display = 'none';
|
|
58
|
+
popup.innerHTML = '' +
|
|
59
|
+
'<select class="te-codeblock-lang form-select form-select-sm">' +
|
|
60
|
+
LANGUAGES.map(function(l){ return '<option value="' + l.id + '">' + l.label + '</option>'; }).join('') +
|
|
61
|
+
'</select>' +
|
|
62
|
+
'<button type="button" class="btn btn-sm btn-primary te-codeblock-insert">Insert</button>';
|
|
63
|
+
document.body.appendChild(popup);
|
|
64
|
+
|
|
65
|
+
btn.addEventListener('click', function(e: MouseEvent){
|
|
66
|
+
var rect = btn!.getBoundingClientRect();
|
|
67
|
+
popup.style.left = (window.scrollX + rect.left) + 'px';
|
|
68
|
+
popup.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
|
69
|
+
popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
(popup.querySelector('.te-codeblock-insert') as HTMLButtonElement).addEventListener('click', function(){
|
|
73
|
+
var lang = (popup.querySelector('.te-codeblock-lang') as HTMLSelectElement).value;
|
|
74
|
+
var sel = window.getSelection();
|
|
75
|
+
var text = sel ? sel.toString() : '';
|
|
76
|
+
if(!text) text = 'code';
|
|
77
|
+
var cls = lang && lang !== 'none' ? ' class="language-' + lang + '"' : '';
|
|
78
|
+
var html = '<pre><code' + cls + '>' + text.replace(/</g,'<').replace(/>/g,'>') + '</code></pre>';
|
|
79
|
+
document.execCommand('insertHTML', false, html);
|
|
80
|
+
popup.style.display = 'none';
|
|
81
|
+
ctx.editor.focus();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
document.addEventListener('click', function(e: MouseEvent){
|
|
85
|
+
if(popup.style.display === 'none') return;
|
|
86
|
+
if(!popup.contains(e.target as Node) && e.target !== btn){
|
|
87
|
+
popup.style.display = 'none';
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export default codeBlock;
|