sdocs-dev 1.3.0 → 1.3.1

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.
@@ -1,740 +0,0 @@
1
- // sdocs-write.js — Write mode: contentEditable WYSIWYG with markdown shortcuts
2
- (function () {
3
- 'use strict';
4
-
5
- var S = SDocs;
6
- var writeEl = document.getElementById('write');
7
- S.writeEl = writeEl;
8
-
9
- // ── Enter / exit write mode ──────────────────────────
10
-
11
- function enterWriteMode() {
12
- var html = DOMPurify.sanitize(marked.parse(S.currentBody));
13
- writeEl.innerHTML = html || '<p><br></p>';
14
- copyStyleVars();
15
- setTimeout(updateToolbarState, 0);
16
- }
17
-
18
- function exitWriteMode() {
19
- S.currentBody = htmlToMarkdown(writeEl);
20
- S.render();
21
- S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
22
- S.rawEl.value = SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
23
- }
24
-
25
- function copyStyleVars() {
26
- var style = S.renderedEl.style;
27
- for (var i = 0; i < style.length; i++) {
28
- var prop = style[i];
29
- if (prop.startsWith('--md-')) {
30
- writeEl.style.setProperty(prop, style.getPropertyValue(prop));
31
- }
32
- }
33
- }
34
-
35
- // ── HTML-to-Markdown conversion ──────────────────────
36
-
37
- function htmlToMarkdown(container) {
38
- var lines = [];
39
- walkBlock(container.childNodes, lines, '');
40
- return lines.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
41
- }
42
-
43
- function walkBlock(nodes, lines, indent) {
44
- for (var i = 0; i < nodes.length; i++) {
45
- var node = nodes[i];
46
- if (node.nodeType === 3) {
47
- var text = node.textContent.trim();
48
- if (text) lines.push(indent + text);
49
- continue;
50
- }
51
- if (node.nodeType !== 1) continue;
52
- var tag = node.tagName;
53
-
54
- if (/^H[1-6]$/.test(tag)) {
55
- var level = parseInt(tag[1]);
56
- var hashes = '';
57
- for (var h = 0; h < level; h++) hashes += '#';
58
- lines.push('');
59
- lines.push(hashes + ' ' + inlineToMd(node));
60
- lines.push('');
61
- } else if (tag === 'P') {
62
- lines.push('');
63
- lines.push(indent + inlineToMd(node));
64
- lines.push('');
65
- } else if (tag === 'UL') {
66
- lines.push('');
67
- walkList(node, lines, indent, 'ul');
68
- lines.push('');
69
- } else if (tag === 'OL') {
70
- lines.push('');
71
- walkList(node, lines, indent, 'ol');
72
- lines.push('');
73
- } else if (tag === 'BLOCKQUOTE') {
74
- lines.push('');
75
- var bqLines = [];
76
- walkBlock(node.childNodes, bqLines, '');
77
- for (var b = 0; b < bqLines.length; b++) {
78
- lines.push('> ' + bqLines[b]);
79
- }
80
- lines.push('');
81
- } else if (tag === 'PRE') {
82
- var codeEl = node.querySelector('code');
83
- var lang = '';
84
- if (codeEl) {
85
- var cls = codeEl.className || '';
86
- var m = cls.match(/language-(\S+)/);
87
- if (m) lang = m[1];
88
- }
89
- lines.push('');
90
- lines.push('```' + lang);
91
- lines.push((codeEl || node).textContent);
92
- lines.push('```');
93
- lines.push('');
94
- } else if (tag === 'HR') {
95
- lines.push('');
96
- lines.push('---');
97
- lines.push('');
98
- } else if (tag === 'BR') {
99
- lines.push('');
100
- } else if (tag === 'DIV') {
101
- // contentEditable often wraps lines in divs
102
- if (node.querySelector('h1,h2,h3,h4,h5,h6,p,ul,ol,pre,blockquote')) {
103
- walkBlock(node.childNodes, lines, indent);
104
- } else {
105
- lines.push('');
106
- lines.push(indent + inlineToMd(node));
107
- lines.push('');
108
- }
109
- } else {
110
- // Unknown block — recurse
111
- walkBlock(node.childNodes, lines, indent);
112
- }
113
- }
114
- }
115
-
116
- function walkList(listEl, lines, indent, type) {
117
- var items = listEl.children;
118
- var num = 1;
119
- for (var i = 0; i < items.length; i++) {
120
- if (items[i].tagName !== 'LI') continue;
121
- var li = items[i];
122
- var bullet = type === 'ul' ? '- ' : (num++) + '. ';
123
- var text = '';
124
- var subLists = [];
125
- for (var j = 0; j < li.childNodes.length; j++) {
126
- var child = li.childNodes[j];
127
- if (child.nodeType === 1 && (child.tagName === 'UL' || child.tagName === 'OL')) {
128
- subLists.push(child);
129
- } else if (child.nodeType === 1) {
130
- text += inlineToMd(child);
131
- } else if (child.nodeType === 3) {
132
- text += child.textContent;
133
- }
134
- }
135
- lines.push(indent + bullet + text.trim());
136
- for (var k = 0; k < subLists.length; k++) {
137
- walkList(subLists[k], lines, indent + ' ', subLists[k].tagName === 'UL' ? 'ul' : 'ol');
138
- }
139
- }
140
- }
141
-
142
- function inlineToMd(node) {
143
- var result = '';
144
- for (var i = 0; i < node.childNodes.length; i++) {
145
- var child = node.childNodes[i];
146
- if (child.nodeType === 3) {
147
- result += child.textContent;
148
- } else if (child.nodeType === 1) {
149
- var tag = child.tagName;
150
- if (tag === 'STRONG' || tag === 'B') {
151
- result += '**' + inlineToMd(child) + '**';
152
- } else if (tag === 'EM' || tag === 'I') {
153
- result += '*' + inlineToMd(child) + '*';
154
- } else if (tag === 'S' || tag === 'STRIKE' || tag === 'DEL') {
155
- result += '~~' + inlineToMd(child) + '~~';
156
- } else if (tag === 'CODE') {
157
- result += '`' + child.textContent + '`';
158
- } else if (tag === 'A') {
159
- var href = child.getAttribute('href') || '';
160
- result += '[' + inlineToMd(child) + '](' + href + ')';
161
- } else if (tag === 'IMG') {
162
- var alt = child.getAttribute('alt') || '';
163
- var src = child.getAttribute('src') || '';
164
- result += '![' + alt + '](' + src + ')';
165
- } else if (tag === 'BR') {
166
- result += ' \n';
167
- } else {
168
- result += inlineToMd(child);
169
- }
170
- }
171
- }
172
- return result;
173
- }
174
-
175
- // ── Cursor helpers ──────────────────────────────────
176
-
177
- function placeCursorAtEnd(el) {
178
- var range = document.createRange();
179
- range.selectNodeContents(el);
180
- range.collapse(false);
181
- var sel = window.getSelection();
182
- sel.removeAllRanges();
183
- sel.addRange(range);
184
- }
185
-
186
- function getContainingBlock(node) {
187
- while (node && node !== writeEl) {
188
- if (node.nodeType === 1) {
189
- var display = getComputedStyle(node).display;
190
- if (display === 'block' || display === 'list-item') return node;
191
- }
192
- node = node.parentNode;
193
- }
194
- return null;
195
- }
196
-
197
- // ── Markdown shortcuts ──────────────────────────────
198
-
199
- function checkShortcuts() {
200
- var sel = window.getSelection();
201
- if (!sel.rangeCount) return;
202
- var block = getContainingBlock(sel.anchorNode);
203
- if (!block || block === writeEl) return;
204
- // Only transform simple paragraphs/divs (not already formatted blocks)
205
- if (block.tagName !== 'P' && block.tagName !== 'DIV') return;
206
- var text = block.textContent;
207
-
208
- // Heading: # through ######
209
- var hm = text.match(/^(#{1,6})\s(.+)$/);
210
- if (hm) {
211
- var lvl = hm[1].length;
212
- var heading = document.createElement('h' + lvl);
213
- heading.textContent = hm[2];
214
- block.replaceWith(heading);
215
- placeCursorAtEnd(heading);
216
- return;
217
- }
218
-
219
- // Horizontal rule: ---
220
- if (/^---$/.test(text.trim())) {
221
- var hr = document.createElement('hr');
222
- var p = document.createElement('p');
223
- p.innerHTML = '<br>';
224
- block.replaceWith(hr);
225
- hr.after(p);
226
- placeCursorAtEnd(p);
227
- return;
228
- }
229
-
230
- // Code block: ```
231
- if (/^```(\w*)$/.test(text.trim())) {
232
- var langMatch = text.trim().match(/^```(\w*)$/);
233
- var pre = document.createElement('pre');
234
- var code = document.createElement('code');
235
- if (langMatch[1]) code.className = 'language-' + langMatch[1];
236
- code.textContent = '\n';
237
- pre.appendChild(code);
238
- block.replaceWith(pre);
239
- placeCursorAtEnd(code);
240
- return;
241
- }
242
-
243
- // Blockquote: >
244
- var bqm = text.match(/^>\s(.*)$/);
245
- if (bqm) {
246
- var bq = document.createElement('blockquote');
247
- var bqp = document.createElement('p');
248
- bqp.textContent = bqm[1];
249
- bq.appendChild(bqp);
250
- block.replaceWith(bq);
251
- placeCursorAtEnd(bqp);
252
- return;
253
- }
254
-
255
- // Unordered list: - or *
256
- var ulm = text.match(/^[-*]\s(.*)$/);
257
- if (ulm) {
258
- var ul = document.createElement('ul');
259
- var li = document.createElement('li');
260
- li.textContent = ulm[1];
261
- ul.appendChild(li);
262
- block.replaceWith(ul);
263
- placeCursorAtEnd(li);
264
- return;
265
- }
266
-
267
- // Ordered list: 1.
268
- var olm = text.match(/^(\d+)\.\s(.*)$/);
269
- if (olm) {
270
- var ol = document.createElement('ol');
271
- var oli = document.createElement('li');
272
- oli.textContent = olm[2];
273
- ol.appendChild(oli);
274
- block.replaceWith(ol);
275
- placeCursorAtEnd(oli);
276
- return;
277
- }
278
- }
279
-
280
- // ── Input handler + debounced sync ──────────────────
281
-
282
- writeEl.addEventListener('input', function(e) {
283
- // Content has diverged from the on-disk file — drop local paths.
284
- if (S.invalidateLocalMeta) S.invalidateLocalMeta();
285
-
286
- // Debounce sync
287
- clearTimeout(S._writeSyncTimer);
288
- S._writeSyncTimer = setTimeout(function() {
289
- S.currentBody = htmlToMarkdown(writeEl);
290
- S.syncAll('write');
291
- }, 500);
292
-
293
- // Code block exit: after Enter inserts a line break, check for 2+ consecutive
294
- // empty lines at the end. Chromium represents newlines as <br> elements in
295
- // contentEditable, with an extra trailing BR as a caret placeholder.
296
- // Pattern: N enters = N+1 trailing BRs. So 2 empty Enters = 3+ trailing BRs.
297
- if (S._checkCodeBlockExit) {
298
- var pre = S._checkCodeBlockExit;
299
- S._checkCodeBlockExit = null;
300
- var codeEl = pre.querySelector('code') || pre;
301
- var children = codeEl.childNodes;
302
- var trailingBRs = 0;
303
- for (var ci = children.length - 1; ci >= 0; ci--) {
304
- var child = children[ci];
305
- if (child.nodeType === 1 && child.tagName === 'BR') { trailingBRs++; continue; }
306
- // Skip empty or whitespace-only text nodes (e.g. trailing \n from initialization)
307
- if (child.nodeType === 3 && !child.textContent.trim()) { continue; }
308
- break;
309
- }
310
- // 3+ trailing BRs = 2+ empty Enters at end → exit code block
311
- if (trailingBRs >= 3) {
312
- // Remove all trailing BRs
313
- while (codeEl.lastChild && codeEl.lastChild.nodeType === 1 && codeEl.lastChild.tagName === 'BR') {
314
- codeEl.removeChild(codeEl.lastChild);
315
- }
316
- // Clean trailing text newlines too
317
- if (codeEl.lastChild && codeEl.lastChild.nodeType === 3) {
318
- codeEl.lastChild.textContent = codeEl.lastChild.textContent.replace(/\n+$/, '');
319
- }
320
- // Ensure code block isn't completely empty
321
- if (!codeEl.textContent && !codeEl.querySelector('br')) {
322
- codeEl.textContent = '\n';
323
- }
324
- var exitP = document.createElement('p');
325
- exitP.innerHTML = '<br>';
326
- pre.after(exitP);
327
- placeCursorAtEnd(exitP);
328
- return;
329
- }
330
- }
331
-
332
- // Check block-level shortcuts
333
- if (e.inputType === 'insertText' || e.inputType === 'insertParagraph') {
334
- checkShortcuts();
335
- }
336
- });
337
-
338
- // ── Keyboard shortcuts ──────────────────────────────
339
-
340
- writeEl.addEventListener('keydown', function(e) {
341
- var mod = e.ctrlKey || e.metaKey;
342
-
343
- // Inline formatting shortcuts
344
- if (mod && !e.shiftKey) {
345
- if (e.key === 'b') { e.preventDefault(); document.execCommand('bold', false, null); return; }
346
- if (e.key === 'i') { e.preventDefault(); document.execCommand('italic', false, null); return; }
347
- if (e.key === 'e') { e.preventDefault(); execInlineCode(); return; }
348
- if (e.key === 'k') { e.preventDefault(); insertLink(); return; }
349
- }
350
- if (mod && e.shiftKey && e.key === 'x') {
351
- e.preventDefault();
352
- document.execCommand('strikeThrough', false, null);
353
- return;
354
- }
355
-
356
- // Enter key: special handling for code blocks and blockquotes
357
- if (e.key === 'Enter' && !mod && !e.shiftKey) {
358
- var sel = window.getSelection();
359
- var node = sel.rangeCount ? sel.anchorNode : null;
360
- if (node) {
361
- // Code block: insert line break, not paragraph
362
- var pre = node.nodeType === 1 ? node.closest('pre') : (node.parentElement && node.parentElement.closest('pre'));
363
- if (pre) {
364
- e.preventDefault();
365
- // Flag MUST be set before execCommand because the input event
366
- // fires synchronously during execCommand execution
367
- S._checkCodeBlockExit = pre;
368
- document.execCommand('insertLineBreak', false, null);
369
- return;
370
- }
371
-
372
- // Blockquote: Enter on empty line exits
373
- var bqEl = node.nodeType === 1 ? node.closest('blockquote') : (node.parentElement && node.parentElement.closest('blockquote'));
374
- if (bqEl) {
375
- var block = getContainingBlock(node);
376
- // If current block is empty (just whitespace or <br>), exit blockquote
377
- if (block && block !== writeEl && block !== bqEl) {
378
- var blockText = block.textContent.trim();
379
- if (!blockText) {
380
- e.preventDefault();
381
- block.remove();
382
- // If blockquote is now empty, remove it too
383
- if (!bqEl.textContent.trim() && !bqEl.querySelector('img,hr,pre')) {
384
- var afterP = document.createElement('p');
385
- afterP.innerHTML = '<br>';
386
- bqEl.replaceWith(afterP);
387
- placeCursorAtEnd(afterP);
388
- } else {
389
- // Insert paragraph after blockquote
390
- var afterP = document.createElement('p');
391
- afterP.innerHTML = '<br>';
392
- bqEl.after(afterP);
393
- placeCursorAtEnd(afterP);
394
- }
395
- return;
396
- }
397
- }
398
- }
399
- }
400
- }
401
-
402
- // Tab for list indent
403
- if (e.key === 'Tab') {
404
- var block = getContainingBlock(window.getSelection().anchorNode);
405
- if (block && block.tagName === 'LI') {
406
- e.preventDefault();
407
- document.execCommand(e.shiftKey ? 'outdent' : 'indent', false, null);
408
- }
409
- }
410
- });
411
-
412
- // ── Paste handler: strip formatting ──────────────────
413
-
414
- writeEl.addEventListener('paste', function(e) {
415
- e.preventDefault();
416
- var text = (e.clipboardData || window.clipboardData).getData('text/plain');
417
- document.execCommand('insertText', false, text);
418
- });
419
-
420
- // ── Blur handler: sync immediately ──────────────────
421
-
422
- writeEl.addEventListener('blur', function() {
423
- clearTimeout(S._writeSyncTimer);
424
- S.currentBody = htmlToMarkdown(writeEl);
425
- S.syncAll('write');
426
- });
427
-
428
- // ── Toolbar actions ──────────────────────────────────
429
-
430
- function execInlineCode() {
431
- var sel = window.getSelection();
432
- if (!sel.rangeCount) return;
433
- var range = sel.getRangeAt(0);
434
-
435
- // Check if cursor/selection is already inside a <code> element
436
- var node = sel.anchorNode;
437
- var existingCode = null;
438
- var el = node && (node.nodeType === 1 ? node : node.parentElement);
439
- while (el && el !== writeEl) {
440
- if (el.tagName === 'CODE' && !el.closest('pre')) { existingCode = el; break; }
441
- el = el.parentElement;
442
- }
443
-
444
- if (existingCode) {
445
- // Unwrap: replace <code> with its text content
446
- var text = document.createTextNode(existingCode.textContent);
447
- existingCode.replaceWith(text);
448
- range = document.createRange();
449
- range.selectNodeContents(text);
450
- sel.removeAllRanges();
451
- sel.addRange(range);
452
- return;
453
- }
454
-
455
- if (range.collapsed) {
456
- // No selection: insert an empty <code> span the user can type into
457
- var code = document.createElement('code');
458
- code.textContent = '\u200B'; // zero-width space as placeholder
459
- range.insertNode(code);
460
- range = document.createRange();
461
- range.setStart(code.firstChild, 1);
462
- range.collapse(true);
463
- sel.removeAllRanges();
464
- sel.addRange(range);
465
- return;
466
- }
467
-
468
- // Wrap selection in <code>
469
- var code = document.createElement('code');
470
- code.appendChild(range.extractContents());
471
- range.insertNode(code);
472
- range.selectNodeContents(code);
473
- sel.removeAllRanges();
474
- sel.addRange(range);
475
- }
476
-
477
- function insertLink() {
478
- var sel = window.getSelection();
479
- if (!sel.rangeCount) return;
480
- var text = sel.toString() || 'link text';
481
- var url = prompt('Enter URL:', 'https://');
482
- if (!url) return;
483
- var a = document.createElement('a');
484
- a.href = url;
485
- a.textContent = text;
486
- var range = sel.getRangeAt(0);
487
- range.deleteContents();
488
- range.insertNode(a);
489
- placeCursorAtEnd(a);
490
- }
491
-
492
- function wrapBlock(tagName) {
493
- // Toggle: if already in this block type, revert to <p>
494
- var sel = window.getSelection();
495
- if (sel.rangeCount) {
496
- var node = sel.anchorNode;
497
- var el = node && (node.nodeType === 1 ? node : node.parentElement);
498
- while (el && el !== writeEl) {
499
- if (el.tagName === tagName.toUpperCase()) {
500
- document.execCommand('formatBlock', false, '<p>');
501
- return;
502
- }
503
- el = el.parentElement;
504
- }
505
- }
506
- document.execCommand('formatBlock', false, '<' + tagName + '>');
507
- }
508
-
509
- function toggleBlockquote() {
510
- var sel = window.getSelection();
511
- if (!sel.rangeCount) return;
512
- var node = sel.anchorNode;
513
- // Check if cursor is inside a blockquote
514
- var el = node.nodeType === 1 ? node : node.parentElement;
515
- var bq = null;
516
- while (el && el !== writeEl) {
517
- if (el.tagName === 'BLOCKQUOTE') { bq = el; break; }
518
- el = el.parentElement;
519
- }
520
- if (bq) {
521
- // Unwrap: move all children out of blockquote, replace with paragraphs
522
- var children = [].slice.call(bq.childNodes);
523
- var frag = document.createDocumentFragment();
524
- for (var i = 0; i < children.length; i++) {
525
- var child = children[i];
526
- if (child.nodeType === 1 && (child.tagName === 'P' || child.tagName === 'DIV')) {
527
- frag.appendChild(child);
528
- } else if (child.nodeType === 3 && child.textContent.trim()) {
529
- var p = document.createElement('p');
530
- p.textContent = child.textContent.trim();
531
- frag.appendChild(p);
532
- } else {
533
- frag.appendChild(child);
534
- }
535
- }
536
- bq.replaceWith(frag);
537
- // Place cursor in first paragraph
538
- var firstP = frag.firstChild || frag;
539
- if (firstP.nodeType === 11) firstP = firstP.firstChild; // DocumentFragment
540
- placeCursorAtEnd(firstP);
541
- return;
542
- }
543
- // Wrap current block in blockquote
544
- var block = getContainingBlock(sel.anchorNode);
545
- if (block && block !== writeEl) {
546
- var bqNew = document.createElement('blockquote');
547
- block.replaceWith(bqNew);
548
- bqNew.appendChild(block);
549
- placeCursorAtEnd(block);
550
- }
551
- }
552
-
553
- function insertHR() {
554
- var sel = window.getSelection();
555
- if (!sel.rangeCount) return;
556
- var block = getContainingBlock(sel.anchorNode);
557
- if (!block) return;
558
- var hr = document.createElement('hr');
559
- var p = document.createElement('p');
560
- p.innerHTML = '<br>';
561
- block.after(hr);
562
- hr.after(p);
563
- placeCursorAtEnd(p);
564
- }
565
-
566
- function insertImage() {
567
- var url = prompt('Image URL:', 'https://');
568
- if (!url) return;
569
- var alt = prompt('Alt text:', '') || '';
570
- var img = document.createElement('img');
571
- img.src = url;
572
- img.alt = alt;
573
- var sel = window.getSelection();
574
- if (!sel.rangeCount) return;
575
- var range = sel.getRangeAt(0);
576
- range.deleteContents();
577
- range.insertNode(img);
578
- // Place cursor after the image
579
- range.setStartAfter(img);
580
- range.collapse(true);
581
- sel.removeAllRanges();
582
- sel.addRange(range);
583
- }
584
-
585
- function insertCodeBlock() {
586
- var sel = window.getSelection();
587
- if (!sel.rangeCount) return;
588
- var node = sel.anchorNode;
589
-
590
- // Check if cursor is already inside a code block — toggle off
591
- var el = node && (node.nodeType === 1 ? node : node.parentElement);
592
- var existingPre = null;
593
- while (el && el !== writeEl) {
594
- if (el.tagName === 'PRE') { existingPre = el; break; }
595
- el = el.parentElement;
596
- }
597
- if (existingPre) {
598
- // Unwrap: convert code block content to a paragraph
599
- var codeEl = existingPre.querySelector('code') || existingPre;
600
- var text = codeEl.textContent.replace(/\n+$/, '').replace(/^\n+/, '');
601
- var p = document.createElement('p');
602
- p.textContent = text || '\u00A0';
603
- existingPre.replaceWith(p);
604
- placeCursorAtEnd(p);
605
- return;
606
- }
607
-
608
- // Insert new code block — use selected text as content if any
609
- var range = sel.getRangeAt(0);
610
- var selectedText = sel.toString();
611
- var block = getContainingBlock(node);
612
- var pre = document.createElement('pre');
613
- var code = document.createElement('code');
614
-
615
- if (selectedText) {
616
- code.textContent = selectedText + '\n';
617
- // Remove the selected content from the DOM
618
- range.deleteContents();
619
- // If the containing block is now empty, replace it with the code block
620
- if (block && block !== writeEl && !block.textContent.trim()) {
621
- block.replaceWith(pre);
622
- } else if (block && block !== writeEl) {
623
- block.after(pre);
624
- } else {
625
- writeEl.appendChild(pre);
626
- }
627
- } else {
628
- code.textContent = '\n';
629
- if (block && block !== writeEl) {
630
- block.after(pre);
631
- } else {
632
- writeEl.appendChild(pre);
633
- }
634
- }
635
-
636
- pre.appendChild(code);
637
- var after = document.createElement('p');
638
- after.innerHTML = '<br>';
639
- pre.after(after);
640
- placeCursorAtEnd(code);
641
- }
642
-
643
- // Toolbar button wiring
644
- document.getElementById('wb-bold').addEventListener('click', function() {
645
- writeEl.focus();
646
- document.execCommand('bold', false, null);
647
- });
648
- document.getElementById('wb-italic').addEventListener('click', function() {
649
- writeEl.focus();
650
- document.execCommand('italic', false, null);
651
- });
652
- document.getElementById('wb-strike').addEventListener('click', function() {
653
- writeEl.focus();
654
- document.execCommand('strikeThrough', false, null);
655
- });
656
- document.getElementById('wb-code').addEventListener('click', function() {
657
- writeEl.focus();
658
- execInlineCode();
659
- });
660
- document.getElementById('wb-h1').addEventListener('click', function() { writeEl.focus(); wrapBlock('h1'); });
661
- document.getElementById('wb-h2').addEventListener('click', function() { writeEl.focus(); wrapBlock('h2'); });
662
- document.getElementById('wb-h3').addEventListener('click', function() { writeEl.focus(); wrapBlock('h3'); });
663
- document.getElementById('wb-h4').addEventListener('click', function() { writeEl.focus(); wrapBlock('h4'); });
664
- document.getElementById('wb-h5').addEventListener('click', function() { writeEl.focus(); wrapBlock('h5'); });
665
- document.getElementById('wb-p').addEventListener('click', function() { writeEl.focus(); wrapBlock('p'); });
666
- document.getElementById('wb-ul').addEventListener('click', function() {
667
- writeEl.focus();
668
- document.execCommand('insertUnorderedList', false, null);
669
- });
670
- document.getElementById('wb-ol').addEventListener('click', function() {
671
- writeEl.focus();
672
- document.execCommand('insertOrderedList', false, null);
673
- });
674
- document.getElementById('wb-bq').addEventListener('click', function() {
675
- writeEl.focus();
676
- toggleBlockquote();
677
- });
678
- document.getElementById('wb-codeblock').addEventListener('click', function() { writeEl.focus(); insertCodeBlock(); });
679
- document.getElementById('wb-link').addEventListener('click', function() { writeEl.focus(); insertLink(); });
680
- document.getElementById('wb-image').addEventListener('click', function() { writeEl.focus(); insertImage(); });
681
- document.getElementById('wb-hr').addEventListener('click', function() { writeEl.focus(); insertHR(); });
682
- document.getElementById('wb-clear').addEventListener('click', function() {
683
- writeEl.focus();
684
- document.execCommand('removeFormat', false, null);
685
- });
686
-
687
- // ── Active toolbar state tracking ──────────────────────
688
-
689
- var BLOCK_BTN_MAP = { H1: 'wb-h1', H2: 'wb-h2', H3: 'wb-h3', H4: 'wb-h4', H5: 'wb-h5', P: 'wb-p', DIV: 'wb-p' };
690
- var BLOCK_BTN_IDS = ['wb-h1', 'wb-h2', 'wb-h3', 'wb-h4', 'wb-h5', 'wb-p', 'wb-ul', 'wb-ol', 'wb-bq', 'wb-codeblock'];
691
-
692
- function updateToolbarState() {
693
- var sel = window.getSelection();
694
- var activeBlock = null;
695
-
696
- if (sel.rangeCount) {
697
- var node = sel.anchorNode;
698
- var el = node && (node.nodeType === 1 ? node : node.parentElement);
699
- while (el && el !== writeEl) {
700
- var tag = el.tagName;
701
- if (BLOCK_BTN_MAP[tag]) { activeBlock = BLOCK_BTN_MAP[tag]; break; }
702
- if (tag === 'LI') {
703
- var list = el.parentElement;
704
- activeBlock = list && list.tagName === 'OL' ? 'wb-ol' : 'wb-ul';
705
- break;
706
- }
707
- if (tag === 'BLOCKQUOTE') { activeBlock = 'wb-bq'; break; }
708
- if (tag === 'PRE') { activeBlock = 'wb-codeblock'; break; }
709
- el = el.parentElement;
710
- }
711
- }
712
-
713
- for (var i = 0; i < BLOCK_BTN_IDS.length; i++) {
714
- document.getElementById(BLOCK_BTN_IDS[i]).classList.toggle('active', BLOCK_BTN_IDS[i] === activeBlock);
715
- }
716
-
717
- document.getElementById('wb-bold').classList.toggle('active', document.queryCommandState('bold'));
718
- document.getElementById('wb-italic').classList.toggle('active', document.queryCommandState('italic'));
719
- document.getElementById('wb-strike').classList.toggle('active', document.queryCommandState('strikeThrough'));
720
- }
721
-
722
- document.addEventListener('selectionchange', function() {
723
- if (S.currentMode === 'write') updateToolbarState();
724
- });
725
-
726
- // ── Convert title→data-tip for CSS tooltips ──────────
727
-
728
- var tipBtns = document.querySelectorAll('.write-tb-btn[title]');
729
- for (var t = 0; t < tipBtns.length; t++) {
730
- tipBtns[t].setAttribute('data-tip', tipBtns[t].getAttribute('title'));
731
- tipBtns[t].removeAttribute('title');
732
- }
733
-
734
- // ── Register on SDocs ──────────────────────────────
735
-
736
- S.enterWriteMode = enterWriteMode;
737
- S.exitWriteMode = exitWriteMode;
738
- S.updateToolbarState = updateToolbarState;
739
-
740
- })();