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,61 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const pasteImage: Plugin = {
|
|
4
|
+
name: 'pasteImage',
|
|
5
|
+
order: 1,
|
|
6
|
+
init(ctx: PluginContext): void {
|
|
7
|
+
if(ctx.features.pasteImage === false) return;
|
|
8
|
+
var editor = ctx.editor;
|
|
9
|
+
|
|
10
|
+
function getImageFiles(e: ClipboardEvent): File[] {
|
|
11
|
+
var files: File[] = [];
|
|
12
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
13
|
+
if(items){
|
|
14
|
+
for(var i = 0; i < items.length; i++){
|
|
15
|
+
if(items[i].kind === 'file'){
|
|
16
|
+
var f = items[i].getAsFile();
|
|
17
|
+
if(f && /^image\//i.test(f.type)) files.push(f);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
var rawFiles = e.clipboardData && e.clipboardData.files;
|
|
22
|
+
if(rawFiles){
|
|
23
|
+
for(var j = 0; j < rawFiles.length; j++){
|
|
24
|
+
if(/^image\//i.test(rawFiles[j].type)){
|
|
25
|
+
var dup = false;
|
|
26
|
+
for(var k = 0; k < files.length; k++){
|
|
27
|
+
if(files[k] === rawFiles[j] || files[k].name === rawFiles[j].name && files[k].size === rawFiles[j].size && files[k].lastModified === rawFiles[j].lastModified){
|
|
28
|
+
dup = true; break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if(!dup) files.push(rawFiles[j]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
editor.addEventListener('paste', function(e: ClipboardEvent){
|
|
39
|
+
var imageFiles = getImageFiles(e);
|
|
40
|
+
if(imageFiles.length === 0) return;
|
|
41
|
+
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
e.stopImmediatePropagation();
|
|
44
|
+
|
|
45
|
+
imageFiles.forEach(function(file){
|
|
46
|
+
var handler = (ctx.TulihEditor && ctx.TulihEditor.onUploadImage) || function(f: File | Blob, cb: (url: string) => void){
|
|
47
|
+
var r = new FileReader();
|
|
48
|
+
r.onload = function(){ cb(r.result as string); };
|
|
49
|
+
r.readAsDataURL(f);
|
|
50
|
+
};
|
|
51
|
+
handler(file, function(url: string){
|
|
52
|
+
if(!url) return;
|
|
53
|
+
var html = '<img src="' + url.replace(/"/g,'"') + '" alt="' + ((file as File).name || '').replace(/"/g,'"') + '" style="max-width:100%;" />';
|
|
54
|
+
document.execCommand('insertHTML', false, html);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}, true);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default pasteImage;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
import * as H from '../core/helpers';
|
|
3
|
+
|
|
4
|
+
const pastePlain: Plugin = {
|
|
5
|
+
name: 'pastePlain',
|
|
6
|
+
order: 24,
|
|
7
|
+
css: '' +
|
|
8
|
+
'.te-pasteplain-btn.active { background: #e7f1ff; border-color: #b6d4fe; }',
|
|
9
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light te-pasteplain-btn" title="Paste as plain text" data-cmd="togglePastePlain"><i class="ti ti-clipboard"></i></button>',
|
|
10
|
+
init(ctx: PluginContext): void {
|
|
11
|
+
if(!ctx.features.pastePlain) return;
|
|
12
|
+
var editor = ctx.editor;
|
|
13
|
+
ctx._pastePlain = false;
|
|
14
|
+
|
|
15
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="togglePastePlain"]');
|
|
16
|
+
if(btn){
|
|
17
|
+
btn.addEventListener('click', function(){
|
|
18
|
+
ctx._pastePlain = !ctx._pastePlain;
|
|
19
|
+
(btn as HTMLElement).classList.toggle('active', ctx._pastePlain as boolean);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
editor.addEventListener('paste', function(e: ClipboardEvent){
|
|
24
|
+
if(!ctx._pastePlain) return;
|
|
25
|
+
e.stopImmediatePropagation();
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
var text = (e.clipboardData && e.clipboardData.getData('text/plain')) || '';
|
|
28
|
+
if(text){
|
|
29
|
+
var safe = H.escapeHtml(text);
|
|
30
|
+
var html = '<p>' + safe.replace(/\n{2,}/g, '</p><p>').replace(/\n/g, '<br>') + '</p>';
|
|
31
|
+
document.execCommand('insertHTML', false, html);
|
|
32
|
+
}
|
|
33
|
+
setTimeout(function(){
|
|
34
|
+
try {
|
|
35
|
+
H.normalizeParagraphs(editor);
|
|
36
|
+
H.ensureInitialParagraph(editor);
|
|
37
|
+
} catch(_){}
|
|
38
|
+
}, 0);
|
|
39
|
+
}, true);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default pastePlain;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const pre: Plugin = {
|
|
4
|
+
name: 'pre',
|
|
5
|
+
order: 21,
|
|
6
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Preformatted" data-cmd="formatBlock" data-val="pre"><i class="ti ti-code"></i></button>',
|
|
7
|
+
init(ctx: PluginContext): void {
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default pre;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
function applyReadOnly(ctx: PluginContext, ro: boolean): void {
|
|
4
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="toggleReadOnly"]');
|
|
5
|
+
if(ro){
|
|
6
|
+
ctx.editor.contentEditable = 'false';
|
|
7
|
+
ctx.wrapper.classList.add('te-readonly');
|
|
8
|
+
if(btn) btn.innerHTML = '<i class="ti ti-lock-open"></i>';
|
|
9
|
+
} else {
|
|
10
|
+
ctx.editor.contentEditable = 'true';
|
|
11
|
+
ctx.wrapper.classList.remove('te-readonly');
|
|
12
|
+
if(btn) btn.innerHTML = '<i class="ti ti-lock"></i>';
|
|
13
|
+
}
|
|
14
|
+
if(btn) btn.classList.toggle('active', ro);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const readOnly: Plugin = {
|
|
18
|
+
name: 'readOnly',
|
|
19
|
+
order: 75,
|
|
20
|
+
css: '' +
|
|
21
|
+
'.te-readonly-btn.active { background: #fff3cd; border-color: #ffecb5; }' +
|
|
22
|
+
'.te-readonly .te-toolbar select,' +
|
|
23
|
+
'.te-readonly .te-toolbar input,' +
|
|
24
|
+
'.te-readonly .te-toolbar .btn {' +
|
|
25
|
+
' opacity: .5; pointer-events: none;' +
|
|
26
|
+
'}' +
|
|
27
|
+
'.te-readonly .te-editor { background: #f8f9fa; cursor: default; }',
|
|
28
|
+
toolbarHTML: '',
|
|
29
|
+
init(ctx: PluginContext): void {
|
|
30
|
+
if(ctx.features.readOnly === false) return;
|
|
31
|
+
|
|
32
|
+
if(ctx.options.readOnly) {
|
|
33
|
+
applyReadOnly(ctx, true);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="toggleReadOnly"]');
|
|
37
|
+
if(!btn) return;
|
|
38
|
+
|
|
39
|
+
btn.addEventListener('click', function(){
|
|
40
|
+
var isRo = ctx.editor.contentEditable !== 'false';
|
|
41
|
+
applyReadOnly(ctx, !isRo);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default readOnly;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
var STORAGE_KEY = 'te-shortcut-map';
|
|
4
|
+
|
|
5
|
+
var DEFAULT_MAP: Record<string, {key: string; ctrl: boolean; shift: boolean}> = {
|
|
6
|
+
'Bold': {key:'b', ctrl:true, shift:false},
|
|
7
|
+
'Italic': {key:'i', ctrl:true, shift:false},
|
|
8
|
+
'Underline': {key:'u', ctrl:true, shift:false},
|
|
9
|
+
'Strike': {key:'s', ctrl:true, shift:false},
|
|
10
|
+
'Undo': {key:'z', ctrl:true, shift:false},
|
|
11
|
+
'Redo': {key:'z', ctrl:true, shift:true},
|
|
12
|
+
'RedoAlt': {key:'y', ctrl:true, shift:false},
|
|
13
|
+
'Link': {key:'k', ctrl:true, shift:false}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function loadMap(): Record<string, {key: string; ctrl: boolean; shift: boolean}>{
|
|
17
|
+
try {
|
|
18
|
+
var raw = localStorage.getItem(STORAGE_KEY);
|
|
19
|
+
if(raw) return JSON.parse(raw);
|
|
20
|
+
} catch(_){}
|
|
21
|
+
return JSON.parse(JSON.stringify(DEFAULT_MAP));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function saveMap(map: Record<string, {key: string; ctrl: boolean; shift: boolean}>): void{
|
|
25
|
+
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(map)); } catch(_){}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function esc(s: string): string{ return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
29
|
+
|
|
30
|
+
const shortcutCustomizer: Plugin = {
|
|
31
|
+
name: 'shortcutCustomizer',
|
|
32
|
+
order: 6,
|
|
33
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Customize shortcuts" data-cmd="customizeShortcuts"><i class="ti ti-edit"></i></button>',
|
|
34
|
+
modalHTML: '' +
|
|
35
|
+
'<div class="te-modal" data-te-modal="shortcutCustomizer">' +
|
|
36
|
+
' <div class="te-modal-backdrop" data-te-close></div>' +
|
|
37
|
+
' <div class="te-modal-dialog" style="width:420px">' +
|
|
38
|
+
' <div class="te-modal-header"><strong>Customize Shortcuts</strong><button type="button" class="btn-close" data-te-close></button></div>' +
|
|
39
|
+
' <div class="te-modal-body te-shortcut-body"></div>' +
|
|
40
|
+
' <div class="te-modal-footer">' +
|
|
41
|
+
' <button type="button" class="btn btn-sm btn-secondary" data-te-sc-reset>Reset</button>' +
|
|
42
|
+
' <button type="button" class="btn btn-sm btn-secondary" data-te-close>Close</button>' +
|
|
43
|
+
' <button type="button" class="btn btn-sm btn-primary" data-te-shortcut-save>Save</button>' +
|
|
44
|
+
' </div>' +
|
|
45
|
+
' </div>' +
|
|
46
|
+
'</div>',
|
|
47
|
+
css: '' +
|
|
48
|
+
'.te-shortcut-body table { width: 100%; border-collapse: collapse; }' +
|
|
49
|
+
'.te-shortcut-body td, .te-shortcut-body th { padding: 4px 8px; border-bottom: 1px solid #dee2e6; text-align: left; font-size: .85rem; }' +
|
|
50
|
+
'.te-shortcut-body th { font-weight: 600; color: #495057; }' +
|
|
51
|
+
'.te-shortcut-body input[type="text"] { width: 40px; padding: 2px 6px; border: 1px solid #dee2e6; border-radius: 3px; font-size: .85rem; }' +
|
|
52
|
+
'.te-shortcut-body input[type="text"]:focus { outline: none; border-color: #86b7fe; box-shadow: 0 0 0 2px rgba(13,110,253,.25); }' +
|
|
53
|
+
'.te-shortcut-body label { font-size: .8rem; margin-left: 4px; }',
|
|
54
|
+
init(ctx: PluginContext): void {
|
|
55
|
+
if(!ctx.features.shortcutCustomizer) return;
|
|
56
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="customizeShortcuts"]');
|
|
57
|
+
if(!btn) return;
|
|
58
|
+
|
|
59
|
+
btn.addEventListener('click', function(){
|
|
60
|
+
var modal = ctx.wrapper.querySelector('[data-te-modal="shortcutCustomizer"]');
|
|
61
|
+
if(!modal) return;
|
|
62
|
+
var body = modal.querySelector('.te-shortcut-body');
|
|
63
|
+
if(!body) return;
|
|
64
|
+
|
|
65
|
+
var map = loadMap();
|
|
66
|
+
var names = ['Bold','Italic','Underline','Strike','Undo','Redo','RedoAlt','Link'];
|
|
67
|
+
var html = '<table><tr><th>Action</th><th>Shortcut</th></tr>';
|
|
68
|
+
for(var i = 0; i < names.length; i++){
|
|
69
|
+
var n = names[i];
|
|
70
|
+
var s = map[n] || {key:'', ctrl:true, shift:false};
|
|
71
|
+
var label = n === 'RedoAlt' ? 'Redo (alt)' : n;
|
|
72
|
+
html += '<tr><td>' + label + '</td><td>' +
|
|
73
|
+
'Ctrl <input type="checkbox" data-ctrl="' + n + '" ' + (s.ctrl ? 'checked' : '') + ' />' +
|
|
74
|
+
' Shift <input type="checkbox" data-shift="' + n + '" ' + (s.shift ? 'checked' : '') + ' />' +
|
|
75
|
+
' <input type="text" class="te-sc-input" data-action="' + n + '" value="' + esc(s.key) + '" placeholder="key" maxlength="1" />' +
|
|
76
|
+
'</td></tr>';
|
|
77
|
+
}
|
|
78
|
+
html += '</table>';
|
|
79
|
+
body.innerHTML = html;
|
|
80
|
+
modal.classList.add('is-open');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
ctx.wrapper.addEventListener('click', function(e: MouseEvent){
|
|
84
|
+
var saveBtn = (e.target as HTMLElement).closest('[data-te-shortcut-save]');
|
|
85
|
+
if(!saveBtn) return;
|
|
86
|
+
var modal = ctx.wrapper.querySelector('[data-te-modal="shortcutCustomizer"]');
|
|
87
|
+
if(!modal) return;
|
|
88
|
+
var map: Record<string, {key: string; ctrl: boolean; shift: boolean}> = {};
|
|
89
|
+
modal.querySelectorAll('.te-sc-input').forEach(function(inp){
|
|
90
|
+
var action = inp.getAttribute('data-action');
|
|
91
|
+
var key = (inp as HTMLInputElement).value.trim().toLowerCase();
|
|
92
|
+
var ctrl = (modal!.querySelector('[data-ctrl="' + action + '"]') as HTMLInputElement).checked;
|
|
93
|
+
var shift = (modal!.querySelector('[data-shift="' + action + '"]') as HTMLInputElement).checked;
|
|
94
|
+
if(key) map[action!] = {key:key, ctrl:ctrl, shift:shift};
|
|
95
|
+
});
|
|
96
|
+
saveMap(map);
|
|
97
|
+
modal.classList.remove('is-open');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
ctx.wrapper.addEventListener('click', function(e: MouseEvent){
|
|
101
|
+
var resetBtn = (e.target as HTMLElement).closest('[data-te-sc-reset]');
|
|
102
|
+
if(!resetBtn) return;
|
|
103
|
+
var modal = ctx.wrapper.querySelector('[data-te-modal="shortcutCustomizer"]');
|
|
104
|
+
if(!modal) return;
|
|
105
|
+
saveMap(JSON.parse(JSON.stringify(DEFAULT_MAP)));
|
|
106
|
+
var body = modal.querySelector('.te-shortcut-body');
|
|
107
|
+
var names = ['Bold','Italic','Underline','Strike','Undo','Redo','RedoAlt','Link'];
|
|
108
|
+
var html = '<table><tr><th>Action</th><th>Shortcut</th></tr>';
|
|
109
|
+
for(var i = 0; i < names.length; i++){
|
|
110
|
+
var n = names[i];
|
|
111
|
+
var s = DEFAULT_MAP[n];
|
|
112
|
+
var label = n === 'RedoAlt' ? 'Redo (alt)' : n;
|
|
113
|
+
html += '<tr><td>' + label + '</td><td>' +
|
|
114
|
+
'Ctrl <input type="checkbox" data-ctrl="' + n + '" ' + (s.ctrl ? 'checked' : '') + ' />' +
|
|
115
|
+
' Shift <input type="checkbox" data-shift="' + n + '" ' + (s.shift ? 'checked' : '') + ' />' +
|
|
116
|
+
' <input type="text" class="te-sc-input" data-action="' + n + '" value="' + esc(s.key) + '" placeholder="key" maxlength="1" />' +
|
|
117
|
+
'</td></tr>';
|
|
118
|
+
}
|
|
119
|
+
html += '</table>';
|
|
120
|
+
if(body) body.innerHTML = html;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default shortcutCustomizer;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const shortcutsHelp: Plugin = {
|
|
4
|
+
name: 'shortcutsHelp',
|
|
5
|
+
order: 90,
|
|
6
|
+
css: '' +
|
|
7
|
+
'.te-shortcuts-table { width: 100%; border-collapse: collapse; font-size: .875rem; }' +
|
|
8
|
+
'.te-shortcuts-table th, .te-shortcuts-table td { padding: .4rem .75rem; border-bottom: 1px solid #e9ecef; text-align: left; color: #000; }' +
|
|
9
|
+
'.te-shortcuts-table th { background: #f8f9fa; font-weight: 600; }' +
|
|
10
|
+
'.te-shortcuts-table kbd { display: inline-block; padding: 2px 6px; font-size: .75rem; font-family: monospace; background: #f0f0f0; border: 1px solid #d0d0d0; border-radius: 3px; color: #000; }',
|
|
11
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Keyboard shortcuts" data-cmd="shortcutsHelp"><i class="ti ti-help-circle"></i></button>',
|
|
12
|
+
modalHTML: '' +
|
|
13
|
+
'<div class="te-shortcuts-modal te-modal" aria-hidden="true">' +
|
|
14
|
+
' <div class="te-modal-backdrop" data-te-close></div>' +
|
|
15
|
+
' <div class="te-modal-dialog">' +
|
|
16
|
+
' <div class="te-modal-header">' +
|
|
17
|
+
' <h5 class="te-modal-title m-0">Keyboard Shortcuts</h5>' +
|
|
18
|
+
' <button type="button" class="btn-close" data-te-close aria-label="Close"></button>' +
|
|
19
|
+
' </div>' +
|
|
20
|
+
' <div class="te-modal-body">' +
|
|
21
|
+
' <table class="te-shortcuts-table">' +
|
|
22
|
+
' <thead><tr><th>Shortcut</th><th>Action</th></tr></thead>' +
|
|
23
|
+
' <tbody>' +
|
|
24
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>B</kbd></td><td>Bold</td></tr>' +
|
|
25
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>I</kbd></td><td>Italic</td></tr>' +
|
|
26
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>U</kbd></td><td>Underline</td></tr>' +
|
|
27
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>S</kbd></td><td>Strikethrough</td></tr>' +
|
|
28
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>Z</kbd></td><td>Undo</td></tr>' +
|
|
29
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Z</kbd></td><td>Redo</td></tr>' +
|
|
30
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>Y</kbd></td><td>Redo</td></tr>' +
|
|
31
|
+
' <tr><td><kbd>Ctrl</kbd> + <kbd>K</kbd></td><td>Insert link</td></tr>' +
|
|
32
|
+
' <tr><td><kbd>Tab</kbd></td><td>Indent list item</td></tr>' +
|
|
33
|
+
' <tr><td><kbd>Shift</kbd> + <kbd>Tab</kbd></td><td>Outdent list item</td></tr>' +
|
|
34
|
+
' </tbody>' +
|
|
35
|
+
' </table>' +
|
|
36
|
+
' </div>' +
|
|
37
|
+
' </div>' +
|
|
38
|
+
'</div>',
|
|
39
|
+
init(ctx: PluginContext): void {
|
|
40
|
+
if(ctx.features.shortcutsHelp === false) return;
|
|
41
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="shortcutsHelp"]');
|
|
42
|
+
if(btn){
|
|
43
|
+
btn.addEventListener('click', function(){
|
|
44
|
+
var m = ctx.wrapper.querySelector('.te-shortcuts-modal');
|
|
45
|
+
if(m) m.classList.add('is-open');
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default shortcutsHelp;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const source: Plugin = {
|
|
4
|
+
name: 'source',
|
|
5
|
+
order: 65,
|
|
6
|
+
css: '' +
|
|
7
|
+
'.te-source-toggle.active { background: #e7f1ff; border-color: #b6d4fe; }' +
|
|
8
|
+
'.te-editor.te-source-mode { display: none; }' +
|
|
9
|
+
'.te-source-textarea {' +
|
|
10
|
+
' width: 100%; min-height: 200px; border: 1px solid #dee2e6;' +
|
|
11
|
+
' border-radius: .375rem; padding: .75rem; resize: vertical;' +
|
|
12
|
+
' font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;' +
|
|
13
|
+
' font-size: .85rem; line-height: 1.6; display: none;' +
|
|
14
|
+
'}',
|
|
15
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-outline-secondary" title="Toggle source" data-cmd="editSource"><i class="ti ti-source-code"></i></button>',
|
|
16
|
+
init(ctx: PluginContext): void {
|
|
17
|
+
if(!ctx.features.source) return;
|
|
18
|
+
|
|
19
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="editSource"]') as HTMLButtonElement | null;
|
|
20
|
+
if(!btn) return;
|
|
21
|
+
|
|
22
|
+
var ta = document.createElement('textarea');
|
|
23
|
+
ta.className = 'te-source-textarea';
|
|
24
|
+
ta.spellcheck = false;
|
|
25
|
+
ctx.editor.parentNode!.insertBefore(ta, ctx.editor);
|
|
26
|
+
|
|
27
|
+
var sourceMode = false;
|
|
28
|
+
|
|
29
|
+
function formatSource(html: string): string {
|
|
30
|
+
if(!html) return '';
|
|
31
|
+
var out = String(html);
|
|
32
|
+
var blockTags = ['p','h1','h2','h3','h4','h5','h6','blockquote','pre','li','tr','thead','tbody','tfoot','table','ul','ol'];
|
|
33
|
+
blockTags.forEach(function(tag){
|
|
34
|
+
var openRe = new RegExp('<' + tag + '(\\s[^>]*)?>', 'gi');
|
|
35
|
+
var closeRe = new RegExp('</' + tag + '>', 'gi');
|
|
36
|
+
out = out.replace(openRe, function(m){ return '\n' + m; });
|
|
37
|
+
out = out.replace(closeRe, function(m){ return m + '\n'; });
|
|
38
|
+
});
|
|
39
|
+
out = out.replace(/\n{2,}/g, '\n');
|
|
40
|
+
return out.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toggleSource(){
|
|
44
|
+
sourceMode = !sourceMode;
|
|
45
|
+
if(sourceMode){
|
|
46
|
+
ta.value = formatSource(ctx.editor.innerHTML);
|
|
47
|
+
ctx.editor.classList.add('te-source-mode');
|
|
48
|
+
ta.style.display = 'block';
|
|
49
|
+
ta.focus();
|
|
50
|
+
} else {
|
|
51
|
+
var safe = ctx.utils.sanitizeHTML ? ctx.utils.sanitizeHTML(ta.value, ctx.options.sanitize || {}) : ta.value;
|
|
52
|
+
ctx.editor.innerHTML = safe;
|
|
53
|
+
ctx.editor.classList.remove('te-source-mode');
|
|
54
|
+
ta.style.display = 'none';
|
|
55
|
+
ctx.editor.focus();
|
|
56
|
+
}
|
|
57
|
+
btn!.classList.toggle('active', sourceMode);
|
|
58
|
+
var allControls = ctx.toolbar.querySelectorAll('[data-cmd], select, input');
|
|
59
|
+
for(var i = 0; i < allControls.length; i++){
|
|
60
|
+
if(allControls[i] === btn) continue;
|
|
61
|
+
if(sourceMode){
|
|
62
|
+
(allControls[i] as HTMLElement).setAttribute('disabled', 'disabled');
|
|
63
|
+
(allControls[i] as HTMLElement).style.opacity = '.4';
|
|
64
|
+
(allControls[i] as HTMLElement).style.pointerEvents = 'none';
|
|
65
|
+
} else {
|
|
66
|
+
(allControls[i] as HTMLElement).removeAttribute('disabled');
|
|
67
|
+
(allControls[i] as HTMLElement).style.opacity = '';
|
|
68
|
+
(allControls[i] as HTMLElement).style.pointerEvents = '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
btn.addEventListener('click', toggleSource);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default source;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
var CHARS = '©®™§±•·†‡•¶′″←↑→↓↔↕⇒⇔∞≈≠≤≥√∛∜÷×∑∏∫∂∇∅∈∉∋∧∨∩∪⊂⊃⊆⊇¬∧∨⇒⇔∀∃∄∵∴∷∠∟∡∥∦∼≅≜≛≝≟≠≡≢≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋀⋁⋂⋃⋄⋅⋆⋇⋈⋉⋊⋋⋌⋍⋎⋏⋐⋑⋒⋓⋔⋕⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟€£¥₩₽₨₪₫₭₮₰₱₲₳₴₵₶₷₸₹₺₻₼₽₾₿'.split('');
|
|
4
|
+
|
|
5
|
+
const specialChars: Plugin = {
|
|
6
|
+
name: 'specialChars',
|
|
7
|
+
order: 29,
|
|
8
|
+
css: '' +
|
|
9
|
+
'.te-sc-popup {' +
|
|
10
|
+
' position: absolute; z-index: 3000;' +
|
|
11
|
+
' display: grid; grid-template-columns: repeat(10, 1fr); gap: 2px;' +
|
|
12
|
+
' background: #fff; border: 1px solid #dee2e6;' +
|
|
13
|
+
' border-radius: .5rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
|
|
14
|
+
' padding: .5rem; max-width: 320px; max-height: 200px; overflow-y: auto;' +
|
|
15
|
+
'}' +
|
|
16
|
+
'.te-sc-popup button {' +
|
|
17
|
+
' background: none; border: none; font-size: 1rem;' +
|
|
18
|
+
' padding: .25rem; cursor: pointer; border-radius: .25rem; text-align: center;' +
|
|
19
|
+
'}' +
|
|
20
|
+
'.te-sc-popup button:hover { background: #f0f0f0; }',
|
|
21
|
+
toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Special characters" data-cmd="specialChars"><i class="ti ti-hash"></i></button>',
|
|
22
|
+
init(ctx: PluginContext): void {
|
|
23
|
+
if(!ctx.features.specialChars) return;
|
|
24
|
+
|
|
25
|
+
var popup = document.createElement('div');
|
|
26
|
+
popup.className = 'te-sc-popup';
|
|
27
|
+
popup.style.display = 'none';
|
|
28
|
+
CHARS.forEach(function(ch){
|
|
29
|
+
var btn = document.createElement('button');
|
|
30
|
+
btn.type = 'button';
|
|
31
|
+
btn.textContent = ch;
|
|
32
|
+
btn.setAttribute('data-sc', ch);
|
|
33
|
+
popup.appendChild(btn);
|
|
34
|
+
});
|
|
35
|
+
document.body.appendChild(popup);
|
|
36
|
+
|
|
37
|
+
var btn = ctx.wrapper.querySelector('[data-cmd="specialChars"]') as HTMLElement | null;
|
|
38
|
+
if(btn){
|
|
39
|
+
btn.addEventListener('click', function(e: MouseEvent){
|
|
40
|
+
var rect = (btn as HTMLElement).getBoundingClientRect();
|
|
41
|
+
popup.style.left = (window.scrollX + rect.left) + 'px';
|
|
42
|
+
popup.style.top = (window.scrollY + rect.bottom + 4) + 'px';
|
|
43
|
+
popup.style.display = popup.style.display === 'none' ? 'grid' : 'none';
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
popup.addEventListener('click', function(e: MouseEvent){
|
|
48
|
+
var ch = (e.target as HTMLElement) && (e.target as HTMLElement).getAttribute('data-sc');
|
|
49
|
+
if(!ch) return;
|
|
50
|
+
document.execCommand('insertHTML', false, ch);
|
|
51
|
+
popup.style.display = 'none';
|
|
52
|
+
ctx.editor.focus();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
document.addEventListener('click', function(e: MouseEvent){
|
|
56
|
+
if(popup.style.display === 'none') return;
|
|
57
|
+
if(!popup.contains(e.target as Node) && e.target !== btn){
|
|
58
|
+
popup.style.display = 'none';
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default specialChars;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const statusBar: Plugin = {
|
|
4
|
+
name: 'statusBar',
|
|
5
|
+
order: 100,
|
|
6
|
+
css: '' +
|
|
7
|
+
'.te-statusbar {' +
|
|
8
|
+
' display: flex; justify-content: space-between; gap: 1rem;' +
|
|
9
|
+
' padding: 4px 8px; font-size: .75rem; color: #6c757d;' +
|
|
10
|
+
' background: #f8f9fa; border: 1px solid #dee2e6;' +
|
|
11
|
+
' border-top: none; border-radius: 0 0 .375rem .375rem;' +
|
|
12
|
+
'}' +
|
|
13
|
+
'.te-statusbar .te-status-tag {' +
|
|
14
|
+
' font-weight: 600;' +
|
|
15
|
+
' font-family: monospace;' +
|
|
16
|
+
'}',
|
|
17
|
+
init(ctx: PluginContext): void {
|
|
18
|
+
if(ctx.features.statusBar === false) return;
|
|
19
|
+
var editor = ctx.editor;
|
|
20
|
+
|
|
21
|
+
var bar = document.createElement('div');
|
|
22
|
+
bar.className = 'te-statusbar';
|
|
23
|
+
bar.innerHTML = '<span class="te-status-tag"></span><span class="te-status-right"><span class="te-status-words">0 words</span><span class="te-status-chars">0 characters</span></span>';
|
|
24
|
+
editor.parentNode!.insertBefore(bar, editor.nextSibling);
|
|
25
|
+
|
|
26
|
+
function getActiveTag(): string {
|
|
27
|
+
var sel = window.getSelection();
|
|
28
|
+
if(!sel || sel.rangeCount === 0) return '';
|
|
29
|
+
var n: Node | null = sel.anchorNode;
|
|
30
|
+
if(n && n.nodeType === 3) n = n.parentNode;
|
|
31
|
+
if(!n || !editor.contains(n)) return '';
|
|
32
|
+
var tag = '';
|
|
33
|
+
var el: HTMLElement | null = n as HTMLElement;
|
|
34
|
+
while(el && el !== editor){
|
|
35
|
+
var t = el.tagName ? el.tagName.toLowerCase() : '';
|
|
36
|
+
if(['p','h1','h2','h3','h4','h5','h6','div','blockquote','pre','li','td','th','ol','ul','table','thead','tbody','tfoot','tr'].indexOf(t) !== -1){
|
|
37
|
+
tag = t;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
el = el.parentElement;
|
|
41
|
+
}
|
|
42
|
+
return tag;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function update(): void {
|
|
46
|
+
var sel = window.getSelection();
|
|
47
|
+
var hasSel = sel && !sel.isCollapsed && editor.contains(sel.anchorNode) && editor.contains(sel.focusNode);
|
|
48
|
+
var text = editor.textContent || '';
|
|
49
|
+
var words = text.trim() ? text.trim().split(/\s+/).length : 0;
|
|
50
|
+
var chars = text.length;
|
|
51
|
+
var tagEl = bar.querySelector('.te-status-tag') as HTMLElement | null;
|
|
52
|
+
var rightEl = bar.querySelector('.te-status-right') as HTMLElement | null;
|
|
53
|
+
if(tagEl) tagEl.textContent = getActiveTag();
|
|
54
|
+
if(rightEl){
|
|
55
|
+
var wEl = rightEl.querySelector('.te-status-words') as HTMLElement | null;
|
|
56
|
+
var cEl = rightEl.querySelector('.te-status-chars') as HTMLElement | null;
|
|
57
|
+
if(!wEl || !cEl){
|
|
58
|
+
rightEl.innerHTML = '<span class="te-status-words"></span><span class="te-status-chars"></span>';
|
|
59
|
+
wEl = rightEl.querySelector('.te-status-words') as HTMLElement | null;
|
|
60
|
+
cEl = rightEl.querySelector('.te-status-chars') as HTMLElement | null;
|
|
61
|
+
}
|
|
62
|
+
if(wEl) wEl.textContent = words + ' word' + (words !== 1 ? 's' : '');
|
|
63
|
+
if(cEl) cEl.textContent = chars + ' character' + (chars !== 1 ? 's' : '');
|
|
64
|
+
if(hasSel){
|
|
65
|
+
var selText = sel!.toString();
|
|
66
|
+
var selWords = selText.trim() ? selText.trim().split(/\s+/).length : 0;
|
|
67
|
+
var txt = selWords + ' word' + (selWords !== 1 ? 's' : '') + ', ' + selText.length + ' char selected | ' + words + ' word' + (words !== 1 ? 's' : '') + ', ' + chars + ' characters';
|
|
68
|
+
if(wEl) wEl.style.display = 'none';
|
|
69
|
+
if(cEl) cEl.textContent = txt;
|
|
70
|
+
} else {
|
|
71
|
+
if(wEl) wEl.style.display = '';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
editor.addEventListener('input', update);
|
|
77
|
+
editor.addEventListener('paste', function(){ setTimeout(update, 100); });
|
|
78
|
+
document.addEventListener('selectionchange', function(){
|
|
79
|
+
if(document.activeElement === editor || editor.contains(document.activeElement)) update();
|
|
80
|
+
});
|
|
81
|
+
update();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default statusBar;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Plugin, PluginContext } from '../types';
|
|
2
|
+
|
|
3
|
+
const subSuper: Plugin = {
|
|
4
|
+
name: 'subSuper',
|
|
5
|
+
order: 26,
|
|
6
|
+
toolbarHTML: '' +
|
|
7
|
+
'<button type="button" class="btn btn-sm btn-light" title="Subscript" data-cmd="subscript">x₂</button>' +
|
|
8
|
+
'<button type="button" class="btn btn-sm btn-light" title="Superscript" data-cmd="superscript">x²</button>',
|
|
9
|
+
init(ctx: PluginContext): void {
|
|
10
|
+
if (!ctx.features.subSuper) return;
|
|
11
|
+
ctx.wrapper.querySelectorAll('[data-cmd="subscript"],[data-cmd="superscript"]').forEach(function (btn) {
|
|
12
|
+
btn.addEventListener('click', function () {
|
|
13
|
+
document.execCommand(btn.getAttribute('data-cmd')!, false, undefined);
|
|
14
|
+
ctx.editor.focus();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default subSuper;
|