sunny-html-editor 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.
package/html-editor.js ADDED
@@ -0,0 +1,2042 @@
1
+ /**
2
+ * Sunny HTML Editor - 軽量WYSIWYGエディタ
3
+ * @version 1.0.0
4
+ */
5
+ (function() {
6
+ var container = document.getElementById('editorContainer');
7
+ var editModeBtn = document.getElementById('editMode');
8
+ var toggleSidebarBtn = document.getElementById('toggleSidebar');
9
+ var saveHtmlBtn = document.getElementById('saveHtml');
10
+ var exportMdBtn = document.getElementById('exportMd');
11
+ var exportHtmlBtn = document.getElementById('exportHtml');
12
+ var exportExcelBtn = document.getElementById('exportExcel');
13
+ var exportPdfBtn = document.getElementById('exportPdf');
14
+ var isEditMode = false;
15
+ var sectionCounter = { h2: 100, h3: 100, h4: 100 };
16
+
17
+ // ========== 新しいliにフォーカスを設定するヘルパー関数 ==========
18
+ function focusNewLi(newLi) {
19
+ // 作成直後フラグを設定(focusoutで即削除されないように)
20
+ newLi.dataset.justCreated = 'true';
21
+
22
+ setTimeout(function() {
23
+ if (!newLi.parentNode) {
24
+ return;
25
+ }
26
+ newLi.focus();
27
+ // カーソル位置を明示的に設定
28
+ var range = document.createRange();
29
+ var sel = window.getSelection();
30
+ // テキストノードがあればその中に、なければ要素自体に
31
+ if (newLi.firstChild && newLi.firstChild.nodeType === 3) {
32
+ range.setStart(newLi.firstChild, 0);
33
+ range.setEnd(newLi.firstChild, 0);
34
+ } else {
35
+ range.selectNodeContents(newLi);
36
+ range.collapse(true);
37
+ }
38
+ sel.removeAllRanges();
39
+ sel.addRange(range);
40
+ }, 10);
41
+ }
42
+
43
+ // ========== HTML保存 ==========
44
+ saveHtmlBtn.addEventListener('click', function() {
45
+ var wasEditMode = isEditMode;
46
+ if (isEditMode) editModeBtn.click();
47
+
48
+ var html = '<!DOCTYPE html>\n' + document.documentElement.outerHTML;
49
+
50
+ if (wasEditMode) editModeBtn.click();
51
+
52
+ var blob = new Blob([html], { type: 'text/html; charset=utf-8' });
53
+ var url = URL.createObjectURL(blob);
54
+ var a = document.createElement('a');
55
+ a.href = url;
56
+
57
+ var h1Element = container.querySelector('h1');
58
+ var fileName = 'design_spec';
59
+ if (h1Element) {
60
+ fileName = h1Element.innerHTML
61
+ .replace(/<br\s*\/?>/gi, '-')
62
+ .replace(/<\/p>\s*<p>/gi, '-')
63
+ .replace(/<[^>]*>/g, '')
64
+ .replace(/\s+/g, '')
65
+ .trim();
66
+ }
67
+ a.download = fileName + '.html';
68
+ a.click();
69
+ URL.revokeObjectURL(url);
70
+ });
71
+
72
+ // ========== Markdown出力 ==========
73
+ exportMdBtn.addEventListener('click', function() {
74
+ var md = htmlToMarkdown();
75
+
76
+ navigator.clipboard.writeText(md).then(function() {
77
+ var originalText = exportMdBtn.textContent;
78
+ exportMdBtn.textContent = '✓';
79
+ setTimeout(function() {
80
+ exportMdBtn.textContent = originalText;
81
+ }, 1000);
82
+ });
83
+ });
84
+
85
+ // ========== HTMLコピー ==========
86
+ exportHtmlBtn.addEventListener('click', function() {
87
+ var html = getPlainHtml();
88
+
89
+ navigator.clipboard.writeText(html).then(function() {
90
+ var originalText = exportHtmlBtn.textContent;
91
+ exportHtmlBtn.textContent = '✓';
92
+ setTimeout(function() {
93
+ exportHtmlBtn.textContent = originalText;
94
+ }, 1000);
95
+ });
96
+ });
97
+
98
+ function getPlainHtml() {
99
+ var content = container.cloneNode(true);
100
+
101
+ // ツールバーを除去
102
+ var toolbar = content.querySelector('.toolbar');
103
+ if (toolbar) toolbar.remove();
104
+
105
+ // 右クリックメニューを除去
106
+ content.querySelectorAll('.context-menu').forEach(function(menu) {
107
+ menu.remove();
108
+ });
109
+
110
+ // contenteditable属性を除去
111
+ content.querySelectorAll('[contenteditable]').forEach(function(el) {
112
+ el.removeAttribute('contenteditable');
113
+ });
114
+
115
+ // edit-modeクラスを除去
116
+ content.classList.remove('edit-mode');
117
+
118
+ // id属性を除去(containerのid)
119
+ content.removeAttribute('id');
120
+
121
+ // コンテンツHTML取得(コメント除去)
122
+ var innerHtml = content.innerHTML
123
+ .replace(/<!--[\s\S]*?-->/g, '') // コメント除去
124
+ .replace(/^\s*[\r\n]/gm, '') // 空行除去
125
+ .trim();
126
+
127
+ // タイトル取得
128
+ var h1 = container.querySelector('h1');
129
+ var title = h1 ? h1.textContent.trim() : 'Document';
130
+
131
+ // CSSを取得(サイドバー、リサイザー、ツールバー、右クリックメニュー関連を除外)
132
+ var styleEl = document.querySelector('style');
133
+ var css = '';
134
+ if (styleEl) {
135
+ css = styleEl.textContent
136
+ .replace(/\.sidebar[\s\S]*?\}\n/g, '')
137
+ .replace(/\.sidebar [^{]*\{[\s\S]*?\}\n/g, '')
138
+ .replace(/\.resizer[\s\S]*?\}\n/g, '')
139
+ .replace(/\.toolbar[\s\S]*?\}\n/g, '')
140
+ .replace(/\.toolbar [^{]*\{[\s\S]*?\}\n/g, '')
141
+ .replace(/\.context-menu[\s\S]*?\}\n/g, '')
142
+ .replace(/\.context-menu[^{]*\{[\s\S]*?\}\n/g, '')
143
+ .replace(/\.edit-mode[\s\S]*?\}\n/g, '')
144
+ .replace(/\.edit-mode [^{]*\{[\s\S]*?\}\n/g, '')
145
+ .replace(/@media[^{]*\{[\s\S]*?\}\s*\}/g, '')
146
+ .replace(/\n\s*\n/g, '\n')
147
+ .trim();
148
+ }
149
+
150
+ // 完全なHTML構造(CSS含む、ラッパー構造含む)
151
+ var html = '<!DOCTYPE html>\n' +
152
+ '<html lang="ja">\n' +
153
+ '<head>\n' +
154
+ ' <meta charset="UTF-8">\n' +
155
+ ' <title>' + title + '</title>\n' +
156
+ '<style>\n' + css + '\n</style>\n' +
157
+ '</head>\n' +
158
+ '<body>\n' +
159
+ '<div class="layout">\n' +
160
+ ' <main class="main">\n' +
161
+ ' <div class="container">\n' +
162
+ innerHtml + '\n' +
163
+ ' </div>\n' +
164
+ ' </main>\n' +
165
+ '</div>\n' +
166
+ '</body>\n' +
167
+ '</html>';
168
+
169
+ return html;
170
+ }
171
+
172
+ // リストをMarkdownに変換(再帰対応)
173
+ function processListToMarkdown(listElement, indent) {
174
+ var lines = [];
175
+ var items = listElement.children;
176
+ var isOrdered = listElement.tagName === 'OL';
177
+ var counter = 1;
178
+
179
+ for (var i = 0; i < items.length; i++) {
180
+ var li = items[i];
181
+ if (li.tagName !== 'LI') continue;
182
+
183
+ // li直下のテキスト(子リストを除く)
184
+ var text = '';
185
+ li.childNodes.forEach(function(node) {
186
+ if (node.nodeType === 3) {
187
+ text += node.textContent;
188
+ } else if (node.nodeType === 1 && !['UL', 'OL'].includes(node.tagName)) {
189
+ text += node.textContent;
190
+ }
191
+ });
192
+ text = text.trim();
193
+
194
+ // マーカー生成
195
+ var marker = isOrdered ? (counter++ + '. ') : '- ';
196
+ lines.push(indent + marker + text);
197
+
198
+ // 子リストを再帰処理
199
+ var childList = li.querySelector('ul, ol');
200
+ if (childList && li.contains(childList) && childList.parentElement === li) {
201
+ var childLines = processListToMarkdown(childList, indent + ' ');
202
+ lines = lines.concat(childLines);
203
+ }
204
+ }
205
+
206
+ return lines;
207
+ }
208
+
209
+ // blockquoteをMarkdownに変換
210
+ function processBlockquoteToMarkdown(blockquote) {
211
+ var lines = [];
212
+ var text = '';
213
+
214
+ // 内部のp要素を取得、なければ直接テキスト
215
+ var paragraphs = blockquote.querySelectorAll('p');
216
+ if (paragraphs.length > 0) {
217
+ paragraphs.forEach(function(p) {
218
+ lines.push('> ' + p.textContent.trim());
219
+ });
220
+ } else {
221
+ // p要素がない場合は直接テキストを取得
222
+ text = blockquote.textContent.trim();
223
+ var textLines = text.split('\n');
224
+ textLines.forEach(function(line) {
225
+ line = line.trim();
226
+ if (line) {
227
+ lines.push('> ' + line);
228
+ }
229
+ });
230
+ }
231
+
232
+ return lines;
233
+ }
234
+
235
+ // pre/codeをMarkdownに変換
236
+ function processPreToMarkdown(pre) {
237
+ var lines = [];
238
+ var codeElement = pre.querySelector('code');
239
+ var language = '';
240
+
241
+ if (codeElement) {
242
+ // class="language-xxx" から言語を取得
243
+ var classList = codeElement.className.split(' ');
244
+ for (var i = 0; i < classList.length; i++) {
245
+ if (classList[i].startsWith('language-')) {
246
+ language = classList[i].replace('language-', '');
247
+ break;
248
+ }
249
+ }
250
+ }
251
+
252
+ var codeText = codeElement ? codeElement.textContent : pre.textContent;
253
+
254
+ lines.push('```' + language);
255
+ lines.push(codeText);
256
+ lines.push('```');
257
+
258
+ return lines;
259
+ }
260
+
261
+ function htmlToMarkdown() {
262
+ var lines = [];
263
+ var content = container.querySelector('.content');
264
+ if (!content) content = container;
265
+
266
+ // 対象要素を拡張(ul, ol, pre, blockquote を追加)
267
+ var elements = content.querySelectorAll('h1, h2, h3, h4, p, table, hr, ul, ol, pre, blockquote');
268
+
269
+ // 処理済みの要素を追跡(ネストされたリストの重複処理を防ぐ)
270
+ var processed = new Set();
271
+
272
+ elements.forEach(function(el) {
273
+ // 既に処理済みならスキップ
274
+ if (processed.has(el)) return;
275
+
276
+ // 親がul/olの場合はスキップ(親リストで処理される)
277
+ if ((el.tagName === 'UL' || el.tagName === 'OL') && el.parentElement.closest('ul, ol')) {
278
+ return;
279
+ }
280
+
281
+ var tag = el.tagName;
282
+
283
+ if (tag === 'H1') {
284
+ var h1Text = el.innerHTML
285
+ .replace(/<br\s*\/?>/gi, ' - ')
286
+ .replace(/<[^>]*>/g, '')
287
+ .trim();
288
+ lines.push('# ' + h1Text);
289
+ lines.push('');
290
+ } else if (tag === 'H2') {
291
+ lines.push('## ' + el.textContent.trim());
292
+ lines.push('');
293
+ } else if (tag === 'H3') {
294
+ lines.push('### ' + el.textContent.trim());
295
+ lines.push('');
296
+ } else if (tag === 'H4') {
297
+ lines.push('#### ' + el.textContent.trim());
298
+ lines.push('');
299
+ } else if (tag === 'P') {
300
+ lines.push(el.textContent.trim());
301
+ lines.push('');
302
+ } else if (tag === 'HR') {
303
+ lines.push('---');
304
+ lines.push('');
305
+ } else if (tag === 'UL' || tag === 'OL') {
306
+ var listLines = processListToMarkdown(el, '');
307
+ lines = lines.concat(listLines);
308
+ lines.push('');
309
+ processed.add(el);
310
+ } else if (tag === 'PRE') {
311
+ var preLines = processPreToMarkdown(el);
312
+ lines = lines.concat(preLines);
313
+ lines.push('');
314
+ processed.add(el);
315
+ } else if (tag === 'BLOCKQUOTE') {
316
+ var bqLines = processBlockquoteToMarkdown(el);
317
+ lines = lines.concat(bqLines);
318
+ lines.push('');
319
+ processed.add(el);
320
+ } else if (tag === 'TABLE') {
321
+ var rows = el.querySelectorAll('tr');
322
+
323
+ // ヘッダー行から「選択肢」列のインデックスを特定
324
+ var choiceColIndex = -1;
325
+ if (rows.length > 0) {
326
+ var headerCells = rows[0].querySelectorAll('th, td');
327
+ headerCells.forEach(function(cell, index) {
328
+ if (cell.textContent.trim() === '選択肢') {
329
+ choiceColIndex = index;
330
+ }
331
+ });
332
+ }
333
+
334
+ rows.forEach(function(row, rowIndex) {
335
+ var cells = row.querySelectorAll('th, td');
336
+ var cellTexts = [];
337
+ cells.forEach(function(cell, cellIndex) {
338
+ var text;
339
+ // 「選択肢」列かつデータ行の場合は<br>を / に変換
340
+ if (cellIndex === choiceColIndex && rowIndex > 0) {
341
+ var tempDiv = document.createElement('div');
342
+ tempDiv.innerHTML = cell.innerHTML;
343
+ tempDiv.innerHTML = tempDiv.innerHTML.replace(/<br\s*\/?>/gi, ' / ');
344
+ text = tempDiv.textContent.trim();
345
+ } else {
346
+ text = cell.textContent.trim();
347
+ }
348
+ cellTexts.push(text);
349
+ });
350
+ lines.push('| ' + cellTexts.join(' | ') + ' |');
351
+
352
+ if (rowIndex === 0) {
353
+ var separator = cells.length > 0
354
+ ? '|' + Array(cells.length).fill('------').join('|') + '|'
355
+ : '';
356
+ lines.push(separator);
357
+ }
358
+ });
359
+ lines.push('');
360
+ }
361
+ });
362
+
363
+ return lines.join('\n');
364
+ }
365
+
366
+ // ========== サイドバー関連 ==========
367
+ var layout = document.querySelector('.layout');
368
+ var main = document.querySelector('.main');
369
+
370
+ // サイドバーHTMLを動的に生成
371
+ function generateSidebarHtml() {
372
+ var h1 = container.querySelector('h1');
373
+ var h1Text = h1 ? h1.textContent : 'Document';
374
+ var h1Html = h1Text.replace(/\s*[-_]\s*/g, '<br>');
375
+
376
+ var navItems = '';
377
+ var headings = container.querySelectorAll('h2[id], h3[id]');
378
+ headings.forEach(function(h) {
379
+ var className = h.tagName === 'H2' ? 'nav-h2' : 'nav-h3';
380
+ navItems += '<li><a href="#' + h.id + '" class="' + className + '">' + h.textContent + '</a></li>';
381
+ });
382
+
383
+ return '<aside class="sidebar"><h1>' + h1Html + '</h1><nav><ul>' + navItems + '</ul></nav></aside>';
384
+ }
385
+
386
+ var sidebarHtml = generateSidebarHtml();
387
+ var resizerHtml = '<div class="resizer"></div>';
388
+ var savedWidth = 280;
389
+ var sidebarExists = false;
390
+ var isResizing = false;
391
+
392
+ function updateActiveLink() {
393
+ var sections = document.querySelectorAll('h2[id], h3[id]');
394
+ var navLinks = document.querySelectorAll('.sidebar nav a');
395
+ if (navLinks.length === 0) return;
396
+ var current = '';
397
+ sections.forEach(function(section) {
398
+ var sectionTop = section.offsetTop;
399
+ if (window.scrollY >= sectionTop - 100) {
400
+ current = section.getAttribute('id');
401
+ }
402
+ });
403
+ navLinks.forEach(function(link) {
404
+ link.classList.remove('active');
405
+ if (link.getAttribute('href') === '#' + current) {
406
+ link.classList.add('active');
407
+ }
408
+ });
409
+ }
410
+
411
+ window.addEventListener('scroll', updateActiveLink);
412
+
413
+ function updateResizerPosition() {
414
+ var r = document.querySelector('.resizer');
415
+ var s = document.querySelector('.sidebar');
416
+ if (r && s) r.style.left = s.offsetWidth + 'px';
417
+ }
418
+
419
+ function setupResizer() {
420
+ var r = document.querySelector('.resizer');
421
+ if (!r) return;
422
+ r.addEventListener('mousedown', function(e) {
423
+ isResizing = true;
424
+ document.body.style.cursor = 'ew-resize';
425
+ document.body.style.userSelect = 'none';
426
+ });
427
+ }
428
+
429
+ document.addEventListener('mousemove', function(e) {
430
+ if (!isResizing) return;
431
+ var s = document.querySelector('.sidebar');
432
+ if (!s) return;
433
+ var newWidth = e.clientX;
434
+ if (newWidth < 200) newWidth = 200;
435
+ if (newWidth > 400) newWidth = 400;
436
+ s.style.width = newWidth + 'px';
437
+ main.style.marginLeft = newWidth + 'px';
438
+ updateResizerPosition();
439
+ });
440
+
441
+ document.addEventListener('mouseup', function() {
442
+ if (isResizing) {
443
+ isResizing = false;
444
+ document.body.style.cursor = '';
445
+ document.body.style.userSelect = '';
446
+ }
447
+ });
448
+
449
+ toggleSidebarBtn.addEventListener('click', function() {
450
+ var s = document.querySelector('.sidebar');
451
+ var r = document.querySelector('.resizer');
452
+
453
+ if (sidebarExists) {
454
+ savedWidth = s.offsetWidth;
455
+ sidebarHtml = s.outerHTML;
456
+ resizerHtml = r.outerHTML;
457
+ s.remove();
458
+ r.remove();
459
+ main.style.marginLeft = '0';
460
+ sidebarExists = false;
461
+ } else {
462
+ main.insertAdjacentHTML('beforebegin', sidebarHtml);
463
+ main.insertAdjacentHTML('beforebegin', resizerHtml);
464
+ var newSidebar = document.querySelector('.sidebar');
465
+ newSidebar.style.width = savedWidth + 'px';
466
+ main.style.marginLeft = savedWidth + 'px';
467
+ setupResizer();
468
+ updateResizerPosition();
469
+ updateActiveLink();
470
+ sidebarExists = true;
471
+ }
472
+ });
473
+
474
+ // ========== サイドナビ更新 ==========
475
+ function updateSidebarNav() {
476
+ var s = document.querySelector('.sidebar');
477
+ if (s) {
478
+ var navUl = s.querySelector('nav ul');
479
+ if (navUl) {
480
+ navUl.innerHTML = '';
481
+
482
+ var headings = container.querySelectorAll('h2[id], h3[id]');
483
+ headings.forEach(function(h) {
484
+ var li = document.createElement('li');
485
+ var a = document.createElement('a');
486
+ a.href = '#' + h.id;
487
+ a.textContent = h.textContent;
488
+ a.className = h.tagName === 'H2' ? 'nav-h2' : 'nav-h3';
489
+ li.appendChild(a);
490
+ navUl.appendChild(li);
491
+ });
492
+
493
+ // サイドバータイトルも更新
494
+ var mainH1 = container.querySelector('h1');
495
+ if (mainH1) {
496
+ var sidebarH1 = s.querySelector('h1');
497
+ if (sidebarH1) {
498
+ var h1Html = mainH1.innerHTML;
499
+ if (!/<br[\s\/]*>/i.test(h1Html)) {
500
+ h1Html = mainH1.textContent.replace(/\s*[-_]\s*/g, '<br>');
501
+ }
502
+ sidebarH1.innerHTML = h1Html;
503
+ }
504
+ }
505
+
506
+ sidebarHtml = s.outerHTML;
507
+ }
508
+ } else {
509
+ // サイドバーが非表示の場合も更新
510
+ sidebarHtml = generateSidebarHtml();
511
+ }
512
+ }
513
+
514
+ // ========== 編集モード切り替え ==========
515
+ editModeBtn.addEventListener('click', function() {
516
+ isEditMode = !isEditMode;
517
+ if (isEditMode) {
518
+ container.classList.add('edit-mode');
519
+ editModeBtn.classList.add('active');
520
+ enableContentEditable();
521
+ } else {
522
+ container.classList.remove('edit-mode');
523
+ editModeBtn.classList.remove('active');
524
+ disableContentEditable();
525
+ updateSidebarNav();
526
+ }
527
+ });
528
+
529
+ function enableContentEditable() {
530
+ var editables = container.querySelectorAll('h1, h2, h3, h4, td, th, p, li, pre, code, blockquote');
531
+ editables.forEach(function(el) {
532
+ el.setAttribute('contenteditable', 'true');
533
+ });
534
+ }
535
+
536
+ // 空のリスト項目を自動削除
537
+ container.addEventListener('focusout', function(e) {
538
+ if (!isEditMode) return;
539
+ var el = e.target;
540
+ var tag = el.tagName;
541
+
542
+ // 空なら削除する対象
543
+ if (tag === 'LI') {
544
+ // justCreatedフラグがあれば削除せずフラグを消すだけ
545
+ if (el.dataset.justCreated) {
546
+ delete el.dataset.justCreated;
547
+ return;
548
+ }
549
+
550
+ // liの直接のテキストのみで判定(子リストは除外)
551
+ var directText = '';
552
+ el.childNodes.forEach(function(node) {
553
+ if (node.nodeType === 3) {
554
+ directText += node.textContent;
555
+ } else if (node.nodeType === 1 && !['UL', 'OL'].includes(node.tagName)) {
556
+ directText += node.textContent;
557
+ }
558
+ });
559
+
560
+ if (directText.trim() === '') {
561
+ var list = el.closest('ul, ol');
562
+ if (list && list.querySelectorAll(':scope > li').length > 1) {
563
+ el.remove();
564
+ } else if (list) {
565
+ list.remove();
566
+ }
567
+ }
568
+ } else if (el.textContent.trim() === '') {
569
+ if (['H2', 'H3', 'H4', 'P', 'BLOCKQUOTE'].includes(tag)) {
570
+ el.remove();
571
+ if (['H2', 'H3'].includes(tag)) {
572
+ updateSidebarNav();
573
+ }
574
+ } else if (tag === 'PRE') {
575
+ el.remove();
576
+ } else if (tag === 'CODE') {
577
+ var pre = el.closest('pre');
578
+ if (pre) pre.remove();
579
+ }
580
+ }
581
+ });
582
+
583
+ function disableContentEditable() {
584
+ var editables = container.querySelectorAll('[contenteditable="true"]');
585
+ editables.forEach(function(el) {
586
+ el.removeAttribute('contenteditable');
587
+ });
588
+ }
589
+
590
+ // ========== 右クリックメニュー ==========
591
+ var contextMenu = document.getElementById('contextMenu');
592
+ var h1ContextMenu = document.getElementById('h1ContextMenu');
593
+ var headingContextMenu = document.getElementById('headingContextMenu');
594
+ var paragraphContextMenu = document.getElementById('paragraphContextMenu');
595
+ var listContextMenu = document.getElementById('listContextMenu');
596
+ var preContextMenu = document.getElementById('preContextMenu');
597
+ var blockquoteContextMenu = document.getElementById('blockquoteContextMenu');
598
+ var contextTargetCell = null;
599
+ var contextTargetH1 = null;
600
+ var contextTargetHeading = null;
601
+ var contextTargetParagraph = null;
602
+ var contextTargetList = null;
603
+ var contextTargetLi = null;
604
+ var contextTargetPre = null;
605
+ var contextTargetBlockquote = null;
606
+
607
+ // メニュー表示位置を調整するヘルパー関数
608
+ function showContextMenu(menu, x, y) {
609
+ menu.style.left = x + 'px';
610
+ menu.style.top = y + 'px';
611
+ menu.classList.add('show');
612
+
613
+ // 表示後に実際のサイズで位置調整
614
+ var rect = menu.getBoundingClientRect();
615
+ if (rect.right > window.innerWidth) {
616
+ menu.style.left = (window.innerWidth - rect.width - 10) + 'px';
617
+ }
618
+ if (rect.bottom > window.innerHeight) {
619
+ menu.style.top = (window.innerHeight - rect.height - 10) + 'px';
620
+ }
621
+ }
622
+
623
+ container.addEventListener('contextmenu', function(e) {
624
+ if (!isEditMode) return;
625
+
626
+ contextMenu.classList.remove('show');
627
+ h1ContextMenu.classList.remove('show');
628
+ headingContextMenu.classList.remove('show');
629
+ paragraphContextMenu.classList.remove('show');
630
+ listContextMenu.classList.remove('show');
631
+ preContextMenu.classList.remove('show');
632
+ blockquoteContextMenu.classList.remove('show');
633
+
634
+ var x = e.clientX;
635
+ var y = e.clientY;
636
+
637
+ var h1 = e.target.closest('h1');
638
+ if (h1) {
639
+ e.preventDefault();
640
+ contextTargetH1 = h1;
641
+ showContextMenu(h1ContextMenu, x, y);
642
+ return;
643
+ }
644
+
645
+ var heading = e.target.closest('h2, h3, h4');
646
+ if (heading) {
647
+ e.preventDefault();
648
+ contextTargetHeading = heading;
649
+ showContextMenu(headingContextMenu, x, y);
650
+ return;
651
+ }
652
+
653
+ var cell = e.target.closest('td, th');
654
+ if (cell) {
655
+ e.preventDefault();
656
+ contextTargetCell = cell;
657
+ showContextMenu(contextMenu, x, y);
658
+ return;
659
+ }
660
+
661
+ var li = e.target.closest('li');
662
+ if (li) {
663
+ e.preventDefault();
664
+ contextTargetLi = li;
665
+ contextTargetList = li.closest('ul, ol');
666
+ showContextMenu(listContextMenu, x, y);
667
+ return;
668
+ }
669
+
670
+ var pre = e.target.closest('pre');
671
+ if (pre) {
672
+ e.preventDefault();
673
+ contextTargetPre = pre;
674
+ showContextMenu(preContextMenu, x, y);
675
+ return;
676
+ }
677
+
678
+ var blockquote = e.target.closest('blockquote');
679
+ if (blockquote) {
680
+ e.preventDefault();
681
+ contextTargetBlockquote = blockquote;
682
+ showContextMenu(blockquoteContextMenu, x, y);
683
+ return;
684
+ }
685
+
686
+ var paragraph = e.target.closest('p');
687
+ if (paragraph) {
688
+ e.preventDefault();
689
+ contextTargetParagraph = paragraph;
690
+ showContextMenu(paragraphContextMenu, x, y);
691
+ return;
692
+ }
693
+ });
694
+
695
+ document.addEventListener('click', function() {
696
+ contextMenu.classList.remove('show');
697
+ h1ContextMenu.classList.remove('show');
698
+ headingContextMenu.classList.remove('show');
699
+ paragraphContextMenu.classList.remove('show');
700
+ listContextMenu.classList.remove('show');
701
+ preContextMenu.classList.remove('show');
702
+ blockquoteContextMenu.classList.remove('show');
703
+ });
704
+
705
+ document.addEventListener('keydown', function(e) {
706
+ if (e.key === 'Escape') {
707
+ contextMenu.classList.remove('show');
708
+ h1ContextMenu.classList.remove('show');
709
+ headingContextMenu.classList.remove('show');
710
+ paragraphContextMenu.classList.remove('show');
711
+ listContextMenu.classList.remove('show');
712
+ preContextMenu.classList.remove('show');
713
+ blockquoteContextMenu.classList.remove('show');
714
+ }
715
+
716
+ // ↓↑キーで編集可能要素間を移動(複数行対応)
717
+ if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isEditMode) {
718
+ var sel = window.getSelection();
719
+ var currentEl = null;
720
+
721
+ if (sel.rangeCount > 0) {
722
+ var node = sel.anchorNode;
723
+ if (node.nodeType === 3) {
724
+ node = node.parentElement;
725
+ }
726
+ currentEl = node.closest('[contenteditable="true"]');
727
+ }
728
+
729
+ if (currentEl) {
730
+ // カーソルが最終行/先頭行にいるかチェック
731
+ var range = sel.getRangeAt(0);
732
+ var cursorRect = range.getBoundingClientRect();
733
+ var elementRect = currentEl.getBoundingClientRect();
734
+ var lineHeight = parseInt(window.getComputedStyle(currentEl).lineHeight) || 24;
735
+
736
+ var isAtLastLine = (elementRect.bottom - cursorRect.bottom) < lineHeight;
737
+ var isAtFirstLine = (cursorRect.top - elementRect.top) < lineHeight;
738
+
739
+ // ↓で最終行、または↑で先頭行のときだけ要素間移動
740
+ var shouldMove = (e.key === 'ArrowDown' && isAtLastLine) ||
741
+ (e.key === 'ArrowUp' && isAtFirstLine);
742
+
743
+ if (shouldMove) {
744
+ var allEditables = Array.from(container.querySelectorAll('[contenteditable="true"]'));
745
+ var currentIndex = allEditables.indexOf(currentEl);
746
+
747
+ var targetEl = null;
748
+ if (e.key === 'ArrowDown' && currentIndex < allEditables.length - 1) {
749
+ targetEl = allEditables[currentIndex + 1];
750
+ } else if (e.key === 'ArrowUp' && currentIndex > 0) {
751
+ targetEl = allEditables[currentIndex - 1];
752
+ }
753
+
754
+ if (targetEl) {
755
+ e.preventDefault();
756
+ targetEl.focus();
757
+ // Selection APIでカーソルを設定
758
+ var newRange = document.createRange();
759
+ var textNode = null;
760
+ for (var i = 0; i < targetEl.childNodes.length; i++) {
761
+ var cnode = targetEl.childNodes[i];
762
+ if (cnode.nodeType === 3 && cnode.textContent.trim()) {
763
+ textNode = cnode;
764
+ break;
765
+ }
766
+ }
767
+ if (textNode) {
768
+ newRange.setStart(textNode, 0);
769
+ newRange.collapse(true);
770
+ } else {
771
+ newRange.selectNodeContents(targetEl);
772
+ newRange.collapse(true);
773
+ }
774
+ sel.removeAllRanges();
775
+ sel.addRange(newRange);
776
+ }
777
+ }
778
+ // shouldMoveでない場合はデフォルト動作(行内移動)
779
+ }
780
+ }
781
+
782
+ if (e.ctrlKey && !e.shiftKey && !e.altKey) {
783
+ switch (e.key.toLowerCase()) {
784
+ case 'b':
785
+ e.preventDefault();
786
+ toggleSidebarBtn.click();
787
+ break;
788
+ case 'e':
789
+ e.preventDefault();
790
+ editModeBtn.click();
791
+ break;
792
+ case 'm':
793
+ e.preventDefault();
794
+ exportMdBtn.click();
795
+ break;
796
+ }
797
+ }
798
+
799
+ if (e.key === 'Enter' && isEditMode) {
800
+ var el = document.activeElement;
801
+ if (el && el.hasAttribute('contenteditable')) {
802
+ if (e.ctrlKey || e.shiftKey) {
803
+ // Ctrl + Enter または Shift + Enter
804
+ e.preventDefault();
805
+
806
+ // キャレット位置から最も近いliを取得
807
+ var targetLi = null;
808
+ var selection = window.getSelection();
809
+ if (selection.rangeCount > 0) {
810
+ var node = selection.anchorNode;
811
+ if (node.nodeType === 3) {
812
+ targetLi = node.parentElement.closest('li');
813
+ } else {
814
+ targetLi = node.closest('li');
815
+ }
816
+ }
817
+
818
+ if (targetLi) {
819
+ // リスト項目の場合は同じレベルに次の項目を追加
820
+ var newLi = document.createElement('li');
821
+ newLi.textContent = '';
822
+ newLi.setAttribute('contenteditable', 'true');
823
+ targetLi.after(newLi);
824
+ focusNewLi(newLi);
825
+ } else if (el.tagName === 'LI') {
826
+ var newLi = document.createElement('li');
827
+ newLi.textContent = '';
828
+ newLi.setAttribute('contenteditable', 'true');
829
+ el.after(newLi);
830
+ focusNewLi(newLi);
831
+ } else {
832
+ // その他は改行
833
+ document.execCommand('insertLineBreak');
834
+ }
835
+ } else {
836
+ // Enter のみ → 決定(blur)
837
+ e.preventDefault();
838
+ el.blur();
839
+ }
840
+ }
841
+ }
842
+
843
+ // Backspaceで空liを削除して前の要素に移動
844
+ if (e.key === 'Backspace' && isEditMode) {
845
+ var sel = window.getSelection();
846
+ var currentLi = null;
847
+
848
+ if (sel.rangeCount > 0) {
849
+ var node = sel.anchorNode;
850
+ if (node.nodeType === 3) {
851
+ node = node.parentElement;
852
+ }
853
+ currentLi = node.closest('li');
854
+ }
855
+
856
+ if (currentLi) {
857
+ // liの直接のテキストのみを取得(子リストは除外)
858
+ var directText = '';
859
+ currentLi.childNodes.forEach(function(n) {
860
+ if (n.nodeType === 3) {
861
+ directText += n.textContent;
862
+ } else if (n.nodeType === 1 && !['UL', 'OL'].includes(n.tagName)) {
863
+ directText += n.textContent;
864
+ }
865
+ });
866
+
867
+ // 空、または残り1文字でカーソルが先頭にある場合
868
+ var isAtStart = sel.anchorOffset === 0;
869
+ var isEmpty = directText.trim() === '';
870
+
871
+ if (isEmpty || (directText.trim().length === 1 && isAtStart)) {
872
+ // 前の要素を探す
873
+ var allEditables = Array.from(container.querySelectorAll('[contenteditable="true"]'));
874
+ var currentIndex = allEditables.indexOf(currentLi);
875
+
876
+ if (currentIndex > 0) {
877
+ e.preventDefault();
878
+ var prevEl = allEditables[currentIndex - 1];
879
+
880
+ // liを削除
881
+ var list = currentLi.closest('ul, ol');
882
+ if (list && list.querySelectorAll(':scope > li').length > 1) {
883
+ currentLi.remove();
884
+ } else if (list) {
885
+ // 最後の1つなら親リストごと削除
886
+ list.remove();
887
+ }
888
+
889
+ // 前の要素にフォーカス
890
+ prevEl.focus();
891
+ // カーソルを末尾に
892
+ var range = document.createRange();
893
+ range.selectNodeContents(prevEl);
894
+ range.collapse(false);
895
+ sel.removeAllRanges();
896
+ sel.addRange(range);
897
+ }
898
+ }
899
+ }
900
+ }
901
+ });
902
+
903
+ contextMenu.addEventListener('click', function(e) {
904
+ e.stopPropagation();
905
+ var item = e.target.closest('.context-menu-item');
906
+ if (!item || !contextTargetCell) return;
907
+
908
+ var action = item.dataset.action;
909
+ var table = contextTargetCell.closest('table');
910
+ var row = contextTargetCell.closest('tr');
911
+ var rowIndex = row.rowIndex;
912
+ var cellIndex = contextTargetCell.cellIndex;
913
+ var colCount = table.rows[0].cells.length;
914
+
915
+ switch (action) {
916
+ case 'addRowBelow':
917
+ var newRow = table.insertRow(rowIndex + 1);
918
+ for (var i = 0; i < colCount; i++) {
919
+ var cell = newRow.insertCell();
920
+ cell.textContent = '-';
921
+ if (isEditMode) cell.setAttribute('contenteditable', 'true');
922
+ }
923
+ break;
924
+
925
+ case 'addColRight':
926
+ for (var i = 0; i < table.rows.length; i++) {
927
+ var cell = table.rows[i].insertCell(cellIndex + 1);
928
+ if (i === 0) {
929
+ var th = document.createElement('th');
930
+ th.textContent = '新規列';
931
+ if (isEditMode) th.setAttribute('contenteditable', 'true');
932
+ cell.replaceWith(th);
933
+ } else {
934
+ cell.textContent = '-';
935
+ if (isEditMode) cell.setAttribute('contenteditable', 'true');
936
+ }
937
+ }
938
+ break;
939
+
940
+ case 'deleteRow':
941
+ if (table.rows.length > 2) {
942
+ table.deleteRow(rowIndex);
943
+ }
944
+ break;
945
+
946
+ case 'deleteCol':
947
+ if (colCount > 2) {
948
+ for (var i = 0; i < table.rows.length; i++) {
949
+ table.rows[i].deleteCell(cellIndex);
950
+ }
951
+ }
952
+ break;
953
+
954
+ case 'deleteTable':
955
+ table.remove();
956
+ break;
957
+
958
+ case 'addH2':
959
+ insertElement(table, createHeading(2), true);
960
+ break;
961
+ case 'addH3':
962
+ insertElement(table, createHeading(3), true);
963
+ break;
964
+ case 'addH4':
965
+ insertElement(table, createHeading(4), true);
966
+ break;
967
+ case 'addTableBelow':
968
+ insertElement(table, createTable(), true);
969
+ break;
970
+ case 'addParagraph':
971
+ insertElement(table, createParagraph(), true);
972
+ break;
973
+ case 'addUl':
974
+ insertElement(table, createList(false), true);
975
+ break;
976
+ case 'addOl':
977
+ insertElement(table, createList(true), true);
978
+ break;
979
+ case 'addPre':
980
+ insertElement(table, createCodeBlock(), true);
981
+ break;
982
+ case 'addBlockquote':
983
+ insertElement(table, createBlockquote(), true);
984
+ break;
985
+ }
986
+
987
+ contextMenu.classList.remove('show');
988
+ contextTargetCell = null;
989
+ });
990
+
991
+ h1ContextMenu.addEventListener('click', function(e) {
992
+ e.stopPropagation();
993
+ var item = e.target.closest('.context-menu-item');
994
+ if (!item || !contextTargetH1) return;
995
+
996
+ var action = item.dataset.action;
997
+ var target = contextTargetH1;
998
+
999
+ switch (action) {
1000
+ case 'addH2':
1001
+ insertElement(target, createHeading(2), true);
1002
+ break;
1003
+ case 'addTable':
1004
+ insertElement(target, createTable(), true);
1005
+ break;
1006
+ case 'addParagraph':
1007
+ insertElement(target, createParagraph(), true);
1008
+ break;
1009
+ case 'addUl':
1010
+ insertElement(target, createList(false), true);
1011
+ break;
1012
+ case 'addOl':
1013
+ insertElement(target, createList(true), true);
1014
+ break;
1015
+ case 'addPre':
1016
+ insertElement(target, createCodeBlock(), true);
1017
+ break;
1018
+ case 'addBlockquote':
1019
+ insertElement(target, createBlockquote(), true);
1020
+ break;
1021
+ }
1022
+
1023
+ h1ContextMenu.classList.remove('show');
1024
+ contextTargetH1 = null;
1025
+ });
1026
+
1027
+ headingContextMenu.addEventListener('click', function(e) {
1028
+ e.stopPropagation();
1029
+ var item = e.target.closest('.context-menu-item');
1030
+ if (!item || !contextTargetHeading) return;
1031
+
1032
+ var action = item.dataset.action;
1033
+ var target = contextTargetHeading;
1034
+
1035
+ switch (action) {
1036
+ case 'addH2':
1037
+ insertElement(target, createHeading(2), true);
1038
+ break;
1039
+ case 'addH3':
1040
+ insertElement(target, createHeading(3), true);
1041
+ break;
1042
+ case 'addH4':
1043
+ insertElement(target, createHeading(4), true);
1044
+ break;
1045
+ case 'addTable':
1046
+ insertElement(target, createTable(), true);
1047
+ break;
1048
+ case 'addParagraph':
1049
+ insertElement(target, createParagraph(), true);
1050
+ break;
1051
+ case 'addUl':
1052
+ insertElement(target, createList(false), true);
1053
+ break;
1054
+ case 'addOl':
1055
+ insertElement(target, createList(true), true);
1056
+ break;
1057
+ case 'addPre':
1058
+ insertElement(target, createCodeBlock(), true);
1059
+ break;
1060
+ case 'addBlockquote':
1061
+ insertElement(target, createBlockquote(), true);
1062
+ break;
1063
+ case 'deleteSection':
1064
+ var heading = contextTargetHeading;
1065
+ var level = parseInt(heading.tagName[1]);
1066
+ var next = heading.nextElementSibling;
1067
+
1068
+ while (next) {
1069
+ var nextTag = next.tagName;
1070
+ if (nextTag === 'HR') break;
1071
+ if (nextTag && nextTag.match(/^H[2-4]$/)) {
1072
+ var nextLevel = parseInt(nextTag[1]);
1073
+ if (nextLevel <= level) break;
1074
+ }
1075
+
1076
+ var toRemove = next;
1077
+ next = next.nextElementSibling;
1078
+ toRemove.remove();
1079
+ }
1080
+
1081
+ heading.remove();
1082
+ updateSidebarNav();
1083
+ break;
1084
+ }
1085
+
1086
+ headingContextMenu.classList.remove('show');
1087
+ contextTargetHeading = null;
1088
+ });
1089
+
1090
+ paragraphContextMenu.addEventListener('click', function(e) {
1091
+ e.stopPropagation();
1092
+ var item = e.target.closest('.context-menu-item');
1093
+ if (!item || !contextTargetParagraph) return;
1094
+
1095
+ var action = item.dataset.action;
1096
+ var target = contextTargetParagraph;
1097
+
1098
+ switch (action) {
1099
+ case 'addH2':
1100
+ insertElement(target, createHeading(2), true);
1101
+ break;
1102
+ case 'addH3':
1103
+ insertElement(target, createHeading(3), true);
1104
+ break;
1105
+ case 'addH4':
1106
+ insertElement(target, createHeading(4), true);
1107
+ break;
1108
+ case 'addTable':
1109
+ insertElement(target, createTable(), true);
1110
+ break;
1111
+ case 'addParagraph':
1112
+ insertElement(target, createParagraph(), true);
1113
+ break;
1114
+ case 'addUl':
1115
+ insertElement(target, createList(false), true);
1116
+ break;
1117
+ case 'addOl':
1118
+ insertElement(target, createList(true), true);
1119
+ break;
1120
+ case 'addPre':
1121
+ insertElement(target, createCodeBlock(), true);
1122
+ break;
1123
+ case 'addBlockquote':
1124
+ insertElement(target, createBlockquote(), true);
1125
+ break;
1126
+ case 'deleteParagraph':
1127
+ contextTargetParagraph.remove();
1128
+ break;
1129
+ }
1130
+
1131
+ paragraphContextMenu.classList.remove('show');
1132
+ contextTargetParagraph = null;
1133
+ });
1134
+
1135
+ listContextMenu.addEventListener('click', function(e) {
1136
+ e.stopPropagation();
1137
+ var item = e.target.closest('.context-menu-item');
1138
+ if (!item || !contextTargetList) return;
1139
+
1140
+ var action = item.dataset.action;
1141
+ var target = contextTargetList;
1142
+
1143
+ switch (action) {
1144
+ case 'addH2':
1145
+ insertElement(target, createHeading(2), true);
1146
+ break;
1147
+ case 'addH3':
1148
+ insertElement(target, createHeading(3), true);
1149
+ break;
1150
+ case 'addH4':
1151
+ insertElement(target, createHeading(4), true);
1152
+ break;
1153
+ case 'addTable':
1154
+ insertElement(target, createTable(), true);
1155
+ break;
1156
+ case 'addParagraph':
1157
+ insertElement(target, createParagraph(), true);
1158
+ break;
1159
+ case 'addUl':
1160
+ // li内にネストしたulを追加
1161
+ if (contextTargetLi) {
1162
+ var nestedUl = createList(false);
1163
+ contextTargetLi.appendChild(nestedUl);
1164
+ nestedUl.querySelector('li').focus();
1165
+ } else {
1166
+ insertElement(target, createList(false), true);
1167
+ }
1168
+ break;
1169
+ case 'addOl':
1170
+ // li内にネストしたolを追加
1171
+ if (contextTargetLi) {
1172
+ var nestedOl = createList(true);
1173
+ contextTargetLi.appendChild(nestedOl);
1174
+ nestedOl.querySelector('li').focus();
1175
+ } else {
1176
+ insertElement(target, createList(true), true);
1177
+ }
1178
+ break;
1179
+ case 'addPre':
1180
+ insertElement(target, createCodeBlock(), true);
1181
+ break;
1182
+ case 'addBlockquote':
1183
+ insertElement(target, createBlockquote(), true);
1184
+ break;
1185
+ case 'deleteList':
1186
+ target.remove();
1187
+ break;
1188
+ }
1189
+
1190
+ listContextMenu.classList.remove('show');
1191
+ contextTargetList = null;
1192
+ contextTargetLi = null;
1193
+ });
1194
+
1195
+ preContextMenu.addEventListener('click', function(e) {
1196
+ e.stopPropagation();
1197
+ var item = e.target.closest('.context-menu-item');
1198
+ if (!item || !contextTargetPre) return;
1199
+
1200
+ var action = item.dataset.action;
1201
+ var target = contextTargetPre;
1202
+
1203
+ switch (action) {
1204
+ case 'addH2':
1205
+ insertElement(target, createHeading(2), true);
1206
+ break;
1207
+ case 'addH3':
1208
+ insertElement(target, createHeading(3), true);
1209
+ break;
1210
+ case 'addH4':
1211
+ insertElement(target, createHeading(4), true);
1212
+ break;
1213
+ case 'addTable':
1214
+ insertElement(target, createTable(), true);
1215
+ break;
1216
+ case 'addParagraph':
1217
+ insertElement(target, createParagraph(), true);
1218
+ break;
1219
+ case 'addUl':
1220
+ insertElement(target, createList(false), true);
1221
+ break;
1222
+ case 'addOl':
1223
+ insertElement(target, createList(true), true);
1224
+ break;
1225
+ case 'addPre':
1226
+ insertElement(target, createCodeBlock(), true);
1227
+ break;
1228
+ case 'addBlockquote':
1229
+ insertElement(target, createBlockquote(), true);
1230
+ break;
1231
+ case 'deletePre':
1232
+ target.remove();
1233
+ break;
1234
+ }
1235
+
1236
+ preContextMenu.classList.remove('show');
1237
+ contextTargetPre = null;
1238
+ });
1239
+
1240
+ blockquoteContextMenu.addEventListener('click', function(e) {
1241
+ e.stopPropagation();
1242
+ var item = e.target.closest('.context-menu-item');
1243
+ if (!item || !contextTargetBlockquote) return;
1244
+
1245
+ var action = item.dataset.action;
1246
+ var target = contextTargetBlockquote;
1247
+
1248
+ switch (action) {
1249
+ case 'addH2':
1250
+ insertElement(target, createHeading(2), true);
1251
+ break;
1252
+ case 'addH3':
1253
+ insertElement(target, createHeading(3), true);
1254
+ break;
1255
+ case 'addH4':
1256
+ insertElement(target, createHeading(4), true);
1257
+ break;
1258
+ case 'addTable':
1259
+ insertElement(target, createTable(), true);
1260
+ break;
1261
+ case 'addParagraph':
1262
+ insertElement(target, createParagraph(), true);
1263
+ break;
1264
+ case 'addUl':
1265
+ insertElement(target, createList(false), true);
1266
+ break;
1267
+ case 'addOl':
1268
+ insertElement(target, createList(true), true);
1269
+ break;
1270
+ case 'addPre':
1271
+ insertElement(target, createCodeBlock(), true);
1272
+ break;
1273
+ case 'addBlockquote':
1274
+ insertElement(target, createBlockquote(), true);
1275
+ break;
1276
+ case 'deleteBlockquote':
1277
+ target.remove();
1278
+ break;
1279
+ }
1280
+
1281
+ blockquoteContextMenu.classList.remove('show');
1282
+ contextTargetBlockquote = null;
1283
+ });
1284
+
1285
+ function createHeading(level) {
1286
+ var tagName = 'h' + level;
1287
+ var id = 'section-new-' + (++sectionCounter['h' + level]);
1288
+
1289
+ var heading = document.createElement(tagName);
1290
+ heading.id = id;
1291
+ heading.textContent = '新しいセクション';
1292
+ heading.setAttribute('contenteditable', 'true');
1293
+ return heading;
1294
+ }
1295
+
1296
+ function createTable() {
1297
+ var table = document.createElement('table');
1298
+ table.className = 'table-3col';
1299
+ table.innerHTML = '<tr><th>項目1</th><th>項目2</th><th>項目3</th></tr><tr><td>-</td><td>-</td><td>-</td></tr>';
1300
+
1301
+ if (isEditMode) {
1302
+ table.querySelectorAll('th, td').forEach(function(cell) {
1303
+ cell.setAttribute('contenteditable', 'true');
1304
+ });
1305
+ }
1306
+ return table;
1307
+ }
1308
+
1309
+ function createParagraph() {
1310
+ var p = document.createElement('p');
1311
+ p.textContent = '新しい本文';
1312
+ p.setAttribute('contenteditable', 'true');
1313
+ return p;
1314
+ }
1315
+
1316
+ function createList(ordered) {
1317
+ var list = document.createElement(ordered ? 'ol' : 'ul');
1318
+ var li = document.createElement('li');
1319
+ li.textContent = '項目1';
1320
+ li.setAttribute('contenteditable', 'true');
1321
+ list.appendChild(li);
1322
+ return list;
1323
+ }
1324
+
1325
+ function createCodeBlock() {
1326
+ var pre = document.createElement('pre');
1327
+ var code = document.createElement('code');
1328
+ code.textContent = '// コードを入力';
1329
+ code.setAttribute('contenteditable', 'true');
1330
+ pre.appendChild(code);
1331
+ return pre;
1332
+ }
1333
+
1334
+ function createBlockquote() {
1335
+ var bq = document.createElement('blockquote');
1336
+ bq.textContent = '引用文を入力';
1337
+ bq.setAttribute('contenteditable', 'true');
1338
+ return bq;
1339
+ }
1340
+
1341
+ function insertElement(target, element, insertAfter) {
1342
+ if (insertAfter) {
1343
+ target.after(element);
1344
+ } else {
1345
+ target.before(element);
1346
+ }
1347
+
1348
+ if (element.matches('h2, h3, h4, p')) {
1349
+ setTimeout(function() {
1350
+ element.focus();
1351
+ var range = document.createRange();
1352
+ range.selectNodeContents(element);
1353
+ var sel = window.getSelection();
1354
+ sel.removeAllRanges();
1355
+ sel.addRange(range);
1356
+ }, 0);
1357
+ }
1358
+
1359
+ if (element.matches('h2, h3, h4')) {
1360
+ updateSidebarNav();
1361
+ }
1362
+ }
1363
+
1364
+ // ========== PDF出力 ==========
1365
+ exportPdfBtn.addEventListener('click', async function() {
1366
+ var wasEditMode = isEditMode;
1367
+ if (isEditMode) editModeBtn.click();
1368
+
1369
+ exportPdfBtn.textContent = '...';
1370
+
1371
+ try {
1372
+ // jsPDFインスタンス作成
1373
+ var { jsPDF } = window.jspdf;
1374
+ var pdf = new jsPDF({
1375
+ unit: 'mm',
1376
+ format: 'a4',
1377
+ orientation: 'portrait'
1378
+ });
1379
+
1380
+ var pageWidth = 210;
1381
+ var pageHeight = 297;
1382
+ var margin = 15;
1383
+ var contentWidth = pageWidth - margin * 2;
1384
+ var contentHeight = pageHeight - margin * 2;
1385
+ var currentY = margin;
1386
+ var currentPage = 1;
1387
+
1388
+ // しおり情報
1389
+ var bookmarks = [];
1390
+
1391
+ // コンテンツを複製してPDF用に整形
1392
+ var content = container.cloneNode(true);
1393
+
1394
+ // ツールバーと右クリックメニューを除去
1395
+ var toolbar = content.querySelector('.toolbar');
1396
+ if (toolbar) toolbar.remove();
1397
+ content.querySelectorAll('.context-menu').forEach(function(menu) {
1398
+ menu.remove();
1399
+ });
1400
+ content.querySelectorAll('[contenteditable]').forEach(function(el) {
1401
+ el.removeAttribute('contenteditable');
1402
+ });
1403
+
1404
+ // 一時的にDOMに追加(html2canvas用)
1405
+ content.style.position = 'absolute';
1406
+ content.style.left = '-9999px';
1407
+ content.style.width = contentWidth * 3.78 + 'px'; // mm to px
1408
+ content.style.background = 'white';
1409
+ content.style.padding = '0';
1410
+ document.body.appendChild(content);
1411
+
1412
+ // h2で分割してセクションごとに処理
1413
+ var allElements = content.querySelectorAll('h1, h2, h3, h4, p, ul, ol, pre, blockquote, table, hr');
1414
+ var sections = [];
1415
+ var currentSection = { elements: [], h2Text: null, h3List: [] };
1416
+
1417
+ allElements.forEach(function(el) {
1418
+ // ネストされたリストはスキップ(親リストで処理される)
1419
+ if ((el.tagName === 'UL' || el.tagName === 'OL') && el.parentElement.closest('ul, ol')) {
1420
+ return;
1421
+ }
1422
+
1423
+ if (el.tagName === 'H2') {
1424
+ if (currentSection.elements.length > 0 || currentSection.h2Text) {
1425
+ sections.push(currentSection);
1426
+ }
1427
+ currentSection = { elements: [el], h2Text: el.textContent.trim(), h3List: [] };
1428
+ } else if (el.tagName === 'H3') {
1429
+ currentSection.elements.push(el);
1430
+ currentSection.h3List.push({ text: el.textContent.trim(), pageNum: null });
1431
+ } else {
1432
+ currentSection.elements.push(el);
1433
+ }
1434
+ });
1435
+ if (currentSection.elements.length > 0) {
1436
+ sections.push(currentSection);
1437
+ }
1438
+
1439
+ // 各セクションを処理
1440
+ for (var i = 0; i < sections.length; i++) {
1441
+ var section = sections[i];
1442
+
1443
+ // セクション内の要素を一つのdivにまとめる
1444
+ var sectionDiv = document.createElement('div');
1445
+ sectionDiv.style.background = 'white';
1446
+ sectionDiv.style.padding = '10px';
1447
+ section.elements.forEach(function(el) {
1448
+ sectionDiv.appendChild(el.cloneNode(true));
1449
+ });
1450
+ content.innerHTML = '';
1451
+ content.appendChild(sectionDiv);
1452
+
1453
+ // html2canvasでキャプチャ
1454
+ var canvas = await html2canvas(sectionDiv, {
1455
+ scale: 2,
1456
+ useCORS: true,
1457
+ backgroundColor: '#ffffff'
1458
+ });
1459
+
1460
+ var imgData = canvas.toDataURL('image/jpeg', 0.95);
1461
+ var imgWidth = contentWidth;
1462
+ var imgHeight = (canvas.height * imgWidth) / canvas.width;
1463
+
1464
+ // ページに収まるかチェック
1465
+ if (currentY + imgHeight > pageHeight - margin) {
1466
+ // 新しいページを追加
1467
+ pdf.addPage();
1468
+ currentPage++;
1469
+ currentY = margin;
1470
+ }
1471
+
1472
+ // セクションのh2をしおりに追加(ページ確定後)
1473
+ if (section.h2Text) {
1474
+ bookmarks.push({
1475
+ text: section.h2Text,
1476
+ page: currentPage,
1477
+ level: 1,
1478
+ children: []
1479
+ });
1480
+ }
1481
+
1482
+ // h3のページ番号を記録
1483
+ if (section.h3List.length > 0 && bookmarks.length > 0) {
1484
+ var parentBookmark = bookmarks[bookmarks.length - 1];
1485
+ section.h3List.forEach(function(h3) {
1486
+ parentBookmark.children.push({
1487
+ text: h3.text,
1488
+ page: currentPage,
1489
+ level: 2
1490
+ });
1491
+ });
1492
+ }
1493
+
1494
+ // 画像が大きすぎる場合は分割
1495
+ if (imgHeight > contentHeight) {
1496
+ var remainingHeight = imgHeight;
1497
+ var sourceY = 0;
1498
+
1499
+ while (remainingHeight > 0) {
1500
+ var drawHeight = Math.min(contentHeight - (currentY - margin), remainingHeight);
1501
+ var drawHeightPx = (drawHeight / imgHeight) * canvas.height;
1502
+
1503
+ // キャンバスの一部を切り出し
1504
+ var partCanvas = document.createElement('canvas');
1505
+ partCanvas.width = canvas.width;
1506
+ partCanvas.height = drawHeightPx;
1507
+ var ctx = partCanvas.getContext('2d');
1508
+ ctx.drawImage(canvas, 0, sourceY, canvas.width, drawHeightPx, 0, 0, canvas.width, drawHeightPx);
1509
+
1510
+ var partImgData = partCanvas.toDataURL('image/jpeg', 0.95);
1511
+ pdf.addImage(partImgData, 'JPEG', margin, currentY, imgWidth, drawHeight);
1512
+
1513
+ sourceY += drawHeightPx;
1514
+ remainingHeight -= drawHeight;
1515
+
1516
+ if (remainingHeight > 0) {
1517
+ pdf.addPage();
1518
+ currentPage++;
1519
+ currentY = margin;
1520
+ } else {
1521
+ currentY += drawHeight + 5;
1522
+ }
1523
+ }
1524
+ } else {
1525
+ pdf.addImage(imgData, 'JPEG', margin, currentY, imgWidth, imgHeight);
1526
+ currentY += imgHeight + 5;
1527
+ }
1528
+ }
1529
+
1530
+ // 一時要素を削除
1531
+ document.body.removeChild(content);
1532
+
1533
+ // しおりを追加
1534
+ bookmarks.forEach(function(bm) {
1535
+ var parent = pdf.outline.add(null, bm.text, { pageNumber: bm.page });
1536
+ bm.children.forEach(function(child) {
1537
+ pdf.outline.add(parent, child.text, { pageNumber: child.page });
1538
+ });
1539
+ });
1540
+
1541
+ // タイトル取得
1542
+ var h1 = container.querySelector('h1');
1543
+ var fileName = h1 ? h1.textContent.trim().replace(/[\\/:*?"<>|]/g, '_') : 'document';
1544
+
1545
+ // PDF保存
1546
+ pdf.save(fileName + '.pdf');
1547
+
1548
+ } catch (error) {
1549
+ console.error('PDF生成エラー:', error);
1550
+ alert('PDF生成中にエラーが発生しました: ' + error.message);
1551
+ }
1552
+
1553
+ exportPdfBtn.textContent = '📕';
1554
+ if (wasEditMode) editModeBtn.click();
1555
+ });
1556
+
1557
+ // ========== Excel出力 ==========
1558
+ var fontSizes = {
1559
+ heading: 14,
1560
+ tableHeader: 13,
1561
+ body: 12
1562
+ };
1563
+
1564
+ exportExcelBtn.addEventListener('click', async function() {
1565
+ try {
1566
+ await convertToExcel();
1567
+ } catch (error) {
1568
+ console.error('Excel変換エラー:', error);
1569
+ alert('Excel変換中にエラーが発生しました: ' + error.message);
1570
+ }
1571
+ });
1572
+
1573
+ async function convertToExcel() {
1574
+ var content = container.querySelector('.content');
1575
+ if (!content) content = container;
1576
+
1577
+ var sections = parseHtmlStructure(content);
1578
+
1579
+ if (sections.length === 0) {
1580
+ alert('処理可能な構造が見つかりません');
1581
+ return;
1582
+ }
1583
+
1584
+ var workbook = new ExcelJS.Workbook();
1585
+ workbook.creator = 'Design Editor';
1586
+ workbook.created = new Date();
1587
+ workbook.modified = new Date();
1588
+
1589
+ for (var i = 0; i < sections.length; i++) {
1590
+ var section = sections[i];
1591
+ var sheetName = sanitizeSheetName(section.title);
1592
+
1593
+ var existingNames = workbook.worksheets.map(function(ws) { return ws.name; });
1594
+ var nameCounter = 1;
1595
+ var originalSheetName = sheetName;
1596
+
1597
+ while (existingNames.includes(sheetName)) {
1598
+ sheetName = originalSheetName + '_' + nameCounter;
1599
+ nameCounter++;
1600
+ }
1601
+
1602
+ var worksheet = workbook.addWorksheet(sheetName);
1603
+ await processSection(worksheet, section);
1604
+ }
1605
+
1606
+ var buffer = await workbook.xlsx.writeBuffer();
1607
+ var blob = new Blob([buffer], {
1608
+ type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
1609
+ });
1610
+
1611
+ var h1Element = container.querySelector('h1');
1612
+ var fileName = 'design_spec';
1613
+ if (h1Element) {
1614
+ fileName = h1Element.innerHTML
1615
+ .replace(/<br\s*\/?>/gi, '-')
1616
+ .replace(/<\/p>\s*<p>/gi, '-')
1617
+ .replace(/<[^>]*>/g, '')
1618
+ .replace(/\s+/g, '')
1619
+ .trim();
1620
+ }
1621
+
1622
+ var url = window.URL.createObjectURL(blob);
1623
+ var link = document.createElement('a');
1624
+ link.href = url;
1625
+ link.download = fileName + '.xlsx';
1626
+ document.body.appendChild(link);
1627
+ link.click();
1628
+ document.body.removeChild(link);
1629
+ window.URL.revokeObjectURL(url);
1630
+ }
1631
+
1632
+ function parseHtmlStructure(content) {
1633
+ var sections = [];
1634
+ var h2Elements = content.querySelectorAll('h2');
1635
+
1636
+ if (h2Elements.length === 0) {
1637
+ var h1 = content.querySelector('h1');
1638
+ var title = h1 ? h1.textContent.trim() : 'Sheet1';
1639
+ var elements = content.querySelectorAll('h1, h2, h3, h4, p, table, hr, ul, ol, pre, blockquote');
1640
+ sections.push({
1641
+ title: title,
1642
+ elements: Array.from(elements),
1643
+ h2Count: 0,
1644
+ tableCount: content.querySelectorAll('table').length
1645
+ });
1646
+ return sections;
1647
+ }
1648
+
1649
+ h2Elements.forEach(function(h2, index) {
1650
+ var title = h2.textContent.trim();
1651
+ var elements = [h2];
1652
+ var next = h2.nextElementSibling;
1653
+
1654
+ while (next && next.tagName !== 'H2' && next.tagName !== 'HR') {
1655
+ if (['H3', 'H4', 'P', 'TABLE', 'UL', 'OL', 'PRE', 'BLOCKQUOTE'].includes(next.tagName)) {
1656
+ elements.push(next);
1657
+ }
1658
+ next = next.nextElementSibling;
1659
+ }
1660
+
1661
+ sections.push({
1662
+ title: title,
1663
+ elements: elements,
1664
+ h2Count: 1,
1665
+ tableCount: elements.filter(function(el) { return el.tagName === 'TABLE'; }).length
1666
+ });
1667
+ });
1668
+
1669
+ return sections;
1670
+ }
1671
+
1672
+ async function processSection(worksheet, section) {
1673
+ var currentRow = 1;
1674
+ var isFirstH2 = true;
1675
+
1676
+ var maxColumns = 3;
1677
+ section.elements.forEach(function(element) {
1678
+ if (element.tagName === 'TABLE') {
1679
+ var rows = element.querySelectorAll('tr');
1680
+ rows.forEach(function(tr) {
1681
+ var cells = tr.querySelectorAll('th, td');
1682
+ var colCount = 0;
1683
+ cells.forEach(function(cell) {
1684
+ var colspan = parseInt(cell.getAttribute('colspan')) || 1;
1685
+ colCount += colspan;
1686
+ });
1687
+ maxColumns = Math.max(maxColumns, colCount);
1688
+ });
1689
+ }
1690
+ });
1691
+
1692
+ for (var i = 0; i < section.elements.length; i++) {
1693
+ var element = section.elements[i];
1694
+
1695
+ if (element.tagName === 'H2' && isFirstH2) {
1696
+ isFirstH2 = false;
1697
+ // h2を1行目に出力
1698
+ var h2Text = element.textContent.trim();
1699
+ var h2Styles = getElementStyles(element);
1700
+
1701
+ for (var col = 1; col <= maxColumns; col++) {
1702
+ var h2Cell = worksheet.getCell(currentRow, col);
1703
+ if (col === 1) h2Cell.value = h2Text;
1704
+ h2Cell.font = {
1705
+ name: 'メイリオ',
1706
+ size: 18,
1707
+ bold: true,
1708
+ color: { argb: h2Styles.textArgb || 'FF1A2332' }
1709
+ };
1710
+ if (h2Styles.bgArgb) {
1711
+ h2Cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: h2Styles.bgArgb } };
1712
+ }
1713
+ h2Cell.alignment = { vertical: 'middle', horizontal: 'left', wrapText: true };
1714
+ h2Cell.border = {
1715
+ bottom: { style: 'medium', color: { argb: 'FF0F2733' } }
1716
+ };
1717
+ }
1718
+ worksheet.getRow(currentRow).height = 30;
1719
+ currentRow += 2;
1720
+ continue;
1721
+ }
1722
+
1723
+ currentRow = await processElement(worksheet, element, currentRow, maxColumns);
1724
+ }
1725
+
1726
+ var MAX_COLUMN_WIDTH = 110;
1727
+ var MIN_COLUMN_WIDTH = 10;
1728
+
1729
+ var actualMaxCol = 0;
1730
+ worksheet.eachRow(function(row) {
1731
+ if (row.actualCellCount > actualMaxCol) {
1732
+ actualMaxCol = row.actualCellCount;
1733
+ }
1734
+ });
1735
+
1736
+ for (var colNum = 1; colNum <= actualMaxCol; colNum++) {
1737
+ var maxWidth = 0;
1738
+
1739
+ worksheet.getColumn(colNum).eachCell({ includeEmpty: false }, function(cell) {
1740
+ if (cell.value) {
1741
+ var cellText = cell.value.toString();
1742
+ var cellFontSize = fontSizes.body;
1743
+ if (cell.font && cell.font.size) {
1744
+ cellFontSize = cell.font.size;
1745
+ }
1746
+
1747
+ var lines = cellText.split(/\r\n|\n|\r/);
1748
+ lines.forEach(function(line) {
1749
+ var lineWidth = getTextWidthWithFontSize(line, cellFontSize);
1750
+ maxWidth = Math.max(maxWidth, lineWidth);
1751
+ });
1752
+ }
1753
+ });
1754
+
1755
+ if (maxWidth > 0) {
1756
+ var optimalWidth = Math.ceil(maxWidth * 1.0) + 4;
1757
+ worksheet.getColumn(colNum).width = Math.min(Math.max(optimalWidth, MIN_COLUMN_WIDTH), MAX_COLUMN_WIDTH);
1758
+ }
1759
+ }
1760
+ }
1761
+
1762
+ // リストをExcel用テキストに変換(再帰対応)
1763
+ function processListToExcelText(listElement, indent, level) {
1764
+ level = level || 1;
1765
+ var lines = [];
1766
+ var items = listElement.children;
1767
+ var isOrdered = listElement.tagName === 'OL';
1768
+ var counter = 1;
1769
+
1770
+ // 階層に応じたulマーカー
1771
+ var ulMarkers = ['●', '○', '■'];
1772
+ var ulMarker = ulMarkers[Math.min(level - 1, 2)] + ' ';
1773
+
1774
+ for (var i = 0; i < items.length; i++) {
1775
+ var li = items[i];
1776
+ if (li.tagName !== 'LI') continue;
1777
+
1778
+ var text = '';
1779
+ li.childNodes.forEach(function(node) {
1780
+ if (node.nodeType === 3) {
1781
+ text += node.textContent;
1782
+ } else if (node.nodeType === 1 && !['UL', 'OL'].includes(node.tagName)) {
1783
+ text += node.textContent;
1784
+ }
1785
+ });
1786
+ text = text.trim();
1787
+
1788
+ var marker = isOrdered ? (counter++ + '. ') : ulMarker;
1789
+ lines.push(indent + marker + text);
1790
+
1791
+ var childList = li.querySelector('ul, ol');
1792
+ if (childList && childList.parentElement === li) {
1793
+ var childLines = processListToExcelText(childList, indent + ' ', level + 1);
1794
+ lines = lines.concat(childLines);
1795
+ }
1796
+ }
1797
+
1798
+ return lines;
1799
+ }
1800
+
1801
+ async function processElement(worksheet, element, currentRow, maxColumns) {
1802
+ var tag = element.tagName;
1803
+
1804
+ switch (tag) {
1805
+ case 'H2':
1806
+ currentRow++;
1807
+ var h2Text = element.textContent.trim();
1808
+ var h2Styles = getElementStyles(element);
1809
+
1810
+ for (var col = 1; col <= maxColumns; col++) {
1811
+ var h2Cell = worksheet.getCell(currentRow, col);
1812
+ if (col === 1) h2Cell.value = h2Text;
1813
+ h2Cell.font = { name: 'メイリオ', size: 15, bold: true, color: { argb: h2Styles.textArgb || 'FFFFFFFF' } };
1814
+ if (h2Styles.bgArgb) {
1815
+ h2Cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: h2Styles.bgArgb } };
1816
+ }
1817
+ h2Cell.alignment = { vertical: 'middle', horizontal: 'left', wrapText: true };
1818
+ h2Cell.border = {
1819
+ top: { style: 'medium' },
1820
+ bottom: { style: 'medium' },
1821
+ left: col === 1 ? { style: 'medium' } : undefined,
1822
+ right: col === maxColumns ? { style: 'medium' } : undefined
1823
+ };
1824
+ }
1825
+ currentRow += 2;
1826
+ break;
1827
+
1828
+ case 'H3':
1829
+ var h3Text = element.textContent.trim();
1830
+ var h3Styles = getElementStyles(element);
1831
+
1832
+ for (var col = 1; col <= maxColumns; col++) {
1833
+ var h3Cell = worksheet.getCell(currentRow, col);
1834
+ if (col === 1) h3Cell.value = h3Text;
1835
+ h3Cell.font = { name: 'メイリオ', size: fontSizes.heading, bold: true, color: { argb: h3Styles.textArgb || 'FFFFFFFF' } };
1836
+ if (h3Styles.bgArgb) {
1837
+ h3Cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: h3Styles.bgArgb } };
1838
+ }
1839
+ h3Cell.alignment = { vertical: 'middle', horizontal: 'left', wrapText: true };
1840
+ h3Cell.border = {
1841
+ top: { style: 'medium' },
1842
+ bottom: { style: 'medium' },
1843
+ left: col === 1 ? { style: 'medium' } : undefined,
1844
+ right: col === maxColumns ? { style: 'medium' } : undefined
1845
+ };
1846
+ }
1847
+ currentRow++;
1848
+ break;
1849
+
1850
+ case 'H4':
1851
+ var h4Text = element.textContent.trim();
1852
+ var h4Styles = getElementStyles(element);
1853
+
1854
+ for (var col = 1; col <= maxColumns; col++) {
1855
+ var h4Cell = worksheet.getCell(currentRow, col);
1856
+ if (col === 1) h4Cell.value = h4Text;
1857
+ h4Cell.font = { name: 'メイリオ', size: fontSizes.heading, bold: true, color: { argb: h4Styles.textArgb || 'FF1A3442' } };
1858
+ if (h4Styles.bgArgb) {
1859
+ h4Cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: h4Styles.bgArgb } };
1860
+ }
1861
+ h4Cell.alignment = { vertical: 'middle', horizontal: 'left', wrapText: true };
1862
+ h4Cell.border = {
1863
+ top: { style: 'thin' },
1864
+ bottom: { style: 'thin' },
1865
+ left: col === 1 ? { style: 'thin' } : undefined,
1866
+ right: col === maxColumns ? { style: 'thin' } : undefined
1867
+ };
1868
+ }
1869
+ currentRow++;
1870
+ break;
1871
+
1872
+ case 'TABLE':
1873
+ currentRow = processTable(worksheet, element, currentRow);
1874
+ currentRow++;
1875
+ break;
1876
+
1877
+ case 'P':
1878
+ var pText = element.textContent.trim();
1879
+ if (pText) {
1880
+ var pCell = worksheet.getCell(currentRow, 1);
1881
+ pCell.value = pText;
1882
+ pCell.font = { name: 'メイリオ', size: fontSizes.body };
1883
+ pCell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true };
1884
+ currentRow++;
1885
+ }
1886
+ break;
1887
+
1888
+ case 'UL':
1889
+ case 'OL':
1890
+ var listLines = processListToExcelText(element, '');
1891
+ listLines.forEach(function(lineText) {
1892
+ if (lineText) {
1893
+ var listCell = worksheet.getCell(currentRow, 1);
1894
+ listCell.value = lineText;
1895
+ listCell.font = { name: 'メイリオ', size: fontSizes.body };
1896
+ listCell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true };
1897
+ currentRow++;
1898
+ }
1899
+ });
1900
+ break;
1901
+
1902
+ case 'PRE':
1903
+ var codeElement = element.querySelector('code');
1904
+ var codeText = codeElement ? codeElement.textContent : element.textContent;
1905
+ if (codeText) {
1906
+ var codeCell = worksheet.getCell(currentRow, 1);
1907
+ codeCell.value = codeText;
1908
+ codeCell.font = { name: 'Consolas', size: fontSizes.body };
1909
+ codeCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF4F4F4' } };
1910
+ codeCell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true };
1911
+ codeCell.border = {
1912
+ top: { style: 'thin', color: { argb: 'FFDDDDDD' } },
1913
+ left: { style: 'thin', color: { argb: 'FFDDDDDD' } },
1914
+ bottom: { style: 'thin', color: { argb: 'FFDDDDDD' } },
1915
+ right: { style: 'thin', color: { argb: 'FFDDDDDD' } }
1916
+ };
1917
+ currentRow++;
1918
+ }
1919
+ break;
1920
+
1921
+ case 'BLOCKQUOTE':
1922
+ var bqText = element.textContent.trim();
1923
+ if (bqText) {
1924
+ var bqCell = worksheet.getCell(currentRow, 1);
1925
+ bqCell.value = bqText;
1926
+ bqCell.font = { name: 'メイリオ', size: fontSizes.body, italic: true, color: { argb: 'FF555555' } };
1927
+ bqCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF9F9F9' } };
1928
+ bqCell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true };
1929
+ bqCell.border = {
1930
+ left: { style: 'medium', color: { argb: 'FF4A90A4' } }
1931
+ };
1932
+ currentRow++;
1933
+ }
1934
+ break;
1935
+ }
1936
+
1937
+ return currentRow;
1938
+ }
1939
+
1940
+ function processTable(worksheet, table, startRow) {
1941
+ var rows = table.querySelectorAll('tr');
1942
+ var currentRow = startRow;
1943
+
1944
+ rows.forEach(function(tr) {
1945
+ var cells = tr.querySelectorAll('th, td');
1946
+ var colIndex = 1;
1947
+
1948
+ cells.forEach(function(cell) {
1949
+ var excelCell = worksheet.getCell(currentRow, colIndex);
1950
+ excelCell.value = cell.textContent.trim();
1951
+
1952
+ excelCell.font = { name: 'メイリオ', size: fontSizes.body };
1953
+ excelCell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true };
1954
+ excelCell.border = {
1955
+ top: { style: 'thin', color: { argb: 'FF000000' } },
1956
+ left: { style: 'thin', color: { argb: 'FF000000' } },
1957
+ bottom: { style: 'thin', color: { argb: 'FF000000' } },
1958
+ right: { style: 'thin', color: { argb: 'FF000000' } }
1959
+ };
1960
+
1961
+ if (cell.tagName === 'TH') {
1962
+ excelCell.font = { name: 'メイリオ', size: fontSizes.tableHeader, bold: true, color: { argb: 'FF333333' } };
1963
+ excelCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0F0F0' } };
1964
+ }
1965
+
1966
+ colIndex++;
1967
+ });
1968
+
1969
+ currentRow++;
1970
+ });
1971
+
1972
+ return currentRow;
1973
+ }
1974
+
1975
+ function sanitizeSheetName(name) {
1976
+ return name.replace(/[\\\/?\*\[\]:]/g, '_').substring(0, 31);
1977
+ }
1978
+
1979
+ function getTextWidth(text) {
1980
+ if (!text) return 0;
1981
+ var width = 0;
1982
+ var str = text.toString();
1983
+ for (var i = 0; i < str.length; i++) {
1984
+ var char = str[i];
1985
+ width += /[\u3000-\u9fff\uff00-\uffef]/.test(char) ? 2 : 1;
1986
+ }
1987
+ return width;
1988
+ }
1989
+
1990
+ function getTextWidthWithFontSize(text, fontSize) {
1991
+ var baseWidth = getTextWidth(text);
1992
+ var fontSizeRatio = fontSize / 11;
1993
+ return baseWidth * fontSizeRatio;
1994
+ }
1995
+
1996
+ function cssColorToArgb(cssColor) {
1997
+ if (!cssColor || cssColor === 'transparent' || cssColor === 'rgba(0, 0, 0, 0)') {
1998
+ return null;
1999
+ }
2000
+
2001
+ var rgbMatch = cssColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
2002
+ if (rgbMatch) {
2003
+ var r = parseInt(rgbMatch[1]).toString(16).padStart(2, '0');
2004
+ var g = parseInt(rgbMatch[2]).toString(16).padStart(2, '0');
2005
+ var b = parseInt(rgbMatch[3]).toString(16).padStart(2, '0');
2006
+ return 'FF' + r.toUpperCase() + g.toUpperCase() + b.toUpperCase();
2007
+ }
2008
+
2009
+ var hexMatch = cssColor.match(/#([0-9a-fA-F]{6})/);
2010
+ if (hexMatch) {
2011
+ return 'FF' + hexMatch[1].toUpperCase();
2012
+ }
2013
+
2014
+ var shortHexMatch = cssColor.match(/#([0-9a-fA-F]{3})$/);
2015
+ if (shortHexMatch) {
2016
+ var hex = shortHexMatch[1];
2017
+ return 'FF' + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
2018
+ }
2019
+
2020
+ return null;
2021
+ }
2022
+
2023
+ function getElementStyles(element) {
2024
+ var computed = window.getComputedStyle(element);
2025
+ var bgColor = computed.backgroundColor;
2026
+ var textColor = computed.color;
2027
+
2028
+ var bgImage = computed.backgroundImage;
2029
+ if (bgImage && bgImage.includes('gradient')) {
2030
+ var gradientColors = bgImage.match(/rgb\(\d+,\s*\d+,\s*\d+\)/g);
2031
+ if (gradientColors && gradientColors.length > 0) {
2032
+ bgColor = gradientColors[0];
2033
+ }
2034
+ }
2035
+
2036
+ return {
2037
+ bgArgb: cssColorToArgb(bgColor),
2038
+ textArgb: cssColorToArgb(textColor)
2039
+ };
2040
+ }
2041
+
2042
+ })();