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.
Files changed (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/tulih-editor.css +1 -0
  4. package/dist/tulih-editor.es.js +3051 -0
  5. package/dist/tulih-editor.es.js.map +1 -0
  6. package/dist/tulih-editor.umd.js +8 -0
  7. package/dist/tulih-editor.umd.js.map +1 -0
  8. package/dist/types/core/Editor.d.ts +20 -0
  9. package/dist/types/core/PluginManager.d.ts +22 -0
  10. package/dist/types/core/helpers.d.ts +22 -0
  11. package/dist/types/index.d.ts +5 -0
  12. package/dist/types/plugins/align.d.ts +3 -0
  13. package/dist/types/plugins/autoLinkify.d.ts +3 -0
  14. package/dist/types/plugins/autosave.d.ts +3 -0
  15. package/dist/types/plugins/block.d.ts +3 -0
  16. package/dist/types/plugins/caseTransform.d.ts +3 -0
  17. package/dist/types/plugins/codeBlock.d.ts +3 -0
  18. package/dist/types/plugins/colors.d.ts +3 -0
  19. package/dist/types/plugins/darkMode.d.ts +3 -0
  20. package/dist/types/plugins/direction.d.ts +3 -0
  21. package/dist/types/plugins/dragDrop.d.ts +3 -0
  22. package/dist/types/plugins/emoji.d.ts +3 -0
  23. package/dist/types/plugins/emojiAutocomplete.d.ts +3 -0
  24. package/dist/types/plugins/findReplace.d.ts +3 -0
  25. package/dist/types/plugins/floatingToolbar.d.ts +3 -0
  26. package/dist/types/plugins/fontFamily.d.ts +3 -0
  27. package/dist/types/plugins/fontSize.d.ts +3 -0
  28. package/dist/types/plugins/fullscreen.d.ts +3 -0
  29. package/dist/types/plugins/history.d.ts +3 -0
  30. package/dist/types/plugins/hr.d.ts +3 -0
  31. package/dist/types/plugins/iframe.d.ts +3 -0
  32. package/dist/types/plugins/image.d.ts +3 -0
  33. package/dist/types/plugins/imageProps.d.ts +3 -0
  34. package/dist/types/plugins/imageTools.d.ts +3 -0
  35. package/dist/types/plugins/indent.d.ts +3 -0
  36. package/dist/types/plugins/index.d.ts +2 -0
  37. package/dist/types/plugins/inline.d.ts +3 -0
  38. package/dist/types/plugins/inlineCode.d.ts +3 -0
  39. package/dist/types/plugins/keyboardShortcuts.d.ts +3 -0
  40. package/dist/types/plugins/lineHeight.d.ts +3 -0
  41. package/dist/types/plugins/link.d.ts +3 -0
  42. package/dist/types/plugins/linkTooltip.d.ts +3 -0
  43. package/dist/types/plugins/list.d.ts +3 -0
  44. package/dist/types/plugins/markdown.d.ts +3 -0
  45. package/dist/types/plugins/mediaEmbed.d.ts +3 -0
  46. package/dist/types/plugins/pasteImage.d.ts +3 -0
  47. package/dist/types/plugins/pastePlain.d.ts +3 -0
  48. package/dist/types/plugins/pre.d.ts +3 -0
  49. package/dist/types/plugins/readOnly.d.ts +3 -0
  50. package/dist/types/plugins/shortcutCustomizer.d.ts +3 -0
  51. package/dist/types/plugins/shortcutsHelp.d.ts +3 -0
  52. package/dist/types/plugins/source.d.ts +3 -0
  53. package/dist/types/plugins/specialChars.d.ts +3 -0
  54. package/dist/types/plugins/statusBar.d.ts +3 -0
  55. package/dist/types/plugins/subSuper.d.ts +3 -0
  56. package/dist/types/plugins/table.d.ts +3 -0
  57. package/dist/types/plugins/tableBg.d.ts +3 -0
  58. package/dist/types/plugins/tableTools.d.ts +3 -0
  59. package/dist/types/plugins/toolbarCollapse.d.ts +3 -0
  60. package/dist/types/plugins/unlink.d.ts +3 -0
  61. package/dist/types/plugins/wordCount.d.ts +3 -0
  62. package/dist/types/types.d.ts +226 -0
  63. package/package.json +66 -0
  64. package/src/core/Editor.ts +460 -0
  65. package/src/core/PluginManager.ts +140 -0
  66. package/src/core/helpers.ts +209 -0
  67. package/src/css.d.ts +2 -0
  68. package/src/index.ts +87 -0
  69. package/src/plugins/align.ts +72 -0
  70. package/src/plugins/autoLinkify.ts +34 -0
  71. package/src/plugins/autosave.ts +69 -0
  72. package/src/plugins/block.ts +32 -0
  73. package/src/plugins/caseTransform.ts +54 -0
  74. package/src/plugins/codeBlock.ts +93 -0
  75. package/src/plugins/colors.ts +68 -0
  76. package/src/plugins/darkMode.ts +123 -0
  77. package/src/plugins/direction.ts +30 -0
  78. package/src/plugins/dragDrop.ts +68 -0
  79. package/src/plugins/emoji.ts +188 -0
  80. package/src/plugins/emojiAutocomplete.ts +183 -0
  81. package/src/plugins/findReplace.ts +229 -0
  82. package/src/plugins/floatingToolbar.ts +258 -0
  83. package/src/plugins/fontFamily.ts +41 -0
  84. package/src/plugins/fontSize.ts +32 -0
  85. package/src/plugins/fullscreen.ts +36 -0
  86. package/src/plugins/history.ts +14 -0
  87. package/src/plugins/hr.ts +118 -0
  88. package/src/plugins/iframe.ts +88 -0
  89. package/src/plugins/image.ts +107 -0
  90. package/src/plugins/imageProps.ts +119 -0
  91. package/src/plugins/imageTools.ts +344 -0
  92. package/src/plugins/indent.ts +29 -0
  93. package/src/plugins/index.ts +101 -0
  94. package/src/plugins/inline.ts +17 -0
  95. package/src/plugins/inlineCode.ts +21 -0
  96. package/src/plugins/keyboardShortcuts.ts +92 -0
  97. package/src/plugins/lineHeight.ts +40 -0
  98. package/src/plugins/link.ts +344 -0
  99. package/src/plugins/linkTooltip.ts +63 -0
  100. package/src/plugins/list.ts +141 -0
  101. package/src/plugins/markdown.ts +61 -0
  102. package/src/plugins/mediaEmbed.ts +44 -0
  103. package/src/plugins/pasteImage.ts +61 -0
  104. package/src/plugins/pastePlain.ts +43 -0
  105. package/src/plugins/pre.ts +11 -0
  106. package/src/plugins/readOnly.ts +46 -0
  107. package/src/plugins/shortcutCustomizer.ts +125 -0
  108. package/src/plugins/shortcutsHelp.ts +51 -0
  109. package/src/plugins/source.ts +77 -0
  110. package/src/plugins/specialChars.ts +64 -0
  111. package/src/plugins/statusBar.ts +85 -0
  112. package/src/plugins/subSuper.ts +20 -0
  113. package/src/plugins/table.ts +166 -0
  114. package/src/plugins/tableBg.ts +11 -0
  115. package/src/plugins/tableTools.ts +475 -0
  116. package/src/plugins/toolbarCollapse.ts +14 -0
  117. package/src/plugins/unlink.ts +29 -0
  118. package/src/plugins/wordCount.ts +34 -0
  119. package/src/styles/base.css +258 -0
  120. package/src/styles/editor.css +309 -0
  121. package/src/styles/index.css +6 -0
  122. 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,'&quot;') + '" alt="' + ((file as File).name || '').replace(/"/g,'&quot;') + '" 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
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;