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,118 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+
3
+ const hr: Plugin = {
4
+ name: 'hr',
5
+ order: 55,
6
+ toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Horizontal rule" data-cmd="insertHR"><i class="ti ti-minus"></i></button>',
7
+ css: '' +
8
+ '.te-hr-popup {' +
9
+ ' position: fixed; display: none; z-index: 3000;' +
10
+ ' background: #fff; border: 1px solid #dee2e6;' +
11
+ ' border-radius: .375rem; box-shadow: 0 6px 20px rgba(0,0,0,.15);' +
12
+ ' padding: .5rem; white-space: nowrap;' +
13
+ '}' +
14
+ '.te-hr-popup .btn {' +
15
+ ' padding: .15rem .4rem !important; font-size: 1rem !important; line-height: 1 !important;' +
16
+ ' margin-right: 2px;' +
17
+ '}' +
18
+ '.te-hr-popup .btn.active { background: #e7f1ff; border-color: #b6d4fe; }' +
19
+ '.te-hr-popup input[type="color"] {' +
20
+ ' width: 26px; height: 22px; padding: 1px; border: 1px solid #dee2e6;' +
21
+ ' border-radius: 3px; cursor: pointer; vertical-align: middle;' +
22
+ '}',
23
+ init(ctx: PluginContext): void {
24
+ if(!ctx.features.hr) return;
25
+ var btn = ctx.wrapper.querySelector('[data-cmd="insertHR"]');
26
+ if(!btn) return;
27
+
28
+ var popup = document.createElement('div');
29
+ popup.className = 'te-hr-popup';
30
+ popup.innerHTML = '' +
31
+ '<button type="button" class="btn btn-sm btn-light active" data-te-hr="solid" title="Solid">━</button>' +
32
+ '<button type="button" class="btn btn-sm btn-light" data-te-hr="dashed" title="Dashed">┅</button>' +
33
+ '<button type="button" class="btn btn-sm btn-light" data-te-hr="dotted" title="Dotted">┈</button>' +
34
+ '<input type="color" class="te-hr-color" value="#cccccc" title="HR color" />';
35
+ document.body.appendChild(popup);
36
+
37
+ var currentStyle = 'solid';
38
+ var currentColor = '#cccccc';
39
+ var savedRange: Range | null = null;
40
+
41
+ function saveEditorRange(): void{
42
+ var sel = window.getSelection();
43
+ if(sel && sel.rangeCount > 0) savedRange = sel.getRangeAt(0).cloneRange();
44
+ else savedRange = null;
45
+ }
46
+
47
+ function restoreEditorRange(): void{
48
+ if(!savedRange) return;
49
+ var sel = window.getSelection();
50
+ sel!.removeAllRanges();
51
+ sel!.addRange(savedRange);
52
+ }
53
+
54
+ function posPopup(): void{
55
+ var br = (btn as HTMLElement).getBoundingClientRect();
56
+ var pw = popup.offsetWidth || 200;
57
+ var left = br.left + (br.width / 2) - (pw / 2);
58
+ if(left < 4) left = 4;
59
+ if(left + pw > window.innerWidth - 4) left = window.innerWidth - pw - 4;
60
+ popup.style.left = left + 'px';
61
+ popup.style.top = (br.bottom + 4) + 'px';
62
+ }
63
+
64
+ function insertHR(): void{
65
+ ctx.editor.focus();
66
+ restoreEditorRange();
67
+ var style = 'border: none; border-top: 2px ' + currentStyle + ' ' + currentColor + '; margin: 1em 0;';
68
+ document.execCommand('insertHTML', false, '<hr style="' + style + '">');
69
+ hidePopup();
70
+ }
71
+
72
+ function hidePopup(): void{ popup.style.display = 'none'; }
73
+ function showPopup(): void{
74
+ saveEditorRange();
75
+ popup.style.display = 'block';
76
+ posPopup();
77
+ }
78
+
79
+ btn.addEventListener('click', function(e: Event){
80
+ (e as MouseEvent).preventDefault();
81
+ (e as MouseEvent).stopPropagation();
82
+ if(popup.style.display === 'block'){ hidePopup(); return; }
83
+ showPopup();
84
+ });
85
+
86
+ popup.addEventListener('mousedown', function(e: MouseEvent){
87
+ var opt = (e.target as HTMLElement).closest('[data-te-hr]');
88
+ if(opt){
89
+ e.preventDefault();
90
+ currentStyle = opt.getAttribute('data-te-hr')!;
91
+ popup.querySelectorAll('[data-te-hr]').forEach(function(b){ b.classList.remove('active'); });
92
+ opt.classList.add('active');
93
+ insertHR();
94
+ return;
95
+ }
96
+ });
97
+
98
+ popup.addEventListener('change', function(e: Event){
99
+ var inp = (e.target as HTMLElement).closest('.te-hr-color');
100
+ if(inp){
101
+ currentColor = (inp as HTMLInputElement).value;
102
+ insertHR();
103
+ }
104
+ });
105
+
106
+ document.addEventListener('click', function(e: MouseEvent){
107
+ if(popup.style.display !== 'block') return;
108
+ if(!popup.contains(e.target as Node) && e.target !== btn && !(btn as HTMLElement).contains(e.target as Node)){
109
+ hidePopup();
110
+ }
111
+ });
112
+
113
+ window.addEventListener('scroll', function(){ if(popup.style.display === 'block') posPopup(); });
114
+ window.addEventListener('resize', function(){ if(popup.style.display === 'block') posPopup(); });
115
+ }
116
+ };
117
+
118
+ export default hr;
@@ -0,0 +1,88 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+ import * as H from '../core/helpers';
3
+
4
+ const iframe: Plugin = {
5
+ name: 'iframe',
6
+ order: 53,
7
+ toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Iframe" data-cmd="insertIframe"><i class="ti ti-video"></i></button>',
8
+ modalHTML: '' +
9
+ '<div class="te-iframe-modal te-modal" aria-hidden="true">' +
10
+ ' <div class="te-modal-backdrop" data-te-close></div>' +
11
+ ' <div class="te-modal-dialog">' +
12
+ ' <div class="te-modal-header">' +
13
+ ' <h5 class="te-modal-title m-0">Insert Iframe</h5>' +
14
+ ' <button type="button" class="btn-close" data-te-close aria-label="Close"></button>' +
15
+ ' </div>' +
16
+ ' <form class="te-iframe-form">' +
17
+ ' <div class="te-modal-body">' +
18
+ ' <div class="mb-3">' +
19
+ ' <label class="form-label">Src URL</label>' +
20
+ ' <input type="url" class="te-iframe-src form-control" placeholder="https://..." required>' +
21
+ ' </div>' +
22
+ ' <div class="row g-2 mb-3">' +
23
+ ' <div class="col">' +
24
+ ' <label class="form-label">Width (px)</label>' +
25
+ ' <input type="number" min="0" class="te-iframe-width form-control" placeholder="640">' +
26
+ ' </div>' +
27
+ ' <div class="col">' +
28
+ ' <label class="form-label">Height (px)</label>' +
29
+ ' <input type="number" min="0" class="te-iframe-height form-control" placeholder="360">' +
30
+ ' </div>' +
31
+ ' </div>' +
32
+ ' <div class="mb-3">' +
33
+ ' <label class="form-label">Allow (optional)</label>' +
34
+ ' <input type="text" class="te-iframe-allow form-control" placeholder="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share">' +
35
+ ' </div>' +
36
+ ' <div class="form-check mb-2">' +
37
+ ' <input class="te-iframe-allowfullscreen form-check-input" type="checkbox" checked>' +
38
+ ' <label class="form-check-label">Allow Fullscreen</label>' +
39
+ ' </div>' +
40
+ ' <div class="mb-2">' +
41
+ ' <label class="form-label">Title (accessibility, optional)</label>' +
42
+ ' <input type="text" class="te-iframe-title form-control" placeholder="Embedded content description">' +
43
+ ' </div>' +
44
+ ' </div>' +
45
+ ' <div class="te-modal-footer">' +
46
+ ' <button type="button" class="btn btn-outline-secondary" data-te-close>Cancel</button>' +
47
+ ' <button type="submit" class="btn btn-primary">Insert</button>' +
48
+ ' </div>' +
49
+ ' </form>' +
50
+ ' </div>' +
51
+ '</div>',
52
+ init(ctx: PluginContext): void {
53
+ if(!ctx.features.iframe) return;
54
+ var btn = ctx.wrapper.querySelector('[data-cmd="insertIframe"]') as HTMLButtonElement | null;
55
+ if(btn){
56
+ btn.addEventListener('click', function(){
57
+ ctx.saveSel();
58
+ var m = ctx.wrapper.querySelector('.te-iframe-modal') as HTMLElement | null;
59
+ if(m) m.classList.add('is-open');
60
+ });
61
+ }
62
+ var form = ctx.wrapper.querySelector('.te-iframe-form') as HTMLFormElement | null;
63
+ if(form){
64
+ form.addEventListener('submit', function(e: Event){
65
+ e.preventDefault();
66
+ ctx.restoreSel();
67
+ var src = ((ctx.wrapper.querySelector('.te-iframe-src') as HTMLInputElement).value || '').trim();
68
+ var w = ((ctx.wrapper.querySelector('.te-iframe-width') as HTMLInputElement).value || '').trim();
69
+ var h = ((ctx.wrapper.querySelector('.te-iframe-height') as HTMLInputElement).value || '').trim();
70
+ if(!src) return;
71
+ var allowOk = ctx.utils.isAllowedIframeUrl ? ctx.utils.isAllowedIframeUrl(src) : true;
72
+ if(!allowOk) return;
73
+ var attrs = ' src="' + src.replace(/"/g,'&quot;') + '"';
74
+ if(w) attrs += ' width="' + w.replace(/"/g,'&quot;') + '"';
75
+ if(h) attrs += ' height="' + h.replace(/"/g,'&quot;') + '"';
76
+ var sandbox = (ctx.options && ctx.options.iframeSandbox) || '';
77
+ var allow = (ctx.options && ctx.options.iframeAllow) || '';
78
+ if(sandbox) attrs += ' sandbox="' + sandbox.replace(/"/g,'&quot;') + '"';
79
+ if(allow) attrs += ' allow="' + allow.replace(/"/g,'&quot;') + '"';
80
+ var html = '<iframe' + attrs + ' style="max-width:100%;border:0;"></iframe>';
81
+ document.execCommand('insertHTML', false, html);
82
+ (form!.closest('.te-modal') as HTMLElement).classList.remove('is-open');
83
+ });
84
+ }
85
+ }
86
+ };
87
+
88
+ export default iframe;
@@ -0,0 +1,107 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+
3
+ const image: Plugin = {
4
+ name: 'image',
5
+ order: 51,
6
+ toolbarHTML: '<button type="button" class="btn btn-sm btn-light" title="Image" data-cmd="insertImage"><i class="ti ti-photo"></i></button>',
7
+ modalHTML: '' +
8
+ '<div class="te-image-modal te-modal" aria-hidden="true">' +
9
+ ' <div class="te-modal-backdrop" data-te-close></div>' +
10
+ ' <div class="te-modal-dialog">' +
11
+ ' <div class="te-modal-header">' +
12
+ ' <h5 class="te-modal-title m-0">Insert Image</h5>' +
13
+ ' <button type="button" class="btn-close" data-te-close aria-label="Close"></button>' +
14
+ ' </div>' +
15
+ ' <form class="te-image-form">' +
16
+ ' <div class="te-modal-body">' +
17
+ ' <div class="mb-3">' +
18
+ ' <label class="form-label">Image URL</label>' +
19
+ ' <input type="text" class="te-image-url form-control" placeholder="/path/to/image.jpg or https://..." required>' +
20
+ ' <div class="mt-2"><button type="button" class="te-image-browse btn btn-sm btn-outline-secondary"><i class="ti ti-folder"></i> Browse...</button></div>' +
21
+ ' </div>' +
22
+ ' <div class="mb-3">' +
23
+ ' <label class="form-label">Alt text (optional)</label>' +
24
+ ' <input type="text" class="te-image-alt form-control" placeholder="Description">' +
25
+ ' </div>' +
26
+ ' </div>' +
27
+ ' <div class="te-modal-footer">' +
28
+ ' <button type="button" class="btn btn-outline-secondary" data-te-close>Cancel</button>' +
29
+ ' <button type="submit" class="btn btn-primary">Insert</button>' +
30
+ ' </div>' +
31
+ ' </form>' +
32
+ ' </div>' +
33
+ '</div>',
34
+ init(ctx: PluginContext): void {
35
+ if(!ctx.features.image) return;
36
+ var btn = ctx.wrapper.querySelector('[data-cmd="insertImage"]');
37
+ if(btn){
38
+ btn.addEventListener('click', function(){
39
+ ctx.saveSel();
40
+ var m = ctx.wrapper.querySelector('.te-image-modal');
41
+ if(m) m.classList.add('is-open');
42
+ });
43
+ }
44
+ var inputUrl = ctx.wrapper.querySelector('.te-image-url') as HTMLInputElement | null;
45
+
46
+ var browseBtn = ctx.wrapper.querySelector('.te-image-browse');
47
+ if(browseBtn){
48
+ browseBtn.addEventListener('click', function(e: Event){
49
+ e.preventDefault();
50
+ if(ctx.TulihEditor && typeof ctx.TulihEditor.onBrowseImage === 'function'){
51
+ ctx.TulihEditor.onBrowseImage({ container: ctx.wrapper, editor: ctx.editor }, function(url: string){
52
+ if(url && inputUrl){ inputUrl.value = url; inputUrl.focus(); }
53
+ });
54
+ return;
55
+ }
56
+ var fi = document.createElement('input');
57
+ fi.type = 'file';
58
+ fi.accept = 'image/*';
59
+ fi.style.display = 'none';
60
+ document.body.appendChild(fi);
61
+ fi.addEventListener('change', function(){
62
+ var file = fi.files && fi.files[0];
63
+ if(!file){ document.body.removeChild(fi); return; }
64
+ var r = new FileReader();
65
+ r.onload = function(){
66
+ if(inputUrl){
67
+ inputUrl.value = r.result as string;
68
+ inputUrl.focus();
69
+ }
70
+ document.body.removeChild(fi);
71
+ };
72
+ r.readAsDataURL(file);
73
+ });
74
+ fi.click();
75
+ });
76
+ }
77
+ var form = ctx.wrapper.querySelector('.te-image-form') as HTMLFormElement | null;
78
+ if(form){
79
+ form.addEventListener('submit', function(e: Event){
80
+ e.preventDefault();
81
+ var src = ((ctx.wrapper.querySelector('.te-image-url') as HTMLInputElement).value || '').trim();
82
+ var alt = ((ctx.wrapper.querySelector('.te-image-alt') as HTMLInputElement).value || '').trim();
83
+ if(!src) return;
84
+ var ok = ctx.utils.isSafeUrl ? ctx.utils.isSafeUrl(src, (ctx.options as any).imageSchemes || ['http','https','data']) : true;
85
+ if(!ok) return;
86
+ ctx.restoreSel();
87
+ ctx.editor.focus();
88
+ var img = document.createElement('img');
89
+ img.src = src;
90
+ if(alt) img.alt = alt;
91
+ var sel = window.getSelection();
92
+ if(sel && sel.rangeCount > 0){
93
+ var range = sel.getRangeAt(0);
94
+ range.deleteContents();
95
+ range.insertNode(img);
96
+ range.setStartAfter(img);
97
+ range.collapse(true);
98
+ sel.removeAllRanges();
99
+ sel.addRange(range);
100
+ }
101
+ form!.closest('.te-modal')!.classList.remove('is-open');
102
+ });
103
+ }
104
+ }
105
+ };
106
+
107
+ export default image;
@@ -0,0 +1,119 @@
1
+ import type { Plugin, PluginContext } from '../types';
2
+
3
+ const imageProps: Plugin = {
4
+ name: 'imageProps',
5
+ order: 52,
6
+ deps: ['image'],
7
+ modalHTML: '' +
8
+ '<div class="te-image-props-modal te-modal" aria-hidden="true">' +
9
+ ' <div class="te-modal-backdrop" data-te-close></div>' +
10
+ ' <div class="te-modal-dialog">' +
11
+ ' <div class="te-modal-header">' +
12
+ ' <h5 class="te-modal-title m-0">Image Properties</h5>' +
13
+ ' <button type="button" class="btn-close" data-te-close aria-label="Close"></button>' +
14
+ ' </div>' +
15
+ ' <form class="te-image-props-form">' +
16
+ ' <div class="te-modal-body">' +
17
+ ' <div class="row g-2 mb-3">' +
18
+ ' <div class="col">' +
19
+ ' <label class="form-label">Width</label>' +
20
+ ' <input type="number" min="0" class="te-image-props-width form-control" placeholder="e.g. 800">' +
21
+ ' </div>' +
22
+ ' <div class="col">' +
23
+ ' <label class="form-label">Height</label>' +
24
+ ' <input type="number" min="0" class="te-image-props-height form-control" placeholder="e.g. 600">' +
25
+ ' </div>' +
26
+ ' </div>' +
27
+ ' <div class="mb-3">' +
28
+ ' <label class="form-label">Alt</label>' +
29
+ ' <input type="text" class="te-image-props-alt form-control" placeholder="Alternative text">' +
30
+ ' </div>' +
31
+ ' <div class="row g-2">' +
32
+ ' <div class="col">' +
33
+ ' <label class="form-label">Class</label>' +
34
+ ' <input type="text" class="te-image-props-class form-control" placeholder="class-1 class-2">' +
35
+ ' </div>' +
36
+ ' <div class="col">' +
37
+ ' <label class="form-label">ID</label>' +
38
+ ' <input type="text" class="te-image-props-id form-control" placeholder="optional-id">' +
39
+ ' </div>' +
40
+ ' </div>' +
41
+ ' <div class="mb-3 mt-3">' +
42
+ ' <label class="form-label">Style (inline CSS)</label>' +
43
+ ' <input type="text" class="te-image-props-style form-control" placeholder="max-width:100%; height:auto;">' +
44
+ ' </div>' +
45
+ ' </div>' +
46
+ ' <div class="te-modal-footer">' +
47
+ ' <button type="button" class="btn btn-outline-secondary" data-te-close>Cancel</button>' +
48
+ ' <button type="submit" class="btn btn-primary">Apply</button>' +
49
+ ' </div>' +
50
+ ' </form>' +
51
+ ' </div>' +
52
+ '</div>',
53
+ init(ctx: PluginContext): void {
54
+ if(!ctx.features.image || !ctx.features.imageProps) return;
55
+ var modal = ctx.wrapper.querySelector('.te-image-props-modal') as HTMLElement | null;
56
+ var form = ctx.wrapper.querySelector('.te-image-props-form') as HTMLFormElement | null;
57
+ var altInp = ctx.wrapper.querySelector('.te-image-props-alt') as HTMLInputElement | null;
58
+ var wInp = ctx.wrapper.querySelector('.te-image-props-width') as HTMLInputElement | null;
59
+ var hInp = ctx.wrapper.querySelector('.te-image-props-height') as HTMLInputElement | null;
60
+ var clsInp = ctx.wrapper.querySelector('.te-image-props-class') as HTMLInputElement | null;
61
+ var idInp = ctx.wrapper.querySelector('.te-image-props-id') as HTMLInputElement | null;
62
+ var styleInp = ctx.wrapper.querySelector('.te-image-props-style') as HTMLInputElement | null;
63
+ var currentImg: HTMLImageElement | null = null;
64
+
65
+ ctx.editor.addEventListener('dblclick', function(e: MouseEvent){
66
+ var img = e.target && ((e.target as Element).closest && (e.target as Element).closest('img')) as HTMLImageElement | null;
67
+ if(img && ctx.wrapper.contains(img) && modal){
68
+ currentImg = img;
69
+ if(altInp) altInp.value = img.getAttribute('alt') || '';
70
+ if(wInp) wInp.value = img.getAttribute('width') || '';
71
+ if(hInp) hInp.value = img.getAttribute('height') || '';
72
+ if(clsInp) clsInp.value = img.getAttribute('class') || '';
73
+ if(idInp) idInp.value = img.getAttribute('id') || '';
74
+ if(styleInp) styleInp.value = img.getAttribute('style') || '';
75
+ modal.classList.add('is-open');
76
+ }
77
+ });
78
+
79
+ if(form){
80
+ form.addEventListener('submit', function(e: Event){
81
+ e.preventDefault();
82
+ if(!currentImg){
83
+ if(modal) modal.classList.remove('is-open');
84
+ return;
85
+ }
86
+ var alt = (altInp && altInp.value) || '';
87
+ var w = (wInp && wInp.value) || '';
88
+ var h = (hInp && hInp.value) || '';
89
+ var cls = (clsInp && clsInp.value) || '';
90
+ var idv = (idInp && idInp.value) || '';
91
+ var style = (styleInp && styleInp.value) || '';
92
+ if(style){
93
+ var safeCss: string[] = [];
94
+ style.split(';').forEach(function(decl){
95
+ var idx = decl.indexOf(':');
96
+ if(idx === -1) return;
97
+ var prop = decl.slice(0, idx).trim().toLowerCase();
98
+ var val = decl.slice(idx + 1).trim();
99
+ var SAFE = ['color','background-color','font-family','font-size','font-style','font-weight','text-align','text-decoration','line-height','letter-spacing','word-spacing','margin','margin-top','margin-right','margin-bottom','margin-left','padding','padding-top','padding-right','padding-bottom','padding-left','border','border-collapse','border-color','border-style','border-width','border-radius','width','height','max-width','max-height','min-width','min-height','float','clear','vertical-align','direction','white-space'];
100
+ if(SAFE.indexOf(prop) !== -1) safeCss.push(prop + ':' + val);
101
+ });
102
+ style = safeCss.join(';');
103
+ }
104
+
105
+ if(alt) currentImg.setAttribute('alt', alt); else currentImg.removeAttribute('alt');
106
+ if(w) currentImg.setAttribute('width', w); else currentImg.removeAttribute('width');
107
+ if(h) currentImg.setAttribute('height', h); else currentImg.removeAttribute('height');
108
+ if(cls) currentImg.setAttribute('class', cls); else currentImg.removeAttribute('class');
109
+ if(idv) currentImg.setAttribute('id', idv); else currentImg.removeAttribute('id');
110
+ if(style) currentImg.setAttribute('style', style); else currentImg.removeAttribute('style');
111
+
112
+ if(modal) modal.classList.remove('is-open');
113
+ currentImg = null;
114
+ });
115
+ }
116
+ }
117
+ };
118
+
119
+ export default imageProps;