ponkotsu-md-editor 0.1.41 → 0.2.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.
- checksums.yaml +4 -4
- data/app/assets/javascripts/markdown_editor.js +555 -44
- data/lib/ponkotsu/md/editor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fe8a5ee54ef7b3d227b1544f1fe0ea4357f2e087035ef2d1325ed26f544e02a
|
|
4
|
+
data.tar.gz: fe092133a77d8f39136ed3d9bf62e9f180efa1eeb3602c1a37af57f0fb3d95b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4276afc6fb1626c265e0f5fc9242eaab7a6fb7fe5a86dca690329d2a825d209c4cd8753886f57011c6f7b396149dc28739016e7b498d11c003ea962339277225
|
|
7
|
+
data.tar.gz: 64974bbfd25c6c394ba2838608aab7fb192c574528a848c0d9406ed97efa836acadbef688e909cbbf72156d16386611e10e84739821f39f330d60e8e3ea58974
|
|
@@ -30,6 +30,30 @@
|
|
|
30
30
|
|
|
31
31
|
let isPreviewMode = false;
|
|
32
32
|
|
|
33
|
+
const sampleHtml = "aaa bbb ccc ddd eee\n" +
|
|
34
|
+
" \n" +
|
|
35
|
+
" <div>aaa bbb ccc ddd eee</div><div><br></div><div>### aaa bbb ccc ddd eee</div><div></div><div>####</div><div><br></div><div>aaa bbb ccc ddd eee</div>";
|
|
36
|
+
|
|
37
|
+
const actual = analyzeHtml(sampleHtml);
|
|
38
|
+
|
|
39
|
+
function assertEqual(actual, expected, message) {
|
|
40
|
+
if (actual !== expected) {
|
|
41
|
+
console.error('Assertion failed:' + message + '\n' +
|
|
42
|
+
'Expected:' + expected + '\n' +
|
|
43
|
+
'Actual:', actual);
|
|
44
|
+
} else {
|
|
45
|
+
console.log('Assertion passed:', message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
assertEqual(actual[0], 'aaa bbb ccc ddd eee\n \n ', "Line 1");
|
|
50
|
+
assertEqual(actual[1], `aaa bbb ccc ddd eee`, "Line 2");
|
|
51
|
+
assertEqual(actual[2], "⹉", "Line 3 (empty line)");
|
|
52
|
+
assertEqual(actual[3], "### aaa bbb ccc ddd eee", "Line 4");
|
|
53
|
+
assertEqual(actual[4], "####", "Line 5 (header only)");
|
|
54
|
+
assertEqual(actual[5], "⹉", "Line 6 (empty line)");
|
|
55
|
+
assertEqual(actual[6], "aaa bbb ccc ddd eee", "Line 7");
|
|
56
|
+
|
|
33
57
|
// Selection range utilities for contenteditable elements (precision enhanced version)
|
|
34
58
|
function getContentEditableSelection(element) {
|
|
35
59
|
const selection = window.getSelection();
|
|
@@ -119,6 +143,15 @@
|
|
|
119
143
|
selectedText = rangeText;
|
|
120
144
|
}
|
|
121
145
|
|
|
146
|
+
// === 厳密化: selectedTextがfullText.slice(startPos, endPos)と一致しない場合、fullText内でselectedTextの位置を検索 ===
|
|
147
|
+
if (selectedText && fullText.slice(startPos, endPos) !== selectedText) {
|
|
148
|
+
const idx = fullText.indexOf(selectedText);
|
|
149
|
+
if (idx !== -1) {
|
|
150
|
+
startPos = idx;
|
|
151
|
+
endPos = idx + selectedText.length;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
122
155
|
// === デバッグ出力 ===
|
|
123
156
|
console.log('[DEBUG] getContentEditableSelection');
|
|
124
157
|
console.log('fullText:', JSON.stringify(fullText));
|
|
@@ -263,6 +296,379 @@
|
|
|
263
296
|
element.innerText = text;
|
|
264
297
|
}
|
|
265
298
|
|
|
299
|
+
function getLineAndCharIndex(container, offset) {
|
|
300
|
+
const walker = document.createTreeWalker(
|
|
301
|
+
container,
|
|
302
|
+
NodeFilter.SHOW_TEXT,
|
|
303
|
+
null,
|
|
304
|
+
false
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
let currentOffset = 0;
|
|
308
|
+
let lineNumber = 0;
|
|
309
|
+
let charInLine = 0;
|
|
310
|
+
let node;
|
|
311
|
+
|
|
312
|
+
while (node = walker.nextNode()) {
|
|
313
|
+
const nodeText = node.textContent;
|
|
314
|
+
const nodeLength = nodeText.length;
|
|
315
|
+
|
|
316
|
+
if (currentOffset + nodeLength >= offset) {
|
|
317
|
+
const offsetInNode = offset - currentOffset;
|
|
318
|
+
const textBeforeOffset = nodeText.substring(0, offsetInNode);
|
|
319
|
+
|
|
320
|
+
const allTextBefore = container.textContent.substring(0, currentOffset + offsetInNode);
|
|
321
|
+
const linesBeforeOffset = allTextBefore.split('\n');
|
|
322
|
+
|
|
323
|
+
lineNumber = linesBeforeOffset.length - 1;
|
|
324
|
+
charInLine = linesBeforeOffset[linesBeforeOffset.length - 1].length;
|
|
325
|
+
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
currentOffset += nodeLength;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { line: lineNumber, char: charInLine };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getOffsetInContainer(container, node, offset) {
|
|
336
|
+
// DOM構造をリニアに変換して位置を計算
|
|
337
|
+
function buildLinearTextMap(container) {
|
|
338
|
+
let textMap = [];
|
|
339
|
+
let currentPos = 0;
|
|
340
|
+
|
|
341
|
+
function processNode(node) {
|
|
342
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
343
|
+
const text = node.textContent;
|
|
344
|
+
textMap.push({
|
|
345
|
+
node: node,
|
|
346
|
+
type: 'text',
|
|
347
|
+
start: currentPos,
|
|
348
|
+
end: currentPos + text.length,
|
|
349
|
+
text: text
|
|
350
|
+
});
|
|
351
|
+
currentPos += text.length;
|
|
352
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
353
|
+
if (node.tagName === 'DIV') {
|
|
354
|
+
if (node.children.length === 1 && node.children[0].tagName === 'BR') {
|
|
355
|
+
textMap.push({
|
|
356
|
+
node: node,
|
|
357
|
+
type: 'div-br',
|
|
358
|
+
start: currentPos,
|
|
359
|
+
end: currentPos + 1,
|
|
360
|
+
text: '\n'
|
|
361
|
+
});
|
|
362
|
+
currentPos += 1;
|
|
363
|
+
return;
|
|
364
|
+
} else if (node.innerHTML === '<br>' || node.innerHTML === '<br/>' || node.innerHTML === '') {
|
|
365
|
+
textMap.push({
|
|
366
|
+
node: node,
|
|
367
|
+
type: 'empty-div',
|
|
368
|
+
start: currentPos,
|
|
369
|
+
end: currentPos + 1,
|
|
370
|
+
text: '\n'
|
|
371
|
+
});
|
|
372
|
+
currentPos += 1;
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
} else if (node.tagName === 'BR') {
|
|
376
|
+
textMap.push({
|
|
377
|
+
node: node,
|
|
378
|
+
type: 'br',
|
|
379
|
+
start: currentPos,
|
|
380
|
+
end: currentPos + 1,
|
|
381
|
+
text: '\n'
|
|
382
|
+
});
|
|
383
|
+
currentPos += 1;
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 子ノードを処理
|
|
388
|
+
for (let child of node.childNodes) {
|
|
389
|
+
processNode(child);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// コンテナの直接の子ノードから開始
|
|
395
|
+
for (let child of container.childNodes) {
|
|
396
|
+
processNode(child);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return textMap;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const textMap = buildLinearTextMap(container);
|
|
403
|
+
|
|
404
|
+
// 対象ノードを見つけて位置を計算
|
|
405
|
+
for (let item of textMap) {
|
|
406
|
+
if (item.node === node) {
|
|
407
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
408
|
+
return item.start + offset;
|
|
409
|
+
} else {
|
|
410
|
+
// 要素ノードの場合は内部オフセットを計算
|
|
411
|
+
let internalOffset = 0;
|
|
412
|
+
for (let i = 0; i < offset && i < node.childNodes.length; i++) {
|
|
413
|
+
const child = node.childNodes[i];
|
|
414
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
415
|
+
internalOffset += child.textContent.length;
|
|
416
|
+
} else if (child.nodeType === Node.ELEMENT_NODE && child.tagName === 'BR') {
|
|
417
|
+
internalOffset += 1;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return item.start + internalOffset;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function selectLineNumberAndCharIndex(beginEndLenStrings, beginCharIndex, endCharIndex) {
|
|
429
|
+
let retBeginLine = 0, retBeginCharIndex = 0;
|
|
430
|
+
let retEndLine = 0, retEndCharIndex = 0;
|
|
431
|
+
let emptyLineCount = 0;
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < beginEndLenStrings.length; i++) {
|
|
434
|
+
const line = beginEndLenStrings[i];
|
|
435
|
+
|
|
436
|
+
if (line.str === "⹉") {
|
|
437
|
+
emptyLineCount++;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (beginCharIndex >= line.begin && beginCharIndex <= line.end) {
|
|
441
|
+
retBeginLine = i;
|
|
442
|
+
retBeginCharIndex = beginCharIndex - line.begin + emptyLineCount;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
emptyLineCount = 0;
|
|
448
|
+
|
|
449
|
+
for (let i = 0; i < beginEndLenStrings.length; i++) {
|
|
450
|
+
const line = beginEndLenStrings[i];
|
|
451
|
+
|
|
452
|
+
if (line.str === "⹉") {
|
|
453
|
+
emptyLineCount++;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (endCharIndex >= line.begin && endCharIndex <= line.end) {
|
|
457
|
+
retEndLine = i;
|
|
458
|
+
retEndCharIndex = endCharIndex - line.begin + emptyLineCount;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return { begin: { line: retBeginLine, char: retBeginCharIndex }, end: { line: retEndLine, char: retEndCharIndex } };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function replaceLineNumberAndCharIndex(beginEndLenStrings, targetTextPosition, before, after) {
|
|
467
|
+
let newLines = [];
|
|
468
|
+
for (let i = 0; i < beginEndLenStrings.length; i++) {
|
|
469
|
+
const line = beginEndLenStrings[i];
|
|
470
|
+
|
|
471
|
+
// 改行行は変更せずそのまま保持
|
|
472
|
+
if (line.str === "⹉") {
|
|
473
|
+
newLines.push(line.str);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (i === targetTextPosition.begin.line && i === targetTextPosition.end.line) {
|
|
478
|
+
const first = line.str.substring(0, targetTextPosition.begin.char);
|
|
479
|
+
const target = line.str.substring(targetTextPosition.begin.char, targetTextPosition.end.char);
|
|
480
|
+
const last = line.str.substring(targetTextPosition.end.char);
|
|
481
|
+
newLines.push(first + before + target + after + last);
|
|
482
|
+
}
|
|
483
|
+
else if (i === targetTextPosition.begin.line) {
|
|
484
|
+
const first = line.str.substring(0, targetTextPosition.begin.char);
|
|
485
|
+
const target = line.str.substring(targetTextPosition.begin.char);
|
|
486
|
+
newLines.push(first + before + target + after);
|
|
487
|
+
}
|
|
488
|
+
else if (i === targetTextPosition.end.line) {
|
|
489
|
+
const target = line.str.substring(0, targetTextPosition.end.char);
|
|
490
|
+
const last = line.str.substring(targetTextPosition.end.char);
|
|
491
|
+
newLines.push(before + target + after + last);
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
newLines.push(line.str);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return newLines;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function replaceLine(beginEndLenStrings, selectedLine, before, after) {
|
|
501
|
+
|
|
502
|
+
let newLines = [];
|
|
503
|
+
for (let i = 0; i < beginEndLenStrings.length; i++) {
|
|
504
|
+
const line = beginEndLenStrings[i];
|
|
505
|
+
if (i === selectedLine) {
|
|
506
|
+
let addingSpace = "";
|
|
507
|
+
if (!line.str.startsWith(" ")) {
|
|
508
|
+
addingSpace += " ";
|
|
509
|
+
}
|
|
510
|
+
newLines.push(before + addingSpace + line.str + after);
|
|
511
|
+
} else {
|
|
512
|
+
newLines.push(line.str);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return newLines;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function convertToInnerHtml(newLines) {
|
|
520
|
+
let html = "";
|
|
521
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
522
|
+
if (i === 0) {
|
|
523
|
+
html += newLines[i].split("⹉").join("");
|
|
524
|
+
} else {
|
|
525
|
+
let insert = newLines[i];
|
|
526
|
+
if (insert === "⹉") {
|
|
527
|
+
insert = insert.split("⹉").join("<br>");
|
|
528
|
+
} else {
|
|
529
|
+
insert = insert.split("⹉").join("");
|
|
530
|
+
}
|
|
531
|
+
html += "<div>" + insert + "</div>";
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return html;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function getCurrentLineIndex(beginEndLenStrings) {
|
|
538
|
+
const selection = window.getSelection();
|
|
539
|
+
if (!selection.rangeCount) {
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const range = selection.getRangeAt(0);
|
|
544
|
+
|
|
545
|
+
if (!range.collapsed) {
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const beforeCursorText = getTextBeforeCursor(range.startContainer, range.startOffset, beginEndLenStrings);
|
|
550
|
+
const beforeCursorTextLength = beforeCursorText.length;
|
|
551
|
+
let selectedLine = -1;
|
|
552
|
+
for (let i = 0; i < beginEndLenStrings.length; i++) {
|
|
553
|
+
const line = beginEndLenStrings[i];
|
|
554
|
+
if (line.begin <= beforeCursorTextLength && beforeCursorTextLength <= line.end) {
|
|
555
|
+
selectedLine = i;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
line: selectedLine
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function getTextBeforeCursor(container, offset, beginEndLenStrings) {
|
|
565
|
+
const selection = window.getSelection();
|
|
566
|
+
if (!selection.rangeCount) return '';
|
|
567
|
+
|
|
568
|
+
const range = selection.getRangeAt(0);
|
|
569
|
+
const textarea = document.getElementById('editor_content');
|
|
570
|
+
|
|
571
|
+
try {
|
|
572
|
+
// 全テキストとカーソル位置を取得
|
|
573
|
+
const fullText = textarea.innerText || '';
|
|
574
|
+
const text = getCursorPositionInInnerText(textarea, range.startContainer, range.startOffset);
|
|
575
|
+
return text;
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error('Error in getTextBeforeCursor:', error);
|
|
578
|
+
return '';
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function getCursorPositionInInnerText(container, cursorNode, cursorOffset) {
|
|
583
|
+
try {
|
|
584
|
+
const beforeRange = document.createRange();
|
|
585
|
+
beforeRange.setStart(container, 0);
|
|
586
|
+
beforeRange.setEnd(cursorNode, cursorOffset);
|
|
587
|
+
|
|
588
|
+
// cloneContentsを使用してDOM構造を保持
|
|
589
|
+
const fragment = beforeRange.cloneContents();
|
|
590
|
+
const tempDiv = document.createElement('div');
|
|
591
|
+
tempDiv.appendChild(fragment);
|
|
592
|
+
|
|
593
|
+
// innerTextで正確な文字数を取得(空行も含む)
|
|
594
|
+
const r = analyzeHtml(tempDiv.innerHTML.replace('\n \n ', ''), true).join('');
|
|
595
|
+
return r;
|
|
596
|
+
|
|
597
|
+
} catch (error) {
|
|
598
|
+
console.error('Position calculation failed:', error);
|
|
599
|
+
return 0;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function analyzeHtml(target, isCountEmptyDiv = false) {
|
|
604
|
+
let lines = [];
|
|
605
|
+
let remain = target;
|
|
606
|
+
|
|
607
|
+
while (remain.length > 0) {
|
|
608
|
+
// <div><br></div> パターン(改行)- 常に維持
|
|
609
|
+
if (remain.startsWith("<div><br></div>")) {
|
|
610
|
+
lines.push("⹉");
|
|
611
|
+
remain = remain.substring(15);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// <div></div> パターン(空のdiv)- isCountEmptyDivで制御
|
|
616
|
+
if (remain.startsWith("<div></div>")) {
|
|
617
|
+
if (isCountEmptyDiv) {
|
|
618
|
+
lines.push("⹉");
|
|
619
|
+
}
|
|
620
|
+
remain = remain.substring(11);
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// <div>内容</div> パターン
|
|
625
|
+
const divMatch = remain.match(/^<div>([^<]*)<\/div>/);
|
|
626
|
+
if (divMatch) {
|
|
627
|
+
lines.push(divMatch[1]);
|
|
628
|
+
remain = remain.substring(divMatch[0].length);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// <br> パターン(単体の改行)
|
|
633
|
+
if (remain.startsWith("<br>")) {
|
|
634
|
+
lines.push("⹉");
|
|
635
|
+
remain = remain.substring(4);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// <div> の開始を探す
|
|
640
|
+
const divIndex = remain.indexOf("<div>");
|
|
641
|
+
if (divIndex > 0) {
|
|
642
|
+
lines.push(remain.substring(0, divIndex));
|
|
643
|
+
remain = remain.substring(divIndex);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// その他のタグまたは残りのテキスト
|
|
648
|
+
const nextTagIndex = remain.indexOf("<");
|
|
649
|
+
if (nextTagIndex === -1) {
|
|
650
|
+
if (remain.trim()) {
|
|
651
|
+
lines.push(remain);
|
|
652
|
+
}
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (nextTagIndex > 0) {
|
|
657
|
+
lines.push(remain.substring(0, nextTagIndex));
|
|
658
|
+
remain = remain.substring(nextTagIndex);
|
|
659
|
+
} else {
|
|
660
|
+
const tagEnd = remain.indexOf(">");
|
|
661
|
+
if (tagEnd !== -1) {
|
|
662
|
+
remain = remain.substring(tagEnd + 1);
|
|
663
|
+
} else {
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return lines;
|
|
670
|
+
}
|
|
671
|
+
|
|
266
672
|
// Markdown text insertion functionality
|
|
267
673
|
window.insertMarkdown = function(before, after) {
|
|
268
674
|
after = after || '';
|
|
@@ -275,62 +681,148 @@
|
|
|
275
681
|
try {
|
|
276
682
|
// Check if element is contenteditable
|
|
277
683
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
278
|
-
|
|
684
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
279
685
|
|
|
280
|
-
let
|
|
686
|
+
let lines = analyzeHtml(textarea.innerHTML.replace('\n \n ', ''));
|
|
281
687
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
start = textarea.selectionStart || 0;
|
|
291
|
-
end = textarea.selectionEnd || 0;
|
|
292
|
-
selectedText = textarea.value.substring(start, end);
|
|
688
|
+
// === 修正部分:DOM構造に基づいたテキスト表現を構築 ===
|
|
689
|
+
let domBasedText = '';
|
|
690
|
+
for (let i = 0; i < lines.length; i++) {
|
|
691
|
+
if (lines[i] === "⹉") {
|
|
692
|
+
domBasedText += '\n'; // 改行として1文字
|
|
693
|
+
} else {
|
|
694
|
+
domBasedText += lines[i];
|
|
695
|
+
}
|
|
293
696
|
}
|
|
294
697
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
698
|
+
// beginEndLenStringsをDOM構造ベースで構築
|
|
699
|
+
let offset = 0;
|
|
700
|
+
let beginEndLenStrings = [];
|
|
701
|
+
for (let i = 0; i < lines.length; i++) {
|
|
702
|
+
let line = lines[i];
|
|
703
|
+
let actualLength;
|
|
299
704
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
selectedText = selectedText.replace(/^[\s\n.,;:!?]+/, '');
|
|
305
|
-
}
|
|
306
|
-
while (/[\s\n.,;:!?]+$/.test(selectedText)) {
|
|
307
|
-
selectedText = selectedText.replace(/[\s\n.,;:!?]+$/, '');
|
|
705
|
+
if (line === "⹉") {
|
|
706
|
+
actualLength = 1; // 改行として1文字
|
|
707
|
+
} else {
|
|
708
|
+
actualLength = line.length;
|
|
308
709
|
}
|
|
710
|
+
|
|
711
|
+
beginEndLenStrings.push({
|
|
712
|
+
begin: offset,
|
|
713
|
+
end: offset + actualLength,
|
|
714
|
+
len: actualLength,
|
|
715
|
+
str: line,
|
|
716
|
+
});
|
|
717
|
+
offset += actualLength;
|
|
718
|
+
}
|
|
719
|
+
// ===================================================
|
|
720
|
+
|
|
721
|
+
// len:0のものを削除(元の処理を保持)
|
|
722
|
+
beginEndLenStrings = beginEndLenStrings.filter(line => line.len > 0 || line.str === "⹉");
|
|
723
|
+
if (beginEndLenStrings.length === 0) {
|
|
724
|
+
beginEndLenStrings.push({
|
|
725
|
+
begin: 0,
|
|
726
|
+
end: 1,
|
|
727
|
+
len: 1,
|
|
728
|
+
str: "⹉",
|
|
729
|
+
emptyLineCount: 1
|
|
730
|
+
});
|
|
309
731
|
}
|
|
310
732
|
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
const newText = before + selectedText + after;
|
|
733
|
+
// 選択範囲の取得
|
|
734
|
+
const selection = window.getSelection();
|
|
314
735
|
|
|
315
|
-
|
|
736
|
+
let selectLineMode = false;
|
|
737
|
+
let selectedLinePos;
|
|
316
738
|
|
|
317
|
-
if (
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
739
|
+
if (!selection.rangeCount || selection.isCollapsed) {
|
|
740
|
+
// 行選択モード
|
|
741
|
+
selectLineMode = true;
|
|
742
|
+
selectedLinePos = getCurrentLineIndex(beginEndLenStrings);
|
|
321
743
|
}
|
|
322
744
|
|
|
323
|
-
|
|
324
|
-
const newCursorPos = selectedText.length > 0 ?
|
|
325
|
-
start + newText.length :
|
|
326
|
-
start + before.length;
|
|
745
|
+
const range = selection.getRangeAt(0);
|
|
327
746
|
|
|
328
|
-
textarea.
|
|
747
|
+
if (!textarea.contains(range.commonAncestorContainer) &&
|
|
748
|
+
range.commonAncestorContainer !== textarea) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const startOffset = getOffsetInContainer(textarea, range.startContainer, range.startOffset);
|
|
753
|
+
const endOffset = getOffsetInContainer(textarea, range.endContainer, range.endOffset);
|
|
754
|
+
|
|
755
|
+
// === デバッグ出力(削除可能)===
|
|
756
|
+
console.log('DOM-based text:', JSON.stringify(domBasedText));
|
|
757
|
+
console.log('beginEndLenStrings:', beginEndLenStrings);
|
|
758
|
+
console.log('Selection offsets:', startOffset, endOffset);
|
|
759
|
+
console.log('Selected text should be:', JSON.stringify(domBasedText.substring(startOffset, endOffset)));
|
|
760
|
+
// =============================
|
|
761
|
+
|
|
762
|
+
const startPos = getLineAndCharIndex(textarea, startOffset);
|
|
763
|
+
const endPos = getLineAndCharIndex(textarea, endOffset);
|
|
764
|
+
|
|
765
|
+
const selectedTextContent = selection.toString();
|
|
766
|
+
|
|
767
|
+
const targetTextPosition = selectLineNumberAndCharIndex(beginEndLenStrings, startOffset, endOffset);
|
|
768
|
+
|
|
769
|
+
const newLines = !selectLineMode
|
|
770
|
+
? replaceLineNumberAndCharIndex(beginEndLenStrings, targetTextPosition, before, after)
|
|
771
|
+
: replaceLine(beginEndLenStrings, selectedLinePos.line, before, after);
|
|
772
|
+
|
|
773
|
+
const newFullHTML = convertToInnerHtml(newLines);
|
|
329
774
|
|
|
330
775
|
if (isContentEditable) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
776
|
+
textarea.innerHTML = newFullHTML;
|
|
777
|
+
textarea.focus();
|
|
778
|
+
|
|
779
|
+
// 挿入されたテキストノードを直接探してカーソルを設定
|
|
780
|
+
const insertedText = selectedTextContent.length > 0 ?
|
|
781
|
+
(before + selectedTextContent + after) :
|
|
782
|
+
(before + after);
|
|
783
|
+
|
|
784
|
+
// 新しく挿入されたテキストの終端を探す
|
|
785
|
+
const walker = document.createTreeWalker(
|
|
786
|
+
textarea,
|
|
787
|
+
NodeFilter.SHOW_TEXT,
|
|
788
|
+
null,
|
|
789
|
+
false
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
let foundNode = null;
|
|
793
|
+
let foundOffset = 0;
|
|
794
|
+
let node;
|
|
795
|
+
|
|
796
|
+
while (node = walker.nextNode()) {
|
|
797
|
+
if (node.textContent.includes(insertedText)) {
|
|
798
|
+
// 挿入されたテキストを含むノードを発見
|
|
799
|
+
const textIndex = node.textContent.indexOf(insertedText);
|
|
800
|
+
if (textIndex !== -1) {
|
|
801
|
+
foundNode = node;
|
|
802
|
+
foundOffset = textIndex + insertedText.length;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (foundNode) {
|
|
809
|
+
// 見つかったテキストノード内にカーソルを設定
|
|
810
|
+
const range = document.createRange();
|
|
811
|
+
range.setStart(foundNode, Math.min(foundOffset, foundNode.textContent.length));
|
|
812
|
+
range.collapse(true);
|
|
813
|
+
|
|
814
|
+
const selection = window.getSelection();
|
|
815
|
+
selection.removeAllRanges();
|
|
816
|
+
selection.addRange(range);
|
|
817
|
+
} else {
|
|
818
|
+
// フォールバック: 従来の方法
|
|
819
|
+
const newDomText = buildDomBasedText(textarea);
|
|
820
|
+
const targetPosition = Math.min(
|
|
821
|
+
startOffset + insertedText.length,
|
|
822
|
+
newDomText.length
|
|
823
|
+
);
|
|
824
|
+
setContentEditableSelection(textarea, targetPosition, targetPosition);
|
|
825
|
+
}
|
|
334
826
|
}
|
|
335
827
|
|
|
336
828
|
// Fire input event
|
|
@@ -341,12 +833,25 @@
|
|
|
341
833
|
}
|
|
342
834
|
};
|
|
343
835
|
|
|
836
|
+
function buildDomBasedText(element) {
|
|
837
|
+
const lines = analyzeHtml(element.innerHTML);
|
|
838
|
+
let domText = '';
|
|
839
|
+
for (let line of lines) {
|
|
840
|
+
if (line === "⹉") {
|
|
841
|
+
domText += '\n';
|
|
842
|
+
} else {
|
|
843
|
+
domText += line;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return domText;
|
|
847
|
+
}
|
|
848
|
+
|
|
344
849
|
window.insertCode = function() {
|
|
345
850
|
const textarea = document.getElementById('editor_content');
|
|
346
851
|
|
|
347
852
|
// Check if element is contenteditable
|
|
348
853
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
349
|
-
|
|
854
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
350
855
|
|
|
351
856
|
let selectedText;
|
|
352
857
|
if (isContentEditable) {
|
|
@@ -379,7 +884,7 @@
|
|
|
379
884
|
// Switch to preview mode
|
|
380
885
|
// Check if element is contenteditable and get text
|
|
381
886
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
382
|
-
|
|
887
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
383
888
|
|
|
384
889
|
const markdownText = isContentEditable ?
|
|
385
890
|
(textarea.innerText || textarea.textContent || '') :
|
|
@@ -722,6 +1227,11 @@
|
|
|
722
1227
|
});
|
|
723
1228
|
}
|
|
724
1229
|
|
|
1230
|
+
function applyTable() {
|
|
1231
|
+
const tableMarkdown = '| 列1 | 列2 | 列3 |<br>|-----|-----|-----|<br>| セル1 | セル2 | セル3 |<br>| セル4 | セル5 | セル6 |<br><br>';
|
|
1232
|
+
insertMarkdown(tableMarkdown, '');
|
|
1233
|
+
}
|
|
1234
|
+
|
|
725
1235
|
// Enhanced code function with block detection
|
|
726
1236
|
function applyCodeSmart() {
|
|
727
1237
|
const activeElement = document.activeElement;
|
|
@@ -758,6 +1268,7 @@
|
|
|
758
1268
|
window.applyList = applyList;
|
|
759
1269
|
window.applyOrderedList = applyOrderedList;
|
|
760
1270
|
window.applyBlockquote = applyBlockquote;
|
|
1271
|
+
window.applyTable = applyTable;
|
|
761
1272
|
|
|
762
1273
|
// Allowed script tag sources (whitelist)
|
|
763
1274
|
const ALLOWED_SCRIPT_SOURCES = [
|
|
@@ -1320,7 +1831,7 @@
|
|
|
1320
1831
|
|
|
1321
1832
|
// Check if element is contenteditable
|
|
1322
1833
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
1323
|
-
|
|
1834
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
1324
1835
|
|
|
1325
1836
|
if (isContentEditable) {
|
|
1326
1837
|
// For contenteditable elements
|