sdocs-dev 1.2.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.
- package/README.md +149 -0
- package/bin/sdocs-dev.js +282 -38
- package/package.json +13 -11
- package/public/sdocs-slugify.js +12 -0
- package/public/sdocs-styles.js +279 -353
- package/public/brotli-wasm.js +0 -519
- package/public/brotli_wasm_bg.wasm +0 -0
- package/public/css/layout.css +0 -241
- package/public/css/mobile.css +0 -189
- package/public/css/panel.css +0 -393
- package/public/css/rendered.css +0 -295
- package/public/css/tokens.css +0 -116
- package/public/css/write.css +0 -128
- package/public/default.md +0 -255
- package/public/fonts/inter-400.woff2 +0 -0
- package/public/fonts/inter-500.woff2 +0 -0
- package/public/fonts/inter-600.woff2 +0 -0
- package/public/images/examples.png +0 -0
- package/public/index.html +0 -600
- package/public/sdocs-app.js +0 -653
- package/public/sdocs-controls.js +0 -272
- package/public/sdocs-export.js +0 -212
- package/public/sdocs-state.js +0 -59
- package/public/sdocs-theme.js +0 -260
- package/public/sdocs-write.js +0 -737
- package/public/sw.js +0 -108
- package/public/vendor/marked.min.js +0 -6
- package/server.js +0 -128
package/public/sdocs-write.js
DELETED
|
@@ -1,737 +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 = 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 += '';
|
|
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
|
-
})();
|