jotterjs 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 +122 -0
- package/dist/jotter.iife.min.js +5 -0
- package/dist/jotter.js +1370 -0
- package/dist/jotter.min.css +1 -0
- package/dist/jotter.min.js +5 -0
- package/package.json +45 -0
package/dist/jotter.js
ADDED
|
@@ -0,0 +1,1370 @@
|
|
|
1
|
+
// src/jotter.js
|
|
2
|
+
var TOOLBAR_ACTIONS = [
|
|
3
|
+
{ custom: "toggleSource", label: "Source", title: "Edit HTML Source" },
|
|
4
|
+
{ type: "sep" },
|
|
5
|
+
{ cmd: "undo", icon: "undo", title: "Undo (Ctrl+Z)" },
|
|
6
|
+
{ cmd: "redo", icon: "redo", title: "Redo (Ctrl+Y)" },
|
|
7
|
+
{ type: "sep" },
|
|
8
|
+
{ cmd: "copy", icon: "content_copy", title: "Copy" },
|
|
9
|
+
{ cmd: "cut", icon: "content_cut", title: "Cut" },
|
|
10
|
+
{ cmd: "paste", icon: "content_paste", title: "Paste" },
|
|
11
|
+
{ type: "sep" },
|
|
12
|
+
{ cmd: "removeFormat", icon: "format_clear", title: "Clear Formatting" },
|
|
13
|
+
{ type: "sep" },
|
|
14
|
+
{ type: "blockformat" },
|
|
15
|
+
{ type: "fontfamily" },
|
|
16
|
+
{ type: "fontsize" },
|
|
17
|
+
{ type: "sep" },
|
|
18
|
+
{ cmd: "bold", icon: "format_bold", title: "Bold (Ctrl+B)" },
|
|
19
|
+
{ cmd: "italic", icon: "format_italic", title: "Italic (Ctrl+I)" },
|
|
20
|
+
{ cmd: "underline", icon: "format_underlined", title: "Underline (Ctrl+U)" },
|
|
21
|
+
{ cmd: "strikeThrough", icon: "strikethrough_s", title: "Strikethrough" },
|
|
22
|
+
{ cmd: "subscript", icon: "subscript", title: "Subscript" },
|
|
23
|
+
{ cmd: "superscript", icon: "superscript", title: "Superscript" },
|
|
24
|
+
{ custom: "code", icon: "code", title: "Inline Code" },
|
|
25
|
+
{ type: "sep" },
|
|
26
|
+
{ type: "color", cmd: "foreColor", icon: "format_color_text", title: "Text Color" },
|
|
27
|
+
{ type: "color", cmd: "hiliteColor", icon: "format_color_fill", title: "Background Color" },
|
|
28
|
+
{ type: "sep" },
|
|
29
|
+
{ cmd: "justifyLeft", icon: "format_align_left", title: "Align Left" },
|
|
30
|
+
{ cmd: "justifyCenter", icon: "format_align_center", title: "Align Center" },
|
|
31
|
+
{ cmd: "justifyRight", icon: "format_align_right", title: "Align Right" },
|
|
32
|
+
{ type: "sep" },
|
|
33
|
+
{ cmd: "insertUnorderedList", icon: "format_list_bulleted", title: "Bullet List" },
|
|
34
|
+
{ cmd: "insertOrderedList", icon: "format_list_numbered", title: "Numbered List" },
|
|
35
|
+
{ type: "sep" },
|
|
36
|
+
{ type: "popup", id: "link", icon: "insert_link", title: "Insert Link" },
|
|
37
|
+
{ cmd: "unlink", icon: "link_off", title: "Remove Link" },
|
|
38
|
+
{ type: "sep" },
|
|
39
|
+
{ type: "popup", id: "image", icon: "image", title: "Insert Image" },
|
|
40
|
+
{ type: "popup", id: "video", icon: "smart_display", title: "Insert YouTube Video" },
|
|
41
|
+
{ type: "popup", id: "table", icon: "table_chart", title: "Insert Table" },
|
|
42
|
+
{ type: "popup", id: "embed", icon: "html", title: "Insert Embed" },
|
|
43
|
+
{ type: "popup", id: "symbol", icon: "emoji_symbols", title: "Insert Symbol" },
|
|
44
|
+
{ type: "popup", id: "specialchar", icon: "format_shapes", title: "Special Characters" },
|
|
45
|
+
{ type: "popup", id: "lorem", icon: "article", title: "Insert Lorem Ipsum" },
|
|
46
|
+
{ type: "sep" },
|
|
47
|
+
{ type: "theme" },
|
|
48
|
+
{ type: "sep" },
|
|
49
|
+
{ type: "theme" }
|
|
50
|
+
];
|
|
51
|
+
var HEADING_OPTIONS = [
|
|
52
|
+
{ label: "Paragraph", tag: "p" },
|
|
53
|
+
{ label: "Heading 1", tag: "h1" },
|
|
54
|
+
{ label: "Heading 2", tag: "h2" },
|
|
55
|
+
{ label: "Heading 3", tag: "h3" },
|
|
56
|
+
{ label: "Heading 4", tag: "h4" },
|
|
57
|
+
{ label: "Pre / Code", tag: "pre" },
|
|
58
|
+
{ label: "Blockquote", tag: "blockquote" }
|
|
59
|
+
];
|
|
60
|
+
var FONT_FAMILIES = [
|
|
61
|
+
"Arial",
|
|
62
|
+
"Arial Black",
|
|
63
|
+
"Comic Sans MS",
|
|
64
|
+
"Courier New",
|
|
65
|
+
"Georgia",
|
|
66
|
+
"Impact",
|
|
67
|
+
"Lucida Console",
|
|
68
|
+
"Palatino Linotype",
|
|
69
|
+
"Tahoma",
|
|
70
|
+
"Times New Roman",
|
|
71
|
+
"Trebuchet MS",
|
|
72
|
+
"Verdana"
|
|
73
|
+
];
|
|
74
|
+
var FONT_SIZES = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72];
|
|
75
|
+
var SYMBOLS = [
|
|
76
|
+
"\u2190",
|
|
77
|
+
"\u2192",
|
|
78
|
+
"\u2191",
|
|
79
|
+
"\u2193",
|
|
80
|
+
"\u2194",
|
|
81
|
+
"\u2195",
|
|
82
|
+
"\u21D0",
|
|
83
|
+
"\u21D2",
|
|
84
|
+
"\u21D1",
|
|
85
|
+
"\u21D3",
|
|
86
|
+
"\u21D4",
|
|
87
|
+
"\u2022",
|
|
88
|
+
"\xB7",
|
|
89
|
+
"\u25E6",
|
|
90
|
+
"\u25CB",
|
|
91
|
+
"\u25CF",
|
|
92
|
+
"\u25A1",
|
|
93
|
+
"\u25A0",
|
|
94
|
+
"\u25C6",
|
|
95
|
+
"\u25C7",
|
|
96
|
+
"\u25B2",
|
|
97
|
+
"\u25BC",
|
|
98
|
+
"\u2605",
|
|
99
|
+
"\u2606",
|
|
100
|
+
"\u2660",
|
|
101
|
+
"\u2663",
|
|
102
|
+
"\u2665",
|
|
103
|
+
"\u2666",
|
|
104
|
+
"\u2713",
|
|
105
|
+
"\u2717",
|
|
106
|
+
"\u2715",
|
|
107
|
+
"\u2718",
|
|
108
|
+
"\u2248",
|
|
109
|
+
"\u2260",
|
|
110
|
+
"\u2261",
|
|
111
|
+
"\u2264",
|
|
112
|
+
"\u2265",
|
|
113
|
+
"\xF7",
|
|
114
|
+
"\xD7",
|
|
115
|
+
"\xB1",
|
|
116
|
+
"\u221E",
|
|
117
|
+
"\u221A",
|
|
118
|
+
"\u2211",
|
|
119
|
+
"\u220F",
|
|
120
|
+
"\u222B",
|
|
121
|
+
"\u2202",
|
|
122
|
+
"\u2206",
|
|
123
|
+
"\u2207",
|
|
124
|
+
"\u03C0",
|
|
125
|
+
"\u03A9",
|
|
126
|
+
"\u03BC",
|
|
127
|
+
"\u03B1",
|
|
128
|
+
"\u03B2",
|
|
129
|
+
"\u03B3",
|
|
130
|
+
"\xA9",
|
|
131
|
+
"\xAE",
|
|
132
|
+
"\u2122",
|
|
133
|
+
"\xA7",
|
|
134
|
+
"\xB6",
|
|
135
|
+
"\u2020",
|
|
136
|
+
"\u2021",
|
|
137
|
+
"\xB0",
|
|
138
|
+
"\u2032",
|
|
139
|
+
"\u2033",
|
|
140
|
+
"\u2030",
|
|
141
|
+
"\u201C",
|
|
142
|
+
"\u201D",
|
|
143
|
+
"\u2018",
|
|
144
|
+
"\u2019",
|
|
145
|
+
"\xAB",
|
|
146
|
+
"\xBB",
|
|
147
|
+
"\u2039",
|
|
148
|
+
"\u203A",
|
|
149
|
+
"\u2014",
|
|
150
|
+
"\u2013",
|
|
151
|
+
"\u2026",
|
|
152
|
+
"\xBF",
|
|
153
|
+
"\xA1",
|
|
154
|
+
"\u20AC",
|
|
155
|
+
"\xA3",
|
|
156
|
+
"\xA5",
|
|
157
|
+
"\xA2",
|
|
158
|
+
"\u20B9",
|
|
159
|
+
"\u20BD",
|
|
160
|
+
"\u20BF"
|
|
161
|
+
];
|
|
162
|
+
var SPECIAL_CHARS = [
|
|
163
|
+
"\xC0",
|
|
164
|
+
"\xC1",
|
|
165
|
+
"\xC2",
|
|
166
|
+
"\xC3",
|
|
167
|
+
"\xC4",
|
|
168
|
+
"\xC5",
|
|
169
|
+
"\xC6",
|
|
170
|
+
"\xC7",
|
|
171
|
+
"\xC8",
|
|
172
|
+
"\xC9",
|
|
173
|
+
"\xCA",
|
|
174
|
+
"\xCB",
|
|
175
|
+
"\xCC",
|
|
176
|
+
"\xCD",
|
|
177
|
+
"\xCE",
|
|
178
|
+
"\xCF",
|
|
179
|
+
"\xD0",
|
|
180
|
+
"\xD1",
|
|
181
|
+
"\xD2",
|
|
182
|
+
"\xD3",
|
|
183
|
+
"\xD4",
|
|
184
|
+
"\xD5",
|
|
185
|
+
"\xD6",
|
|
186
|
+
"\xD8",
|
|
187
|
+
"\xD9",
|
|
188
|
+
"\xDA",
|
|
189
|
+
"\xDB",
|
|
190
|
+
"\xDC",
|
|
191
|
+
"\xDD",
|
|
192
|
+
"\xDE",
|
|
193
|
+
"\xDF",
|
|
194
|
+
"\xE0",
|
|
195
|
+
"\xE1",
|
|
196
|
+
"\xE2",
|
|
197
|
+
"\xE3",
|
|
198
|
+
"\xE4",
|
|
199
|
+
"\xE5",
|
|
200
|
+
"\xE6",
|
|
201
|
+
"\xE7",
|
|
202
|
+
"\xE8",
|
|
203
|
+
"\xE9",
|
|
204
|
+
"\xEA",
|
|
205
|
+
"\xEB",
|
|
206
|
+
"\xEC",
|
|
207
|
+
"\xED",
|
|
208
|
+
"\xEE",
|
|
209
|
+
"\xEF",
|
|
210
|
+
"\xF0",
|
|
211
|
+
"\xF1",
|
|
212
|
+
"\xF2",
|
|
213
|
+
"\xF3",
|
|
214
|
+
"\xF4",
|
|
215
|
+
"\xF5",
|
|
216
|
+
"\xF6",
|
|
217
|
+
"\xF8",
|
|
218
|
+
"\xF9",
|
|
219
|
+
"\xFA",
|
|
220
|
+
"\xFB",
|
|
221
|
+
"\xFC",
|
|
222
|
+
"\xFD",
|
|
223
|
+
"\xFE",
|
|
224
|
+
"\xFF",
|
|
225
|
+
"\u0152",
|
|
226
|
+
"\u0153",
|
|
227
|
+
"\u0160",
|
|
228
|
+
"\u0161",
|
|
229
|
+
"\u0178",
|
|
230
|
+
"\u017D",
|
|
231
|
+
"\u017E"
|
|
232
|
+
];
|
|
233
|
+
var THEMES = [
|
|
234
|
+
{ id: "default", label: "Default" },
|
|
235
|
+
{ id: "warm", label: "Warm" },
|
|
236
|
+
{ id: "ink", label: "Ink / Navy" },
|
|
237
|
+
{ id: "forest", label: "Forest" }
|
|
238
|
+
];
|
|
239
|
+
var LOREM_VARIANTS = [
|
|
240
|
+
{
|
|
241
|
+
label: "Short \u2014 1 sentence",
|
|
242
|
+
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
label: "Medium \u2014 1 paragraph",
|
|
246
|
+
text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
label: "Long \u2014 3 paragraphs",
|
|
250
|
+
isHTML: true,
|
|
251
|
+
text: "<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.</p><p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p><p>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.</p>"
|
|
252
|
+
}
|
|
253
|
+
];
|
|
254
|
+
var JotterJS = class {
|
|
255
|
+
constructor(target, options = {}) {
|
|
256
|
+
this._target = typeof target === "string" ? document.querySelector(target) : target;
|
|
257
|
+
if (!this._target)
|
|
258
|
+
throw new Error("[JotterJS] Target element not found.");
|
|
259
|
+
this._options = Object.assign({
|
|
260
|
+
placeholder: "Start typing\u2026",
|
|
261
|
+
height: "320px",
|
|
262
|
+
theme: "default",
|
|
263
|
+
onChange: null,
|
|
264
|
+
onFocus: null,
|
|
265
|
+
onBlur: null
|
|
266
|
+
}, options);
|
|
267
|
+
this._listeners = {};
|
|
268
|
+
this._savedRange = null;
|
|
269
|
+
this._lastForeColor = "#e8e4d8";
|
|
270
|
+
this._lastHiliteColor = "#c8a96e";
|
|
271
|
+
this._init();
|
|
272
|
+
}
|
|
273
|
+
// ─── Init ─────────────────────────────────────────────────────────────────
|
|
274
|
+
// Popup is appended to document.body (not the root) to escape overflow:hidden.
|
|
275
|
+
// _savedRange stores the selection before any toolbar interaction steals focus,
|
|
276
|
+
// so popup submit handlers can restore it via _restoreRange().
|
|
277
|
+
_init() {
|
|
278
|
+
const initialHTML = this._target.innerHTML || "";
|
|
279
|
+
this._target.innerHTML = "";
|
|
280
|
+
this._target.classList.add("jotter-host");
|
|
281
|
+
this._root = document.createElement("div");
|
|
282
|
+
this._root.className = "htmled";
|
|
283
|
+
this._toolbar = this._buildToolbar();
|
|
284
|
+
this._editorWrap = document.createElement("div");
|
|
285
|
+
this._editorWrap.className = "jotter-editor-wrap";
|
|
286
|
+
this._editor = document.createElement("div");
|
|
287
|
+
this._editor.className = "jotter-editor";
|
|
288
|
+
this._editor.contentEditable = "true";
|
|
289
|
+
this._editor.setAttribute("data-placeholder", this._options.placeholder);
|
|
290
|
+
this._editor.style.minHeight = this._options.height;
|
|
291
|
+
this._editor.innerHTML = this._sanitize(initialHTML);
|
|
292
|
+
this._editor.spellcheck = true;
|
|
293
|
+
this._source = document.createElement("textarea");
|
|
294
|
+
this._source.className = "jotter-source";
|
|
295
|
+
this._source.setAttribute("aria-label", "HTML source");
|
|
296
|
+
this._source.setAttribute("spellcheck", "false");
|
|
297
|
+
this._source.style.minHeight = this._options.height;
|
|
298
|
+
this._sourceMode = false;
|
|
299
|
+
this._statusBar = this._buildStatusBar();
|
|
300
|
+
this._editorWrap.appendChild(this._editor);
|
|
301
|
+
this._editorWrap.appendChild(this._source);
|
|
302
|
+
this._root.appendChild(this._toolbar);
|
|
303
|
+
this._root.appendChild(this._editorWrap);
|
|
304
|
+
this._root.appendChild(this._statusBar);
|
|
305
|
+
this._target.appendChild(this._root);
|
|
306
|
+
this._popup = this._buildPopupContainer();
|
|
307
|
+
document.body.appendChild(this._popup);
|
|
308
|
+
this._bindEvents();
|
|
309
|
+
this._updateToolbarState();
|
|
310
|
+
this._updateStatus();
|
|
311
|
+
this.setTheme(this._options.theme);
|
|
312
|
+
}
|
|
313
|
+
// ─── Toolbar ──────────────────────────────────────────────────────────────
|
|
314
|
+
/** Renders options.toolbar (or TOOLBAR_ACTIONS) into .jotter-toolbar. */
|
|
315
|
+
_buildToolbar() {
|
|
316
|
+
const bar = document.createElement("div");
|
|
317
|
+
bar.className = "jotter-toolbar";
|
|
318
|
+
this._toolbarEl = bar;
|
|
319
|
+
(this._options.toolbar || TOOLBAR_ACTIONS).forEach((action) => {
|
|
320
|
+
const el = this._buildAction(action);
|
|
321
|
+
if (el)
|
|
322
|
+
bar.appendChild(el);
|
|
323
|
+
});
|
|
324
|
+
return bar;
|
|
325
|
+
}
|
|
326
|
+
/** Dispatches an action descriptor to the appropriate builder. */
|
|
327
|
+
_buildAction(action) {
|
|
328
|
+
switch (action.type) {
|
|
329
|
+
case "sep":
|
|
330
|
+
return this._makeSep();
|
|
331
|
+
case "blockformat":
|
|
332
|
+
return this._buildBlockFormatSelect();
|
|
333
|
+
case "fontfamily":
|
|
334
|
+
return this._buildFontFamilySelect();
|
|
335
|
+
case "fontsize":
|
|
336
|
+
return this._buildFontSizeSelect();
|
|
337
|
+
case "color":
|
|
338
|
+
return this._buildColorBtn(action);
|
|
339
|
+
case "popup":
|
|
340
|
+
return this._buildPopupBtn(action);
|
|
341
|
+
case "theme":
|
|
342
|
+
return this._buildThemeSelect();
|
|
343
|
+
default:
|
|
344
|
+
return this._buildBtn(action);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
_makeSep() {
|
|
348
|
+
const s = document.createElement("span");
|
|
349
|
+
s.className = "jotter-sep";
|
|
350
|
+
return s;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* General button builder. Priority order for click handling:
|
|
354
|
+
* 1. action.onClick(editor) — external callback
|
|
355
|
+
* 2. action.custom — internal method ('toggleSource' | 'code')
|
|
356
|
+
* 3. action.cmd — document.execCommand
|
|
357
|
+
* 4. action.prompt — prompt() then execCommand (legacy fallback)
|
|
358
|
+
*/
|
|
359
|
+
_buildBtn(action) {
|
|
360
|
+
const btn = document.createElement("button");
|
|
361
|
+
btn.type = "button";
|
|
362
|
+
btn.className = "jotter-btn";
|
|
363
|
+
if (action.cmd)
|
|
364
|
+
btn.dataset.cmd = action.cmd;
|
|
365
|
+
if (action.custom)
|
|
366
|
+
btn.dataset.custom = action.custom;
|
|
367
|
+
btn.title = action.title;
|
|
368
|
+
btn.setAttribute("aria-label", action.title);
|
|
369
|
+
if (action.label) {
|
|
370
|
+
btn.classList.add("jotter-btn--text");
|
|
371
|
+
btn.appendChild(document.createTextNode(action.label));
|
|
372
|
+
} else {
|
|
373
|
+
const icon = document.createElement("span");
|
|
374
|
+
icon.className = "material-icons";
|
|
375
|
+
icon.textContent = action.icon;
|
|
376
|
+
btn.appendChild(icon);
|
|
377
|
+
}
|
|
378
|
+
btn.addEventListener("mousedown", (e) => {
|
|
379
|
+
e.preventDefault();
|
|
380
|
+
this._editor.focus();
|
|
381
|
+
if (action.onClick) {
|
|
382
|
+
action.onClick(this);
|
|
383
|
+
} else if (action.custom === "toggleSource") {
|
|
384
|
+
this._toggleSourceMode();
|
|
385
|
+
} else if (action.custom === "code") {
|
|
386
|
+
this._toggleInlineCode();
|
|
387
|
+
} else if (action.cmd === "copy") {
|
|
388
|
+
document.execCommand("copy");
|
|
389
|
+
} else if (action.cmd === "cut") {
|
|
390
|
+
document.execCommand("cut");
|
|
391
|
+
} else if (action.cmd === "paste") {
|
|
392
|
+
this._pasteFromClipboard();
|
|
393
|
+
} else if (action.prompt) {
|
|
394
|
+
const val = window.prompt(action.prompt);
|
|
395
|
+
if (val)
|
|
396
|
+
document.execCommand(action.cmd, false, val);
|
|
397
|
+
} else {
|
|
398
|
+
document.execCommand(action.cmd, false, null);
|
|
399
|
+
}
|
|
400
|
+
this._updateToolbarState();
|
|
401
|
+
this._updateStatus();
|
|
402
|
+
this._emit("change", this.getHTML());
|
|
403
|
+
if (this._options.onChange)
|
|
404
|
+
this._options.onChange(this.getHTML());
|
|
405
|
+
});
|
|
406
|
+
return btn;
|
|
407
|
+
}
|
|
408
|
+
_buildBlockFormatSelect() {
|
|
409
|
+
const sel = document.createElement("select");
|
|
410
|
+
sel.className = "jotter-select";
|
|
411
|
+
sel.title = "Block format";
|
|
412
|
+
sel.dataset.id = "blockformat";
|
|
413
|
+
HEADING_OPTIONS.forEach(({ label, tag }) => {
|
|
414
|
+
const opt = document.createElement("option");
|
|
415
|
+
opt.value = tag;
|
|
416
|
+
opt.textContent = label;
|
|
417
|
+
sel.appendChild(opt);
|
|
418
|
+
});
|
|
419
|
+
sel.addEventListener("mousedown", () => {
|
|
420
|
+
this._savedRange = this._saveRange();
|
|
421
|
+
});
|
|
422
|
+
sel.addEventListener("change", () => {
|
|
423
|
+
this._restoreRange(this._savedRange);
|
|
424
|
+
document.execCommand("formatBlock", false, sel.value);
|
|
425
|
+
this._editor.focus();
|
|
426
|
+
this._updateStatus();
|
|
427
|
+
this._emit("change", this.getHTML());
|
|
428
|
+
if (this._options.onChange)
|
|
429
|
+
this._options.onChange(this.getHTML());
|
|
430
|
+
});
|
|
431
|
+
return sel;
|
|
432
|
+
}
|
|
433
|
+
_buildFontFamilySelect() {
|
|
434
|
+
const sel = document.createElement("select");
|
|
435
|
+
sel.className = "jotter-select jotter-select--font";
|
|
436
|
+
sel.title = "Font family";
|
|
437
|
+
sel.dataset.id = "fontfamily";
|
|
438
|
+
const def = document.createElement("option");
|
|
439
|
+
def.value = "";
|
|
440
|
+
def.textContent = "Font";
|
|
441
|
+
sel.appendChild(def);
|
|
442
|
+
FONT_FAMILIES.forEach((f) => {
|
|
443
|
+
const opt = document.createElement("option");
|
|
444
|
+
opt.value = f;
|
|
445
|
+
opt.textContent = f;
|
|
446
|
+
opt.style.fontFamily = f;
|
|
447
|
+
sel.appendChild(opt);
|
|
448
|
+
});
|
|
449
|
+
sel.addEventListener("mousedown", () => {
|
|
450
|
+
this._savedRange = this._saveRange();
|
|
451
|
+
});
|
|
452
|
+
sel.addEventListener("change", () => {
|
|
453
|
+
if (!sel.value)
|
|
454
|
+
return;
|
|
455
|
+
this._restoreRange(this._savedRange);
|
|
456
|
+
document.execCommand("fontName", false, sel.value);
|
|
457
|
+
this._editor.focus();
|
|
458
|
+
this._emit("change", this.getHTML());
|
|
459
|
+
if (this._options.onChange)
|
|
460
|
+
this._options.onChange(this.getHTML());
|
|
461
|
+
});
|
|
462
|
+
return sel;
|
|
463
|
+
}
|
|
464
|
+
_buildFontSizeSelect() {
|
|
465
|
+
const sel = document.createElement("select");
|
|
466
|
+
sel.className = "jotter-select jotter-select--size";
|
|
467
|
+
sel.title = "Font size";
|
|
468
|
+
sel.dataset.id = "fontsize";
|
|
469
|
+
const def = document.createElement("option");
|
|
470
|
+
def.value = "";
|
|
471
|
+
def.textContent = "Size";
|
|
472
|
+
sel.appendChild(def);
|
|
473
|
+
FONT_SIZES.forEach((s) => {
|
|
474
|
+
const opt = document.createElement("option");
|
|
475
|
+
opt.value = s;
|
|
476
|
+
opt.textContent = `${s}px`;
|
|
477
|
+
sel.appendChild(opt);
|
|
478
|
+
});
|
|
479
|
+
sel.addEventListener("mousedown", () => {
|
|
480
|
+
this._savedRange = this._saveRange();
|
|
481
|
+
});
|
|
482
|
+
sel.addEventListener("change", () => {
|
|
483
|
+
if (!sel.value)
|
|
484
|
+
return;
|
|
485
|
+
this._restoreRange(this._savedRange);
|
|
486
|
+
this._applyFontSize(sel.value);
|
|
487
|
+
this._editor.focus();
|
|
488
|
+
this._emit("change", this.getHTML());
|
|
489
|
+
if (this._options.onChange)
|
|
490
|
+
this._options.onChange(this.getHTML());
|
|
491
|
+
});
|
|
492
|
+
return sel;
|
|
493
|
+
}
|
|
494
|
+
_buildThemeSelect() {
|
|
495
|
+
const sel = document.createElement("select");
|
|
496
|
+
sel.className = "jotter-select jotter-select--theme";
|
|
497
|
+
sel.title = "Editor theme";
|
|
498
|
+
sel.dataset.id = "theme";
|
|
499
|
+
THEMES.forEach(({ id, label }) => {
|
|
500
|
+
const opt = document.createElement("option");
|
|
501
|
+
opt.value = id;
|
|
502
|
+
opt.textContent = label;
|
|
503
|
+
sel.appendChild(opt);
|
|
504
|
+
});
|
|
505
|
+
sel.value = this._options.theme;
|
|
506
|
+
sel.addEventListener("change", () => this.setTheme(sel.value));
|
|
507
|
+
this._themeSelect = sel;
|
|
508
|
+
return sel;
|
|
509
|
+
}
|
|
510
|
+
_buildColorBtn(action) {
|
|
511
|
+
const wrap = document.createElement("span");
|
|
512
|
+
wrap.className = "jotter-color-wrap";
|
|
513
|
+
const btn = document.createElement("button");
|
|
514
|
+
btn.type = "button";
|
|
515
|
+
btn.className = "jotter-btn jotter-color-btn";
|
|
516
|
+
btn.dataset.cmd = action.cmd;
|
|
517
|
+
btn.title = action.title;
|
|
518
|
+
btn.setAttribute("aria-label", action.title);
|
|
519
|
+
const icon = document.createElement("span");
|
|
520
|
+
icon.className = "material-icons";
|
|
521
|
+
icon.textContent = action.icon;
|
|
522
|
+
btn.appendChild(icon);
|
|
523
|
+
const swatch = document.createElement("span");
|
|
524
|
+
swatch.className = "jotter-color-swatch";
|
|
525
|
+
const defaultColor = action.cmd === "foreColor" ? this._lastForeColor : this._lastHiliteColor;
|
|
526
|
+
swatch.style.background = defaultColor;
|
|
527
|
+
btn.appendChild(swatch);
|
|
528
|
+
const input = document.createElement("input");
|
|
529
|
+
input.type = "color";
|
|
530
|
+
input.className = "jotter-color-input";
|
|
531
|
+
input.value = defaultColor;
|
|
532
|
+
input.tabIndex = -1;
|
|
533
|
+
input.addEventListener("change", () => {
|
|
534
|
+
const color = input.value;
|
|
535
|
+
swatch.style.background = color;
|
|
536
|
+
if (action.cmd === "foreColor")
|
|
537
|
+
this._lastForeColor = color;
|
|
538
|
+
else
|
|
539
|
+
this._lastHiliteColor = color;
|
|
540
|
+
this._restoreRange(this._savedRange);
|
|
541
|
+
this._editor.focus();
|
|
542
|
+
document.execCommand(action.cmd, false, color);
|
|
543
|
+
this._emit("change", this.getHTML());
|
|
544
|
+
if (this._options.onChange)
|
|
545
|
+
this._options.onChange(this.getHTML());
|
|
546
|
+
});
|
|
547
|
+
btn.addEventListener("mousedown", (e) => {
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
this._savedRange = this._saveRange();
|
|
550
|
+
input.click();
|
|
551
|
+
});
|
|
552
|
+
wrap.appendChild(btn);
|
|
553
|
+
wrap.appendChild(input);
|
|
554
|
+
return wrap;
|
|
555
|
+
}
|
|
556
|
+
// ─── Popup system ─────────────────────────────────────────────────────────
|
|
557
|
+
// Single _popup element on document.body; toggled via _showPopup / _hidePopup.
|
|
558
|
+
// Clicking the same button again dismisses the popup (toggle).
|
|
559
|
+
// Outside-click and Escape both close it (see _bindEvents).
|
|
560
|
+
_buildPopupBtn(action) {
|
|
561
|
+
const btn = document.createElement("button");
|
|
562
|
+
btn.type = "button";
|
|
563
|
+
btn.className = "jotter-btn";
|
|
564
|
+
btn.title = action.title;
|
|
565
|
+
btn.setAttribute("aria-label", action.title);
|
|
566
|
+
const icon = document.createElement("span");
|
|
567
|
+
icon.className = "material-icons";
|
|
568
|
+
icon.textContent = action.icon;
|
|
569
|
+
btn.appendChild(icon);
|
|
570
|
+
btn.addEventListener("mousedown", (e) => {
|
|
571
|
+
e.preventDefault();
|
|
572
|
+
this._savedRange = this._saveRange();
|
|
573
|
+
if (this._popup.classList.contains("jotter-popup--visible") && this._popup.dataset.popupId === action.id) {
|
|
574
|
+
this._hidePopup();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
this._showPopup(btn, this._buildPopupContent(action.id), action.id);
|
|
578
|
+
});
|
|
579
|
+
return btn;
|
|
580
|
+
}
|
|
581
|
+
_buildPopupContainer() {
|
|
582
|
+
const el = document.createElement("div");
|
|
583
|
+
el.className = "jotter-popup";
|
|
584
|
+
el.setAttribute("role", "dialog");
|
|
585
|
+
return el;
|
|
586
|
+
}
|
|
587
|
+
_showPopup(anchor, content, id) {
|
|
588
|
+
this._popup.innerHTML = "";
|
|
589
|
+
this._popup.appendChild(content);
|
|
590
|
+
this._popup.dataset.popupId = id;
|
|
591
|
+
this._popup.classList.add("jotter-popup--visible");
|
|
592
|
+
const r = anchor.getBoundingClientRect();
|
|
593
|
+
this._popup.style.top = r.bottom + 6 + "px";
|
|
594
|
+
this._popup.style.left = r.left + "px";
|
|
595
|
+
this._popup.style.right = "auto";
|
|
596
|
+
requestAnimationFrame(() => {
|
|
597
|
+
const pr = this._popup.getBoundingClientRect();
|
|
598
|
+
if (pr.right > window.innerWidth - 8) {
|
|
599
|
+
this._popup.style.left = Math.max(8, r.left - (pr.right - window.innerWidth + 8)) + "px";
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
_hidePopup() {
|
|
604
|
+
this._popup.classList.remove("jotter-popup--visible");
|
|
605
|
+
this._popup.dataset.popupId = "";
|
|
606
|
+
}
|
|
607
|
+
/** Returns the DOM subtree for the popup identified by id. */
|
|
608
|
+
_buildPopupContent(id) {
|
|
609
|
+
switch (id) {
|
|
610
|
+
case "link":
|
|
611
|
+
return this._popupLink();
|
|
612
|
+
case "table":
|
|
613
|
+
return this._popupTable();
|
|
614
|
+
case "image":
|
|
615
|
+
return this._popupImage();
|
|
616
|
+
case "video":
|
|
617
|
+
return this._popupVideo();
|
|
618
|
+
case "embed":
|
|
619
|
+
return this._popupEmbed();
|
|
620
|
+
case "symbol":
|
|
621
|
+
return this._popupSymbol();
|
|
622
|
+
case "specialchar":
|
|
623
|
+
return this._popupSpecialChar();
|
|
624
|
+
case "lorem":
|
|
625
|
+
return this._popupLorem();
|
|
626
|
+
default: {
|
|
627
|
+
const d = document.createElement("div");
|
|
628
|
+
d.className = "jotter-popup-inner";
|
|
629
|
+
d.textContent = "Unknown: " + id;
|
|
630
|
+
return d;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/** Hover-to-select grid (up to 8×10). Click inserts <table> with <th> header row. */
|
|
635
|
+
_popupTable() {
|
|
636
|
+
const wrap = document.createElement("div");
|
|
637
|
+
wrap.className = "jotter-popup-inner";
|
|
638
|
+
const title = this._popupTitle("Insert Table");
|
|
639
|
+
wrap.appendChild(title);
|
|
640
|
+
const COLS = 10, ROWS = 8;
|
|
641
|
+
const grid = document.createElement("div");
|
|
642
|
+
grid.className = "jotter-table-grid";
|
|
643
|
+
grid.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
|
|
644
|
+
const hint = document.createElement("div");
|
|
645
|
+
hint.className = "jotter-popup-hint";
|
|
646
|
+
hint.textContent = "Hover to select size";
|
|
647
|
+
const cells = [];
|
|
648
|
+
for (let r = 0; r < ROWS; r++) {
|
|
649
|
+
for (let c = 0; c < COLS; c++) {
|
|
650
|
+
const cell = document.createElement("span");
|
|
651
|
+
cell.className = "jotter-table-cell";
|
|
652
|
+
cell.dataset.r = r;
|
|
653
|
+
cell.dataset.c = c;
|
|
654
|
+
cell.addEventListener("mouseenter", () => {
|
|
655
|
+
hint.textContent = `${r + 1} \xD7 ${c + 1} table`;
|
|
656
|
+
cells.forEach((cl) => {
|
|
657
|
+
cl.classList.toggle(
|
|
658
|
+
"jotter-table-cell--active",
|
|
659
|
+
+cl.dataset.r <= r && +cl.dataset.c <= c
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
cell.addEventListener("click", () => {
|
|
664
|
+
this._insertTable(r + 1, c + 1);
|
|
665
|
+
this._hidePopup();
|
|
666
|
+
});
|
|
667
|
+
cells.push(cell);
|
|
668
|
+
grid.appendChild(cell);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
wrap.appendChild(grid);
|
|
672
|
+
wrap.appendChild(hint);
|
|
673
|
+
return wrap;
|
|
674
|
+
}
|
|
675
|
+
_insertTable(rows, cols) {
|
|
676
|
+
this._restoreRange(this._savedRange);
|
|
677
|
+
this._editor.focus();
|
|
678
|
+
let html = "<table><tbody>";
|
|
679
|
+
for (let r = 0; r < rows; r++) {
|
|
680
|
+
html += "<tr>";
|
|
681
|
+
for (let c = 0; c < cols; c++) {
|
|
682
|
+
html += r === 0 ? "<th><br></th>" : "<td><br></td>";
|
|
683
|
+
}
|
|
684
|
+
html += "</tr>";
|
|
685
|
+
}
|
|
686
|
+
html += "</tbody></table><p><br></p>";
|
|
687
|
+
document.execCommand("insertHTML", false, html);
|
|
688
|
+
this._emit("change", this.getHTML());
|
|
689
|
+
if (this._options.onChange)
|
|
690
|
+
this._options.onChange(this.getHTML());
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Fields: URL, link text, title/tooltip, target.
|
|
694
|
+
* Pre-fills from existing <a> under caret (edit mode) or current selection (text mode).
|
|
695
|
+
* Edit mode mutates the anchor in-place; insert mode uses execCommand('insertHTML').
|
|
696
|
+
*/
|
|
697
|
+
_popupLink() {
|
|
698
|
+
const wrap = document.createElement("div");
|
|
699
|
+
wrap.className = "jotter-popup-inner jotter-popup-form";
|
|
700
|
+
wrap.appendChild(this._popupTitle("Insert Link"));
|
|
701
|
+
let existingAnchor = null;
|
|
702
|
+
let selectedText = "";
|
|
703
|
+
if (this._savedRange) {
|
|
704
|
+
const sel = window.getSelection();
|
|
705
|
+
if (sel && sel.rangeCount) {
|
|
706
|
+
selectedText = sel.toString();
|
|
707
|
+
let node = sel.anchorNode;
|
|
708
|
+
while (node && node !== this._editor) {
|
|
709
|
+
if (node.nodeName === "A") {
|
|
710
|
+
existingAnchor = node;
|
|
711
|
+
break;
|
|
712
|
+
}
|
|
713
|
+
node = node.parentNode;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const urlInput = this._makeField(wrap, "URL", "url", "https://");
|
|
718
|
+
const textInput = this._makeField(wrap, "Link text (leave blank to keep selection)", "text", "");
|
|
719
|
+
const titleInput = this._makeField(wrap, "Title / tooltip", "text", "");
|
|
720
|
+
const targetLabel = document.createElement("label");
|
|
721
|
+
targetLabel.className = "jotter-popup-label";
|
|
722
|
+
targetLabel.textContent = "Open in";
|
|
723
|
+
const targetSel = document.createElement("select");
|
|
724
|
+
targetSel.className = "jotter-popup-select";
|
|
725
|
+
[["(same window)", ""], ["New tab (_blank)", "_blank"], ["Parent frame (_parent)", "_parent"], ["Top frame (_top)", "_top"]].forEach(([text, val]) => {
|
|
726
|
+
const o = document.createElement("option");
|
|
727
|
+
o.value = val;
|
|
728
|
+
o.textContent = text;
|
|
729
|
+
targetSel.appendChild(o);
|
|
730
|
+
});
|
|
731
|
+
wrap.appendChild(targetLabel);
|
|
732
|
+
wrap.appendChild(targetSel);
|
|
733
|
+
if (existingAnchor) {
|
|
734
|
+
urlInput.value = existingAnchor.getAttribute("href") || "";
|
|
735
|
+
textInput.value = existingAnchor.textContent || "";
|
|
736
|
+
titleInput.value = existingAnchor.getAttribute("title") || "";
|
|
737
|
+
targetSel.value = existingAnchor.getAttribute("target") || "";
|
|
738
|
+
} else if (selectedText) {
|
|
739
|
+
textInput.value = selectedText;
|
|
740
|
+
}
|
|
741
|
+
wrap.appendChild(this._makeSubmitBtn(existingAnchor ? "Update Link" : "Insert Link", () => {
|
|
742
|
+
const href = urlInput.value.trim();
|
|
743
|
+
if (!href)
|
|
744
|
+
return;
|
|
745
|
+
const linkText = textInput.value.trim() || selectedText || href;
|
|
746
|
+
const title = titleInput.value.trim();
|
|
747
|
+
const target = targetSel.value;
|
|
748
|
+
let attrs = `href="${this._esc(href)}"`;
|
|
749
|
+
if (target)
|
|
750
|
+
attrs += ` target="${this._esc(target)}"`;
|
|
751
|
+
if (title)
|
|
752
|
+
attrs += ` title="${this._esc(title)}"`;
|
|
753
|
+
this._restoreRange(this._savedRange);
|
|
754
|
+
this._editor.focus();
|
|
755
|
+
if (existingAnchor) {
|
|
756
|
+
existingAnchor.href = href;
|
|
757
|
+
if (target)
|
|
758
|
+
existingAnchor.target = target;
|
|
759
|
+
else
|
|
760
|
+
existingAnchor.removeAttribute("target");
|
|
761
|
+
if (title)
|
|
762
|
+
existingAnchor.title = title;
|
|
763
|
+
else
|
|
764
|
+
existingAnchor.removeAttribute("title");
|
|
765
|
+
existingAnchor.textContent = linkText;
|
|
766
|
+
} else {
|
|
767
|
+
document.execCommand("insertHTML", false, `<a ${attrs}>${this._esc(linkText)}</a>`);
|
|
768
|
+
}
|
|
769
|
+
this._hidePopup();
|
|
770
|
+
this._emit("change", this.getHTML());
|
|
771
|
+
if (this._options.onChange)
|
|
772
|
+
this._options.onChange(this.getHTML());
|
|
773
|
+
}));
|
|
774
|
+
return wrap;
|
|
775
|
+
}
|
|
776
|
+
/** Fields: URL, alt text, width. Inserts <img> via execCommand('insertHTML'). */
|
|
777
|
+
_popupImage() {
|
|
778
|
+
const wrap = document.createElement("div");
|
|
779
|
+
wrap.className = "jotter-popup-inner jotter-popup-form";
|
|
780
|
+
wrap.appendChild(this._popupTitle("Insert Image"));
|
|
781
|
+
const urlInput = this._makeField(wrap, "Image URL", "text", "https://example.com/image.jpg");
|
|
782
|
+
const altInput = this._makeField(wrap, "Alt text", "text", "Descriptive text");
|
|
783
|
+
const widthInput = this._makeField(wrap, "Width (e.g. 400px or 50%)", "text", "");
|
|
784
|
+
wrap.appendChild(this._makeSubmitBtn("Insert Image", () => {
|
|
785
|
+
const src = urlInput.value.trim();
|
|
786
|
+
if (!src)
|
|
787
|
+
return;
|
|
788
|
+
const alt = altInput.value.trim();
|
|
789
|
+
const w = widthInput.value.trim();
|
|
790
|
+
const style = w ? `max-width:${w}` : "max-width:100%";
|
|
791
|
+
this._restoreRange(this._savedRange);
|
|
792
|
+
this._editor.focus();
|
|
793
|
+
document.execCommand(
|
|
794
|
+
"insertHTML",
|
|
795
|
+
false,
|
|
796
|
+
`<img src="${this._esc(src)}" alt="${this._esc(alt)}" style="${style}">`
|
|
797
|
+
);
|
|
798
|
+
this._hidePopup();
|
|
799
|
+
this._emit("change", this.getHTML());
|
|
800
|
+
if (this._options.onChange)
|
|
801
|
+
this._options.onChange(this.getHTML());
|
|
802
|
+
}));
|
|
803
|
+
return wrap;
|
|
804
|
+
}
|
|
805
|
+
/** Accepts any youtube.com or youtu.be URL; extracts 11-char video ID via _ytId(). */
|
|
806
|
+
_popupVideo() {
|
|
807
|
+
const wrap = document.createElement("div");
|
|
808
|
+
wrap.className = "jotter-popup-inner jotter-popup-form";
|
|
809
|
+
wrap.appendChild(this._popupTitle("Insert YouTube Video"));
|
|
810
|
+
const urlInput = this._makeField(wrap, "YouTube URL", "text", "https://www.youtube.com/watch?v=...");
|
|
811
|
+
wrap.appendChild(this._makeSubmitBtn("Embed Video", () => {
|
|
812
|
+
const id = this._ytId(urlInput.value.trim());
|
|
813
|
+
if (!id) {
|
|
814
|
+
urlInput.classList.add("jotter-input--error");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
urlInput.classList.remove("jotter-input--error");
|
|
818
|
+
const html = `<div class="jotter-video-wrap"><iframe src="https://www.youtube.com/embed/${id}" frameborder="0" allowfullscreen loading="lazy" title="YouTube video"></iframe></div><p><br></p>`;
|
|
819
|
+
this._restoreRange(this._savedRange);
|
|
820
|
+
this._editor.focus();
|
|
821
|
+
document.execCommand("insertHTML", false, html);
|
|
822
|
+
this._hidePopup();
|
|
823
|
+
this._emit("change", this.getHTML());
|
|
824
|
+
if (this._options.onChange)
|
|
825
|
+
this._options.onChange(this.getHTML());
|
|
826
|
+
}));
|
|
827
|
+
return wrap;
|
|
828
|
+
}
|
|
829
|
+
_ytId(url) {
|
|
830
|
+
for (const re of [/[?&]v=([A-Za-z0-9_-]{11})/, /youtu\.be\/([A-Za-z0-9_-]{11})/, /embed\/([A-Za-z0-9_-]{11})/]) {
|
|
831
|
+
const m = url.match(re);
|
|
832
|
+
if (m)
|
|
833
|
+
return m[1];
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
/** Pastes raw HTML/embed code directly; no sanitization (user-supplied trusted content). */
|
|
838
|
+
_popupEmbed() {
|
|
839
|
+
const wrap = document.createElement("div");
|
|
840
|
+
wrap.className = "jotter-popup-inner jotter-popup-form";
|
|
841
|
+
wrap.appendChild(this._popupTitle("Insert Embed"));
|
|
842
|
+
const label = document.createElement("label");
|
|
843
|
+
label.className = "jotter-popup-label";
|
|
844
|
+
label.textContent = "Paste HTML / embed code";
|
|
845
|
+
const ta = document.createElement("textarea");
|
|
846
|
+
ta.className = "jotter-popup-textarea";
|
|
847
|
+
ta.placeholder = '<iframe src="..." ...></iframe>';
|
|
848
|
+
ta.rows = 4;
|
|
849
|
+
wrap.appendChild(label);
|
|
850
|
+
wrap.appendChild(ta);
|
|
851
|
+
wrap.appendChild(this._makeSubmitBtn("Insert", () => {
|
|
852
|
+
const html = ta.value.trim();
|
|
853
|
+
if (!html)
|
|
854
|
+
return;
|
|
855
|
+
this._restoreRange(this._savedRange);
|
|
856
|
+
this._editor.focus();
|
|
857
|
+
document.execCommand("insertHTML", false, html + "<p><br></p>");
|
|
858
|
+
this._hidePopup();
|
|
859
|
+
this._emit("change", this.getHTML());
|
|
860
|
+
if (this._options.onChange)
|
|
861
|
+
this._options.onChange(this.getHTML());
|
|
862
|
+
}));
|
|
863
|
+
return wrap;
|
|
864
|
+
}
|
|
865
|
+
/** Renders SYMBOLS array as a clickable character grid. */
|
|
866
|
+
_popupSymbol() {
|
|
867
|
+
const wrap = document.createElement("div");
|
|
868
|
+
wrap.className = "jotter-popup-inner";
|
|
869
|
+
wrap.appendChild(this._popupTitle("Insert Symbol"));
|
|
870
|
+
wrap.appendChild(this._charGrid(SYMBOLS));
|
|
871
|
+
return wrap;
|
|
872
|
+
}
|
|
873
|
+
/** Renders SPECIAL_CHARS (accented/extended Latin) as a clickable character grid. */
|
|
874
|
+
_popupSpecialChar() {
|
|
875
|
+
const wrap = document.createElement("div");
|
|
876
|
+
wrap.className = "jotter-popup-inner";
|
|
877
|
+
wrap.appendChild(this._popupTitle("Special Characters"));
|
|
878
|
+
wrap.appendChild(this._charGrid(SPECIAL_CHARS));
|
|
879
|
+
return wrap;
|
|
880
|
+
}
|
|
881
|
+
_charGrid(chars) {
|
|
882
|
+
const grid = document.createElement("div");
|
|
883
|
+
grid.className = "jotter-char-grid";
|
|
884
|
+
chars.forEach((ch) => {
|
|
885
|
+
const btn = document.createElement("button");
|
|
886
|
+
btn.type = "button";
|
|
887
|
+
btn.className = "jotter-char-btn";
|
|
888
|
+
btn.textContent = ch;
|
|
889
|
+
btn.title = `U+${ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
|
|
890
|
+
btn.addEventListener("mousedown", (e) => {
|
|
891
|
+
e.preventDefault();
|
|
892
|
+
this._restoreRange(this._savedRange);
|
|
893
|
+
this._editor.focus();
|
|
894
|
+
document.execCommand("insertText", false, ch);
|
|
895
|
+
this._hidePopup();
|
|
896
|
+
this._emit("change", this.getHTML());
|
|
897
|
+
if (this._options.onChange)
|
|
898
|
+
this._options.onChange(this.getHTML());
|
|
899
|
+
});
|
|
900
|
+
grid.appendChild(btn);
|
|
901
|
+
});
|
|
902
|
+
return grid;
|
|
903
|
+
}
|
|
904
|
+
/** Three variants (short/medium/long); long variant inserts as HTML paragraphs. */
|
|
905
|
+
_popupLorem() {
|
|
906
|
+
const wrap = document.createElement("div");
|
|
907
|
+
wrap.className = "jotter-popup-inner";
|
|
908
|
+
wrap.appendChild(this._popupTitle("Insert Lorem Ipsum"));
|
|
909
|
+
LOREM_VARIANTS.forEach((v) => {
|
|
910
|
+
const btn = document.createElement("button");
|
|
911
|
+
btn.type = "button";
|
|
912
|
+
btn.className = "jotter-lorem-btn";
|
|
913
|
+
btn.textContent = v.label;
|
|
914
|
+
btn.addEventListener("mousedown", (e) => {
|
|
915
|
+
e.preventDefault();
|
|
916
|
+
this._restoreRange(this._savedRange);
|
|
917
|
+
this._editor.focus();
|
|
918
|
+
if (v.isHTML) {
|
|
919
|
+
document.execCommand("insertHTML", false, v.text);
|
|
920
|
+
} else {
|
|
921
|
+
document.execCommand("insertText", false, v.text);
|
|
922
|
+
}
|
|
923
|
+
this._hidePopup();
|
|
924
|
+
this._emit("change", this.getHTML());
|
|
925
|
+
if (this._options.onChange)
|
|
926
|
+
this._options.onChange(this.getHTML());
|
|
927
|
+
});
|
|
928
|
+
wrap.appendChild(btn);
|
|
929
|
+
});
|
|
930
|
+
return wrap;
|
|
931
|
+
}
|
|
932
|
+
// ─── Popup helpers ────────────────────────────────────────────────────────
|
|
933
|
+
/** Creates a .jotter-popup-title heading element. */
|
|
934
|
+
_popupTitle(text) {
|
|
935
|
+
const el = document.createElement("div");
|
|
936
|
+
el.className = "jotter-popup-title";
|
|
937
|
+
el.textContent = text;
|
|
938
|
+
return el;
|
|
939
|
+
}
|
|
940
|
+
/** Appends a label+input pair to parent; returns the input element. */
|
|
941
|
+
_makeField(parent, labelText, type, placeholder) {
|
|
942
|
+
const label = document.createElement("label");
|
|
943
|
+
label.className = "jotter-popup-label";
|
|
944
|
+
label.textContent = labelText;
|
|
945
|
+
const input = document.createElement("input");
|
|
946
|
+
input.type = type;
|
|
947
|
+
input.className = "jotter-popup-input";
|
|
948
|
+
input.placeholder = placeholder;
|
|
949
|
+
parent.appendChild(label);
|
|
950
|
+
parent.appendChild(input);
|
|
951
|
+
return input;
|
|
952
|
+
}
|
|
953
|
+
_makeSubmitBtn(text, onClick) {
|
|
954
|
+
const btn = document.createElement("button");
|
|
955
|
+
btn.type = "button";
|
|
956
|
+
btn.className = "jotter-popup-submit";
|
|
957
|
+
btn.textContent = text;
|
|
958
|
+
btn.addEventListener("click", onClick);
|
|
959
|
+
return btn;
|
|
960
|
+
}
|
|
961
|
+
/** Escapes ", <, > for safe insertion into HTML attribute values and text. */
|
|
962
|
+
_esc(s) {
|
|
963
|
+
return s.replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
964
|
+
}
|
|
965
|
+
// ─── Status bar ───────────────────────────────────────────────────────────
|
|
966
|
+
_buildStatusBar() {
|
|
967
|
+
const bar = document.createElement("div");
|
|
968
|
+
bar.className = "jotter-status";
|
|
969
|
+
this._wordCountEl = document.createElement("span");
|
|
970
|
+
this._wordCountEl.className = "jotter-status-words";
|
|
971
|
+
this._charCountEl = document.createElement("span");
|
|
972
|
+
this._charCountEl.className = "jotter-status-chars";
|
|
973
|
+
const modeEl = document.createElement("span");
|
|
974
|
+
modeEl.className = "jotter-status-mode";
|
|
975
|
+
modeEl.textContent = "HTML";
|
|
976
|
+
bar.appendChild(this._wordCountEl);
|
|
977
|
+
bar.appendChild(this._charCountEl);
|
|
978
|
+
bar.appendChild(modeEl);
|
|
979
|
+
return bar;
|
|
980
|
+
}
|
|
981
|
+
// ─── Events ───────────────────────────────────────────────────────────────
|
|
982
|
+
_bindEvents() {
|
|
983
|
+
this._editor.addEventListener("input", () => {
|
|
984
|
+
this._updateStatus();
|
|
985
|
+
this._emit("change", this.getHTML());
|
|
986
|
+
if (this._options.onChange)
|
|
987
|
+
this._options.onChange(this.getHTML());
|
|
988
|
+
});
|
|
989
|
+
this._editor.addEventListener("keyup", () => this._updateToolbarState());
|
|
990
|
+
this._editor.addEventListener("mouseup", () => this._updateToolbarState());
|
|
991
|
+
this._editor.addEventListener("focus", () => {
|
|
992
|
+
this._root.classList.add("jotter--focused");
|
|
993
|
+
this._emit("focus");
|
|
994
|
+
if (this._options.onFocus)
|
|
995
|
+
this._options.onFocus();
|
|
996
|
+
});
|
|
997
|
+
this._editor.addEventListener("blur", () => {
|
|
998
|
+
this._root.classList.remove("jotter--focused");
|
|
999
|
+
this._emit("blur");
|
|
1000
|
+
if (this._options.onBlur)
|
|
1001
|
+
this._options.onBlur();
|
|
1002
|
+
});
|
|
1003
|
+
this._editor.addEventListener("keydown", (e) => {
|
|
1004
|
+
if (e.key === "Tab") {
|
|
1005
|
+
e.preventDefault();
|
|
1006
|
+
document.execCommand("insertHTML", false, " ");
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
document.addEventListener("mousedown", (e) => {
|
|
1010
|
+
if (this._popup.classList.contains("jotter-popup--visible") && !this._popup.contains(e.target) && !this._toolbarEl.contains(e.target)) {
|
|
1011
|
+
this._hidePopup();
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
document.addEventListener("keydown", (e) => {
|
|
1015
|
+
if (e.key === "Escape" && this._popup.classList.contains("jotter-popup--visible")) {
|
|
1016
|
+
this._hidePopup();
|
|
1017
|
+
this._editor.focus();
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
// ─── Custom commands ──────────────────────────────────────────────────────
|
|
1022
|
+
/**
|
|
1023
|
+
* Toggles between rich-text (contenteditable) and raw HTML (textarea) views.
|
|
1024
|
+
* Entering source mode: pretty-prints innerHTML into the textarea.
|
|
1025
|
+
* Leaving source mode: sanitizes textarea value back into innerHTML.
|
|
1026
|
+
*/
|
|
1027
|
+
_toggleSourceMode() {
|
|
1028
|
+
this._sourceMode = !this._sourceMode;
|
|
1029
|
+
if (this._sourceMode) {
|
|
1030
|
+
this._source.value = this._prettyHTML(this._editor.innerHTML);
|
|
1031
|
+
this._editor.style.display = "none";
|
|
1032
|
+
this._source.style.display = "block";
|
|
1033
|
+
} else {
|
|
1034
|
+
this._editor.innerHTML = this._sanitize(this._source.value);
|
|
1035
|
+
this._source.style.display = "none";
|
|
1036
|
+
this._editor.style.display = "";
|
|
1037
|
+
this._updateStatus();
|
|
1038
|
+
this._emit("change", this.getHTML());
|
|
1039
|
+
if (this._options.onChange)
|
|
1040
|
+
this._options.onChange(this.getHTML());
|
|
1041
|
+
}
|
|
1042
|
+
this._root.classList.toggle("jotter--source-mode", this._sourceMode);
|
|
1043
|
+
const btn = this._toolbarEl.querySelector('[data-custom="toggleSource"]');
|
|
1044
|
+
if (btn)
|
|
1045
|
+
btn.classList.toggle("jotter-btn--active", this._sourceMode);
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* Minimal HTML formatter for source-mode display.
|
|
1049
|
+
* Algorithm: collapse inter-tag whitespace → split on tag boundaries →
|
|
1050
|
+
* track indent depth, skipping void and inline elements.
|
|
1051
|
+
*/
|
|
1052
|
+
_prettyHTML(html) {
|
|
1053
|
+
let indent = 0;
|
|
1054
|
+
const INDENT = " ";
|
|
1055
|
+
const VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
|
|
1056
|
+
const INLINE = /* @__PURE__ */ new Set(["a", "abbr", "acronym", "b", "bdo", "big", "br", "button", "cite", "code", "dfn", "em", "i", "img", "input", "kbd", "label", "map", "object", "output", "q", "samp", "select", "small", "span", "strong", "sub", "sup", "textarea", "time", "tt", "u", "var"]);
|
|
1057
|
+
return html.replace(/>\s+</g, "><").replace(/(<[^>]+>)/g, "\n$1\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).map((line) => {
|
|
1058
|
+
const closeMatch = line.match(/^<\/(\w+)/);
|
|
1059
|
+
const openMatch = line.match(/^<(\w+)/);
|
|
1060
|
+
const selfClose = line.endsWith("/>");
|
|
1061
|
+
const tag = openMatch ? openMatch[1].toLowerCase() : null;
|
|
1062
|
+
const closeTag = closeMatch ? closeMatch[1].toLowerCase() : null;
|
|
1063
|
+
if (closeTag && !INLINE.has(closeTag))
|
|
1064
|
+
indent = Math.max(0, indent - 1);
|
|
1065
|
+
const out = INDENT.repeat(indent) + line;
|
|
1066
|
+
if (tag && !selfClose && !VOID.has(tag) && !closeTag && !INLINE.has(tag))
|
|
1067
|
+
indent++;
|
|
1068
|
+
return out;
|
|
1069
|
+
}).join("\n");
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* XSS sanitizer using an inert DOMParser document (scripts never execute).
|
|
1073
|
+
* Removes: <script> elements, on* event attributes, javascript: URLs on
|
|
1074
|
+
* href/src/action/formaction/data attributes.
|
|
1075
|
+
*/
|
|
1076
|
+
_sanitize(html) {
|
|
1077
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
1078
|
+
doc.querySelectorAll("script").forEach((el) => el.remove());
|
|
1079
|
+
doc.querySelectorAll("*").forEach((el) => {
|
|
1080
|
+
Array.from(el.attributes).forEach((attr) => {
|
|
1081
|
+
if (attr.name.startsWith("on")) {
|
|
1082
|
+
el.removeAttribute(attr.name);
|
|
1083
|
+
} else if (["href", "src", "action", "formaction", "data"].includes(attr.name)) {
|
|
1084
|
+
if (/^\s*javascript:/i.test(attr.value))
|
|
1085
|
+
el.removeAttribute(attr.name);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
return doc.body.innerHTML;
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Toggles <code> wrapper on selection.
|
|
1093
|
+
* If caret is inside <code>: unwraps (moves children to parent).
|
|
1094
|
+
* If selection exists: wraps with surroundContents (fallback: extract+wrap).
|
|
1095
|
+
* If collapsed: inserts empty <code> with zero-width space and selects it.
|
|
1096
|
+
*/
|
|
1097
|
+
_toggleInlineCode() {
|
|
1098
|
+
const sel = window.getSelection();
|
|
1099
|
+
if (!sel || sel.rangeCount === 0)
|
|
1100
|
+
return;
|
|
1101
|
+
const range = sel.getRangeAt(0);
|
|
1102
|
+
let node = range.commonAncestorContainer;
|
|
1103
|
+
if (node.nodeType === 3)
|
|
1104
|
+
node = node.parentNode;
|
|
1105
|
+
const codeEl = node.closest ? node.closest("code") : null;
|
|
1106
|
+
if (codeEl) {
|
|
1107
|
+
const parent = codeEl.parentNode;
|
|
1108
|
+
while (codeEl.firstChild)
|
|
1109
|
+
parent.insertBefore(codeEl.firstChild, codeEl);
|
|
1110
|
+
parent.removeChild(codeEl);
|
|
1111
|
+
} else if (!range.collapsed) {
|
|
1112
|
+
const code = document.createElement("code");
|
|
1113
|
+
try {
|
|
1114
|
+
range.surroundContents(code);
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
const frag = range.extractContents();
|
|
1117
|
+
code.appendChild(frag);
|
|
1118
|
+
range.insertNode(code);
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
const code = document.createElement("code");
|
|
1122
|
+
code.innerHTML = "​";
|
|
1123
|
+
range.insertNode(code);
|
|
1124
|
+
const r2 = document.createRange();
|
|
1125
|
+
r2.setStart(code, 0);
|
|
1126
|
+
r2.setEnd(code, code.childNodes.length);
|
|
1127
|
+
sel.removeAllRanges();
|
|
1128
|
+
sel.addRange(r2);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Wraps selection in <span style="font-size: Xpx">.
|
|
1133
|
+
* Uses Range API instead of execCommand('fontSize') to avoid the legacy 1-7 scale.
|
|
1134
|
+
*/
|
|
1135
|
+
_applyFontSize(px) {
|
|
1136
|
+
const sel = window.getSelection();
|
|
1137
|
+
if (!sel || sel.rangeCount === 0)
|
|
1138
|
+
return;
|
|
1139
|
+
const range = sel.getRangeAt(0);
|
|
1140
|
+
if (range.collapsed)
|
|
1141
|
+
return;
|
|
1142
|
+
const span = document.createElement("span");
|
|
1143
|
+
span.style.fontSize = px + "px";
|
|
1144
|
+
try {
|
|
1145
|
+
range.surroundContents(span);
|
|
1146
|
+
} catch (e) {
|
|
1147
|
+
const frag = range.extractContents();
|
|
1148
|
+
span.appendChild(frag);
|
|
1149
|
+
range.insertNode(span);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Clipboard paste via Clipboard API (requires user permission prompt).
|
|
1154
|
+
* Falls back to execCommand('paste') which most browsers block silently.
|
|
1155
|
+
* Silent catch: user can always use Ctrl+V as a native fallback.
|
|
1156
|
+
*/
|
|
1157
|
+
async _pasteFromClipboard() {
|
|
1158
|
+
try {
|
|
1159
|
+
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
1160
|
+
const text = await navigator.clipboard.readText();
|
|
1161
|
+
document.execCommand("insertText", false, text);
|
|
1162
|
+
} else {
|
|
1163
|
+
document.execCommand("paste");
|
|
1164
|
+
}
|
|
1165
|
+
} catch (_) {
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// ─── Toolbar state ────────────────────────────────────────────────────────
|
|
1169
|
+
/**
|
|
1170
|
+
* Syncs .jotter-btn--active and blockformat select to current cursor context.
|
|
1171
|
+
* Uses queryCommandState (bold/italic/etc.) and queryCommandValue (formatBlock).
|
|
1172
|
+
*/
|
|
1173
|
+
_updateToolbarState() {
|
|
1174
|
+
this._toolbarEl.querySelectorAll(".jotter-btn[data-cmd]").forEach((btn) => {
|
|
1175
|
+
try {
|
|
1176
|
+
btn.classList.toggle("jotter-btn--active", document.queryCommandState(btn.dataset.cmd));
|
|
1177
|
+
} catch (_) {
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
const bfSel = this._toolbarEl.querySelector('[data-id="blockformat"]');
|
|
1181
|
+
if (bfSel) {
|
|
1182
|
+
const block = document.queryCommandValue("formatBlock").toLowerCase().replace(/[<>]/g, "");
|
|
1183
|
+
const match = HEADING_OPTIONS.find((o) => o.tag === block);
|
|
1184
|
+
if (match)
|
|
1185
|
+
bfSel.value = match.tag;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
_updateStatus() {
|
|
1189
|
+
const text = this._editor.innerText || "";
|
|
1190
|
+
const words = text.trim() === "" ? 0 : text.trim().split(/\s+/).length;
|
|
1191
|
+
const chars = text.replace(/\n/g, "").length;
|
|
1192
|
+
this._wordCountEl.textContent = `${words} word${words !== 1 ? "s" : ""}`;
|
|
1193
|
+
this._charCountEl.textContent = `${chars} char${chars !== 1 ? "s" : ""}`;
|
|
1194
|
+
}
|
|
1195
|
+
/** Clones current selection range before a toolbar interaction steals focus. */
|
|
1196
|
+
_saveRange() {
|
|
1197
|
+
const sel = window.getSelection();
|
|
1198
|
+
return sel && sel.rangeCount > 0 ? sel.getRangeAt(0).cloneRange() : null;
|
|
1199
|
+
}
|
|
1200
|
+
/** Restores a previously saved range so execCommand targets the original selection. */
|
|
1201
|
+
_restoreRange(range) {
|
|
1202
|
+
if (!range)
|
|
1203
|
+
return;
|
|
1204
|
+
const sel = window.getSelection();
|
|
1205
|
+
sel.removeAllRanges();
|
|
1206
|
+
sel.addRange(range);
|
|
1207
|
+
}
|
|
1208
|
+
_emit(event, data) {
|
|
1209
|
+
(this._listeners[event] || []).forEach((fn) => fn(data));
|
|
1210
|
+
}
|
|
1211
|
+
// ─── Public API ─────────────────────────────────────────────────────────
|
|
1212
|
+
// See class-level JSDoc for full method signatures.
|
|
1213
|
+
getHTML() {
|
|
1214
|
+
return this._sourceMode ? this._source.value : this._editor.innerHTML;
|
|
1215
|
+
}
|
|
1216
|
+
getText() {
|
|
1217
|
+
return this._editor.innerText;
|
|
1218
|
+
}
|
|
1219
|
+
isSourceMode() {
|
|
1220
|
+
return this._sourceMode;
|
|
1221
|
+
}
|
|
1222
|
+
toggleSource() {
|
|
1223
|
+
this._toggleSourceMode();
|
|
1224
|
+
return this;
|
|
1225
|
+
}
|
|
1226
|
+
clear() {
|
|
1227
|
+
this._editor.innerHTML = "";
|
|
1228
|
+
this._updateStatus();
|
|
1229
|
+
return this;
|
|
1230
|
+
}
|
|
1231
|
+
focus() {
|
|
1232
|
+
this._editor.focus();
|
|
1233
|
+
return this;
|
|
1234
|
+
}
|
|
1235
|
+
insertHTML(html) {
|
|
1236
|
+
this._editor.focus();
|
|
1237
|
+
document.execCommand("insertHTML", false, this._sanitize(html));
|
|
1238
|
+
this._updateStatus();
|
|
1239
|
+
this._emit("change", this.getHTML());
|
|
1240
|
+
if (this._options.onChange)
|
|
1241
|
+
this._options.onChange(this.getHTML());
|
|
1242
|
+
return this;
|
|
1243
|
+
}
|
|
1244
|
+
insertText(text) {
|
|
1245
|
+
this._editor.focus();
|
|
1246
|
+
document.execCommand("insertText", false, text);
|
|
1247
|
+
this._updateStatus();
|
|
1248
|
+
this._emit("change", this.getHTML());
|
|
1249
|
+
if (this._options.onChange)
|
|
1250
|
+
this._options.onChange(this.getHTML());
|
|
1251
|
+
return this;
|
|
1252
|
+
}
|
|
1253
|
+
setHTML(html) {
|
|
1254
|
+
this._editor.innerHTML = this._sanitize(html);
|
|
1255
|
+
this._updateStatus();
|
|
1256
|
+
return this;
|
|
1257
|
+
}
|
|
1258
|
+
setTheme(name) {
|
|
1259
|
+
const valid = THEMES.find((t) => t.id === name);
|
|
1260
|
+
const id = valid ? name : "default";
|
|
1261
|
+
this._editor.dataset.theme = id;
|
|
1262
|
+
this._options.theme = id;
|
|
1263
|
+
if (this._themeSelect)
|
|
1264
|
+
this._themeSelect.value = id;
|
|
1265
|
+
return this;
|
|
1266
|
+
}
|
|
1267
|
+
setEnabled(enabled) {
|
|
1268
|
+
this._editor.contentEditable = String(enabled);
|
|
1269
|
+
this._root.classList.toggle("jotter--disabled", !enabled);
|
|
1270
|
+
return this;
|
|
1271
|
+
}
|
|
1272
|
+
on(event, fn) {
|
|
1273
|
+
if (!this._listeners[event])
|
|
1274
|
+
this._listeners[event] = [];
|
|
1275
|
+
this._listeners[event].push(fn);
|
|
1276
|
+
return this;
|
|
1277
|
+
}
|
|
1278
|
+
off(event, fn) {
|
|
1279
|
+
if (this._listeners[event])
|
|
1280
|
+
this._listeners[event] = this._listeners[event].filter((f) => f !== fn);
|
|
1281
|
+
return this;
|
|
1282
|
+
}
|
|
1283
|
+
/** Unmounts editor, restores original element innerHTML, removes body popup, clears listeners. */
|
|
1284
|
+
destroy() {
|
|
1285
|
+
const html = this.getHTML();
|
|
1286
|
+
this._hidePopup();
|
|
1287
|
+
if (this._popup.parentNode)
|
|
1288
|
+
this._popup.parentNode.removeChild(this._popup);
|
|
1289
|
+
this._target.classList.remove("jotter-host");
|
|
1290
|
+
this._target.innerHTML = html;
|
|
1291
|
+
this._listeners = {};
|
|
1292
|
+
return html;
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
JotterJS.toolbar = TOOLBAR_ACTIONS;
|
|
1296
|
+
JotterJS.actions = {
|
|
1297
|
+
source: { custom: "toggleSource", label: "Source", title: "Edit HTML Source" },
|
|
1298
|
+
sep: { type: "sep" },
|
|
1299
|
+
blockformat: { type: "blockformat" },
|
|
1300
|
+
fontfamily: { type: "fontfamily" },
|
|
1301
|
+
fontsize: { type: "fontsize" },
|
|
1302
|
+
theme: { type: "theme" },
|
|
1303
|
+
undo: { cmd: "undo", icon: "undo", title: "Undo (Ctrl+Z)" },
|
|
1304
|
+
redo: { cmd: "redo", icon: "redo", title: "Redo (Ctrl+Y)" },
|
|
1305
|
+
bold: { cmd: "bold", icon: "format_bold", title: "Bold (Ctrl+B)" },
|
|
1306
|
+
italic: { cmd: "italic", icon: "format_italic", title: "Italic (Ctrl+I)" },
|
|
1307
|
+
underline: { cmd: "underline", icon: "format_underlined", title: "Underline (Ctrl+U)" },
|
|
1308
|
+
strike: { cmd: "strikeThrough", icon: "strikethrough_s", title: "Strikethrough" },
|
|
1309
|
+
subscript: { cmd: "subscript", icon: "subscript", title: "Subscript" },
|
|
1310
|
+
superscript: { cmd: "superscript", icon: "superscript", title: "Superscript" },
|
|
1311
|
+
code: { custom: "code", icon: "code", title: "Inline Code" },
|
|
1312
|
+
copy: { cmd: "copy", icon: "content_copy", title: "Copy" },
|
|
1313
|
+
cut: { cmd: "cut", icon: "content_cut", title: "Cut" },
|
|
1314
|
+
paste: { cmd: "paste", icon: "content_paste", title: "Paste" },
|
|
1315
|
+
clearFormat: { cmd: "removeFormat", icon: "format_clear", title: "Clear Formatting" },
|
|
1316
|
+
alignLeft: { cmd: "justifyLeft", icon: "format_align_left", title: "Align Left" },
|
|
1317
|
+
alignCenter: { cmd: "justifyCenter", icon: "format_align_center", title: "Align Center" },
|
|
1318
|
+
alignRight: { cmd: "justifyRight", icon: "format_align_right", title: "Align Right" },
|
|
1319
|
+
bullets: { cmd: "insertUnorderedList", icon: "format_list_bulleted", title: "Bullet List" },
|
|
1320
|
+
numbered: { cmd: "insertOrderedList", icon: "format_list_numbered", title: "Numbered List" },
|
|
1321
|
+
link: { type: "popup", id: "link", icon: "insert_link", title: "Insert Link" },
|
|
1322
|
+
unlink: { cmd: "unlink", icon: "link_off", title: "Remove Link" },
|
|
1323
|
+
foreColor: { type: "color", cmd: "foreColor", icon: "format_color_text", title: "Text Color" },
|
|
1324
|
+
hiliteColor: { type: "color", cmd: "hiliteColor", icon: "format_color_fill", title: "Background Color" },
|
|
1325
|
+
image: { type: "popup", id: "image", icon: "image", title: "Insert Image" },
|
|
1326
|
+
video: { type: "popup", id: "video", icon: "smart_display", title: "Insert YouTube Video" },
|
|
1327
|
+
table: { type: "popup", id: "table", icon: "table_chart", title: "Insert Table" },
|
|
1328
|
+
embed: { type: "popup", id: "embed", icon: "html", title: "Insert Embed" },
|
|
1329
|
+
symbol: { type: "popup", id: "symbol", icon: "emoji_symbols", title: "Insert Symbol" },
|
|
1330
|
+
specialChar: { type: "popup", id: "specialchar", icon: "format_shapes", title: "Special Characters" },
|
|
1331
|
+
lorem: { type: "popup", id: "lorem", icon: "article", title: "Insert Lorem Ipsum" }
|
|
1332
|
+
};
|
|
1333
|
+
JotterJS.presets = {
|
|
1334
|
+
minimal: [
|
|
1335
|
+
{ custom: "toggleSource", label: "Source", title: "Edit HTML Source" },
|
|
1336
|
+
{ type: "sep" },
|
|
1337
|
+
{ cmd: "bold", icon: "format_bold", title: "Bold" },
|
|
1338
|
+
{ cmd: "italic", icon: "format_italic", title: "Italic" },
|
|
1339
|
+
{ cmd: "underline", icon: "format_underlined", title: "Underline" },
|
|
1340
|
+
{ type: "sep" },
|
|
1341
|
+
{ type: "popup", id: "link", icon: "insert_link", title: "Insert Link" },
|
|
1342
|
+
{ cmd: "unlink", icon: "link_off", title: "Remove Link" }
|
|
1343
|
+
],
|
|
1344
|
+
writing: [
|
|
1345
|
+
{ custom: "toggleSource", label: "Source", title: "Edit HTML Source" },
|
|
1346
|
+
{ type: "sep" },
|
|
1347
|
+
{ cmd: "undo", icon: "undo", title: "Undo" },
|
|
1348
|
+
{ cmd: "redo", icon: "redo", title: "Redo" },
|
|
1349
|
+
{ type: "sep" },
|
|
1350
|
+
{ type: "blockformat" },
|
|
1351
|
+
{ type: "sep" },
|
|
1352
|
+
{ cmd: "bold", icon: "format_bold", title: "Bold" },
|
|
1353
|
+
{ cmd: "italic", icon: "format_italic", title: "Italic" },
|
|
1354
|
+
{ cmd: "underline", icon: "format_underlined", title: "Underline" },
|
|
1355
|
+
{ cmd: "strikeThrough", icon: "strikethrough_s", title: "Strikethrough" },
|
|
1356
|
+
{ type: "sep" },
|
|
1357
|
+
{ cmd: "insertUnorderedList", icon: "format_list_bulleted", title: "Bullet List" },
|
|
1358
|
+
{ cmd: "insertOrderedList", icon: "format_list_numbered", title: "Ordered List" },
|
|
1359
|
+
{ type: "sep" },
|
|
1360
|
+
{ type: "popup", id: "link", icon: "insert_link", title: "Insert Link" },
|
|
1361
|
+
{ cmd: "unlink", icon: "link_off", title: "Remove Link" },
|
|
1362
|
+
{ type: "sep" },
|
|
1363
|
+
{ type: "popup", id: "image", icon: "image", title: "Insert Image" }
|
|
1364
|
+
]
|
|
1365
|
+
};
|
|
1366
|
+
var jotter_default = JotterJS;
|
|
1367
|
+
export {
|
|
1368
|
+
JotterJS,
|
|
1369
|
+
jotter_default as default
|
|
1370
|
+
};
|