sdocs-dev 1.0.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.
@@ -0,0 +1,737 @@
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 = 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
+ // Debounce sync
284
+ clearTimeout(S._writeSyncTimer);
285
+ S._writeSyncTimer = setTimeout(function() {
286
+ S.currentBody = htmlToMarkdown(writeEl);
287
+ S.syncAll('write');
288
+ }, 500);
289
+
290
+ // Code block exit: after Enter inserts a line break, check for 2+ consecutive
291
+ // empty lines at the end. Chromium represents newlines as <br> elements in
292
+ // contentEditable, with an extra trailing BR as a caret placeholder.
293
+ // Pattern: N enters = N+1 trailing BRs. So 2 empty Enters = 3+ trailing BRs.
294
+ if (S._checkCodeBlockExit) {
295
+ var pre = S._checkCodeBlockExit;
296
+ S._checkCodeBlockExit = null;
297
+ var codeEl = pre.querySelector('code') || pre;
298
+ var children = codeEl.childNodes;
299
+ var trailingBRs = 0;
300
+ for (var ci = children.length - 1; ci >= 0; ci--) {
301
+ var child = children[ci];
302
+ if (child.nodeType === 1 && child.tagName === 'BR') { trailingBRs++; continue; }
303
+ // Skip empty or whitespace-only text nodes (e.g. trailing \n from initialization)
304
+ if (child.nodeType === 3 && !child.textContent.trim()) { continue; }
305
+ break;
306
+ }
307
+ // 3+ trailing BRs = 2+ empty Enters at end → exit code block
308
+ if (trailingBRs >= 3) {
309
+ // Remove all trailing BRs
310
+ while (codeEl.lastChild && codeEl.lastChild.nodeType === 1 && codeEl.lastChild.tagName === 'BR') {
311
+ codeEl.removeChild(codeEl.lastChild);
312
+ }
313
+ // Clean trailing text newlines too
314
+ if (codeEl.lastChild && codeEl.lastChild.nodeType === 3) {
315
+ codeEl.lastChild.textContent = codeEl.lastChild.textContent.replace(/\n+$/, '');
316
+ }
317
+ // Ensure code block isn't completely empty
318
+ if (!codeEl.textContent && !codeEl.querySelector('br')) {
319
+ codeEl.textContent = '\n';
320
+ }
321
+ var exitP = document.createElement('p');
322
+ exitP.innerHTML = '<br>';
323
+ pre.after(exitP);
324
+ placeCursorAtEnd(exitP);
325
+ return;
326
+ }
327
+ }
328
+
329
+ // Check block-level shortcuts
330
+ if (e.inputType === 'insertText' || e.inputType === 'insertParagraph') {
331
+ checkShortcuts();
332
+ }
333
+ });
334
+
335
+ // ── Keyboard shortcuts ──────────────────────────────
336
+
337
+ writeEl.addEventListener('keydown', function(e) {
338
+ var mod = e.ctrlKey || e.metaKey;
339
+
340
+ // Inline formatting shortcuts
341
+ if (mod && !e.shiftKey) {
342
+ if (e.key === 'b') { e.preventDefault(); document.execCommand('bold', false, null); return; }
343
+ if (e.key === 'i') { e.preventDefault(); document.execCommand('italic', false, null); return; }
344
+ if (e.key === 'e') { e.preventDefault(); execInlineCode(); return; }
345
+ if (e.key === 'k') { e.preventDefault(); insertLink(); return; }
346
+ }
347
+ if (mod && e.shiftKey && e.key === 'x') {
348
+ e.preventDefault();
349
+ document.execCommand('strikeThrough', false, null);
350
+ return;
351
+ }
352
+
353
+ // Enter key: special handling for code blocks and blockquotes
354
+ if (e.key === 'Enter' && !mod && !e.shiftKey) {
355
+ var sel = window.getSelection();
356
+ var node = sel.rangeCount ? sel.anchorNode : null;
357
+ if (node) {
358
+ // Code block: insert line break, not paragraph
359
+ var pre = node.nodeType === 1 ? node.closest('pre') : (node.parentElement && node.parentElement.closest('pre'));
360
+ if (pre) {
361
+ e.preventDefault();
362
+ // Flag MUST be set before execCommand because the input event
363
+ // fires synchronously during execCommand execution
364
+ S._checkCodeBlockExit = pre;
365
+ document.execCommand('insertLineBreak', false, null);
366
+ return;
367
+ }
368
+
369
+ // Blockquote: Enter on empty line exits
370
+ var bqEl = node.nodeType === 1 ? node.closest('blockquote') : (node.parentElement && node.parentElement.closest('blockquote'));
371
+ if (bqEl) {
372
+ var block = getContainingBlock(node);
373
+ // If current block is empty (just whitespace or <br>), exit blockquote
374
+ if (block && block !== writeEl && block !== bqEl) {
375
+ var blockText = block.textContent.trim();
376
+ if (!blockText) {
377
+ e.preventDefault();
378
+ block.remove();
379
+ // If blockquote is now empty, remove it too
380
+ if (!bqEl.textContent.trim() && !bqEl.querySelector('img,hr,pre')) {
381
+ var afterP = document.createElement('p');
382
+ afterP.innerHTML = '<br>';
383
+ bqEl.replaceWith(afterP);
384
+ placeCursorAtEnd(afterP);
385
+ } else {
386
+ // Insert paragraph after blockquote
387
+ var afterP = document.createElement('p');
388
+ afterP.innerHTML = '<br>';
389
+ bqEl.after(afterP);
390
+ placeCursorAtEnd(afterP);
391
+ }
392
+ return;
393
+ }
394
+ }
395
+ }
396
+ }
397
+ }
398
+
399
+ // Tab for list indent
400
+ if (e.key === 'Tab') {
401
+ var block = getContainingBlock(window.getSelection().anchorNode);
402
+ if (block && block.tagName === 'LI') {
403
+ e.preventDefault();
404
+ document.execCommand(e.shiftKey ? 'outdent' : 'indent', false, null);
405
+ }
406
+ }
407
+ });
408
+
409
+ // ── Paste handler: strip formatting ──────────────────
410
+
411
+ writeEl.addEventListener('paste', function(e) {
412
+ e.preventDefault();
413
+ var text = (e.clipboardData || window.clipboardData).getData('text/plain');
414
+ document.execCommand('insertText', false, text);
415
+ });
416
+
417
+ // ── Blur handler: sync immediately ──────────────────
418
+
419
+ writeEl.addEventListener('blur', function() {
420
+ clearTimeout(S._writeSyncTimer);
421
+ S.currentBody = htmlToMarkdown(writeEl);
422
+ S.syncAll('write');
423
+ });
424
+
425
+ // ── Toolbar actions ──────────────────────────────────
426
+
427
+ function execInlineCode() {
428
+ var sel = window.getSelection();
429
+ if (!sel.rangeCount) return;
430
+ var range = sel.getRangeAt(0);
431
+
432
+ // Check if cursor/selection is already inside a <code> element
433
+ var node = sel.anchorNode;
434
+ var existingCode = null;
435
+ var el = node && (node.nodeType === 1 ? node : node.parentElement);
436
+ while (el && el !== writeEl) {
437
+ if (el.tagName === 'CODE' && !el.closest('pre')) { existingCode = el; break; }
438
+ el = el.parentElement;
439
+ }
440
+
441
+ if (existingCode) {
442
+ // Unwrap: replace <code> with its text content
443
+ var text = document.createTextNode(existingCode.textContent);
444
+ existingCode.replaceWith(text);
445
+ range = document.createRange();
446
+ range.selectNodeContents(text);
447
+ sel.removeAllRanges();
448
+ sel.addRange(range);
449
+ return;
450
+ }
451
+
452
+ if (range.collapsed) {
453
+ // No selection: insert an empty <code> span the user can type into
454
+ var code = document.createElement('code');
455
+ code.textContent = '\u200B'; // zero-width space as placeholder
456
+ range.insertNode(code);
457
+ range = document.createRange();
458
+ range.setStart(code.firstChild, 1);
459
+ range.collapse(true);
460
+ sel.removeAllRanges();
461
+ sel.addRange(range);
462
+ return;
463
+ }
464
+
465
+ // Wrap selection in <code>
466
+ var code = document.createElement('code');
467
+ code.appendChild(range.extractContents());
468
+ range.insertNode(code);
469
+ range.selectNodeContents(code);
470
+ sel.removeAllRanges();
471
+ sel.addRange(range);
472
+ }
473
+
474
+ function insertLink() {
475
+ var sel = window.getSelection();
476
+ if (!sel.rangeCount) return;
477
+ var text = sel.toString() || 'link text';
478
+ var url = prompt('Enter URL:', 'https://');
479
+ if (!url) return;
480
+ var a = document.createElement('a');
481
+ a.href = url;
482
+ a.textContent = text;
483
+ var range = sel.getRangeAt(0);
484
+ range.deleteContents();
485
+ range.insertNode(a);
486
+ placeCursorAtEnd(a);
487
+ }
488
+
489
+ function wrapBlock(tagName) {
490
+ // Toggle: if already in this block type, revert to <p>
491
+ var sel = window.getSelection();
492
+ if (sel.rangeCount) {
493
+ var node = sel.anchorNode;
494
+ var el = node && (node.nodeType === 1 ? node : node.parentElement);
495
+ while (el && el !== writeEl) {
496
+ if (el.tagName === tagName.toUpperCase()) {
497
+ document.execCommand('formatBlock', false, '<p>');
498
+ return;
499
+ }
500
+ el = el.parentElement;
501
+ }
502
+ }
503
+ document.execCommand('formatBlock', false, '<' + tagName + '>');
504
+ }
505
+
506
+ function toggleBlockquote() {
507
+ var sel = window.getSelection();
508
+ if (!sel.rangeCount) return;
509
+ var node = sel.anchorNode;
510
+ // Check if cursor is inside a blockquote
511
+ var el = node.nodeType === 1 ? node : node.parentElement;
512
+ var bq = null;
513
+ while (el && el !== writeEl) {
514
+ if (el.tagName === 'BLOCKQUOTE') { bq = el; break; }
515
+ el = el.parentElement;
516
+ }
517
+ if (bq) {
518
+ // Unwrap: move all children out of blockquote, replace with paragraphs
519
+ var children = [].slice.call(bq.childNodes);
520
+ var frag = document.createDocumentFragment();
521
+ for (var i = 0; i < children.length; i++) {
522
+ var child = children[i];
523
+ if (child.nodeType === 1 && (child.tagName === 'P' || child.tagName === 'DIV')) {
524
+ frag.appendChild(child);
525
+ } else if (child.nodeType === 3 && child.textContent.trim()) {
526
+ var p = document.createElement('p');
527
+ p.textContent = child.textContent.trim();
528
+ frag.appendChild(p);
529
+ } else {
530
+ frag.appendChild(child);
531
+ }
532
+ }
533
+ bq.replaceWith(frag);
534
+ // Place cursor in first paragraph
535
+ var firstP = frag.firstChild || frag;
536
+ if (firstP.nodeType === 11) firstP = firstP.firstChild; // DocumentFragment
537
+ placeCursorAtEnd(firstP);
538
+ return;
539
+ }
540
+ // Wrap current block in blockquote
541
+ var block = getContainingBlock(sel.anchorNode);
542
+ if (block && block !== writeEl) {
543
+ var bqNew = document.createElement('blockquote');
544
+ block.replaceWith(bqNew);
545
+ bqNew.appendChild(block);
546
+ placeCursorAtEnd(block);
547
+ }
548
+ }
549
+
550
+ function insertHR() {
551
+ var sel = window.getSelection();
552
+ if (!sel.rangeCount) return;
553
+ var block = getContainingBlock(sel.anchorNode);
554
+ if (!block) return;
555
+ var hr = document.createElement('hr');
556
+ var p = document.createElement('p');
557
+ p.innerHTML = '<br>';
558
+ block.after(hr);
559
+ hr.after(p);
560
+ placeCursorAtEnd(p);
561
+ }
562
+
563
+ function insertImage() {
564
+ var url = prompt('Image URL:', 'https://');
565
+ if (!url) return;
566
+ var alt = prompt('Alt text:', '') || '';
567
+ var img = document.createElement('img');
568
+ img.src = url;
569
+ img.alt = alt;
570
+ var sel = window.getSelection();
571
+ if (!sel.rangeCount) return;
572
+ var range = sel.getRangeAt(0);
573
+ range.deleteContents();
574
+ range.insertNode(img);
575
+ // Place cursor after the image
576
+ range.setStartAfter(img);
577
+ range.collapse(true);
578
+ sel.removeAllRanges();
579
+ sel.addRange(range);
580
+ }
581
+
582
+ function insertCodeBlock() {
583
+ var sel = window.getSelection();
584
+ if (!sel.rangeCount) return;
585
+ var node = sel.anchorNode;
586
+
587
+ // Check if cursor is already inside a code block — toggle off
588
+ var el = node && (node.nodeType === 1 ? node : node.parentElement);
589
+ var existingPre = null;
590
+ while (el && el !== writeEl) {
591
+ if (el.tagName === 'PRE') { existingPre = el; break; }
592
+ el = el.parentElement;
593
+ }
594
+ if (existingPre) {
595
+ // Unwrap: convert code block content to a paragraph
596
+ var codeEl = existingPre.querySelector('code') || existingPre;
597
+ var text = codeEl.textContent.replace(/\n+$/, '').replace(/^\n+/, '');
598
+ var p = document.createElement('p');
599
+ p.textContent = text || '\u00A0';
600
+ existingPre.replaceWith(p);
601
+ placeCursorAtEnd(p);
602
+ return;
603
+ }
604
+
605
+ // Insert new code block — use selected text as content if any
606
+ var range = sel.getRangeAt(0);
607
+ var selectedText = sel.toString();
608
+ var block = getContainingBlock(node);
609
+ var pre = document.createElement('pre');
610
+ var code = document.createElement('code');
611
+
612
+ if (selectedText) {
613
+ code.textContent = selectedText + '\n';
614
+ // Remove the selected content from the DOM
615
+ range.deleteContents();
616
+ // If the containing block is now empty, replace it with the code block
617
+ if (block && block !== writeEl && !block.textContent.trim()) {
618
+ block.replaceWith(pre);
619
+ } else if (block && block !== writeEl) {
620
+ block.after(pre);
621
+ } else {
622
+ writeEl.appendChild(pre);
623
+ }
624
+ } else {
625
+ code.textContent = '\n';
626
+ if (block && block !== writeEl) {
627
+ block.after(pre);
628
+ } else {
629
+ writeEl.appendChild(pre);
630
+ }
631
+ }
632
+
633
+ pre.appendChild(code);
634
+ var after = document.createElement('p');
635
+ after.innerHTML = '<br>';
636
+ pre.after(after);
637
+ placeCursorAtEnd(code);
638
+ }
639
+
640
+ // Toolbar button wiring
641
+ document.getElementById('wb-bold').addEventListener('click', function() {
642
+ writeEl.focus();
643
+ document.execCommand('bold', false, null);
644
+ });
645
+ document.getElementById('wb-italic').addEventListener('click', function() {
646
+ writeEl.focus();
647
+ document.execCommand('italic', false, null);
648
+ });
649
+ document.getElementById('wb-strike').addEventListener('click', function() {
650
+ writeEl.focus();
651
+ document.execCommand('strikeThrough', false, null);
652
+ });
653
+ document.getElementById('wb-code').addEventListener('click', function() {
654
+ writeEl.focus();
655
+ execInlineCode();
656
+ });
657
+ document.getElementById('wb-h1').addEventListener('click', function() { writeEl.focus(); wrapBlock('h1'); });
658
+ document.getElementById('wb-h2').addEventListener('click', function() { writeEl.focus(); wrapBlock('h2'); });
659
+ document.getElementById('wb-h3').addEventListener('click', function() { writeEl.focus(); wrapBlock('h3'); });
660
+ document.getElementById('wb-h4').addEventListener('click', function() { writeEl.focus(); wrapBlock('h4'); });
661
+ document.getElementById('wb-h5').addEventListener('click', function() { writeEl.focus(); wrapBlock('h5'); });
662
+ document.getElementById('wb-p').addEventListener('click', function() { writeEl.focus(); wrapBlock('p'); });
663
+ document.getElementById('wb-ul').addEventListener('click', function() {
664
+ writeEl.focus();
665
+ document.execCommand('insertUnorderedList', false, null);
666
+ });
667
+ document.getElementById('wb-ol').addEventListener('click', function() {
668
+ writeEl.focus();
669
+ document.execCommand('insertOrderedList', false, null);
670
+ });
671
+ document.getElementById('wb-bq').addEventListener('click', function() {
672
+ writeEl.focus();
673
+ toggleBlockquote();
674
+ });
675
+ document.getElementById('wb-codeblock').addEventListener('click', function() { writeEl.focus(); insertCodeBlock(); });
676
+ document.getElementById('wb-link').addEventListener('click', function() { writeEl.focus(); insertLink(); });
677
+ document.getElementById('wb-image').addEventListener('click', function() { writeEl.focus(); insertImage(); });
678
+ document.getElementById('wb-hr').addEventListener('click', function() { writeEl.focus(); insertHR(); });
679
+ document.getElementById('wb-clear').addEventListener('click', function() {
680
+ writeEl.focus();
681
+ document.execCommand('removeFormat', false, null);
682
+ });
683
+
684
+ // ── Active toolbar state tracking ──────────────────────
685
+
686
+ 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' };
687
+ var BLOCK_BTN_IDS = ['wb-h1', 'wb-h2', 'wb-h3', 'wb-h4', 'wb-h5', 'wb-p', 'wb-ul', 'wb-ol', 'wb-bq', 'wb-codeblock'];
688
+
689
+ function updateToolbarState() {
690
+ var sel = window.getSelection();
691
+ var activeBlock = null;
692
+
693
+ if (sel.rangeCount) {
694
+ var node = sel.anchorNode;
695
+ var el = node && (node.nodeType === 1 ? node : node.parentElement);
696
+ while (el && el !== writeEl) {
697
+ var tag = el.tagName;
698
+ if (BLOCK_BTN_MAP[tag]) { activeBlock = BLOCK_BTN_MAP[tag]; break; }
699
+ if (tag === 'LI') {
700
+ var list = el.parentElement;
701
+ activeBlock = list && list.tagName === 'OL' ? 'wb-ol' : 'wb-ul';
702
+ break;
703
+ }
704
+ if (tag === 'BLOCKQUOTE') { activeBlock = 'wb-bq'; break; }
705
+ if (tag === 'PRE') { activeBlock = 'wb-codeblock'; break; }
706
+ el = el.parentElement;
707
+ }
708
+ }
709
+
710
+ for (var i = 0; i < BLOCK_BTN_IDS.length; i++) {
711
+ document.getElementById(BLOCK_BTN_IDS[i]).classList.toggle('active', BLOCK_BTN_IDS[i] === activeBlock);
712
+ }
713
+
714
+ document.getElementById('wb-bold').classList.toggle('active', document.queryCommandState('bold'));
715
+ document.getElementById('wb-italic').classList.toggle('active', document.queryCommandState('italic'));
716
+ document.getElementById('wb-strike').classList.toggle('active', document.queryCommandState('strikeThrough'));
717
+ }
718
+
719
+ document.addEventListener('selectionchange', function() {
720
+ if (S.currentMode === 'write') updateToolbarState();
721
+ });
722
+
723
+ // ── Convert title→data-tip for CSS tooltips ──────────
724
+
725
+ var tipBtns = document.querySelectorAll('.write-tb-btn[title]');
726
+ for (var t = 0; t < tipBtns.length; t++) {
727
+ tipBtns[t].setAttribute('data-tip', tipBtns[t].getAttribute('title'));
728
+ tipBtns[t].removeAttribute('title');
729
+ }
730
+
731
+ // ── Register on SDocs ──────────────────────────────
732
+
733
+ S.enterWriteMode = enterWriteMode;
734
+ S.exitWriteMode = exitWriteMode;
735
+ S.updateToolbarState = updateToolbarState;
736
+
737
+ })();