ponkotsu-md-editor 0.1.42 → 0.2.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.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62a1a937b8cf271a226386c2661c8decedc24ae6b2189e92ba2358f8a9428228
|
|
4
|
+
data.tar.gz: 72feda410a62df3df6f05ba7fae531eb7c44f8f1fc77d73284d85a1b69258efa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 57667cc623bba7c40bb54d83dedc7eb8b7a85af87a6b4314f67a43ced501b7d068ef770b0d43b055d8ed5c9cb039c764b16800d5e28c45e19c44d2f6f495590f
|
|
7
|
+
data.tar.gz: e998286e7ddfead67721ed9d287ec148b770e7a1c9f0ec9b360fe02ceec263dca7e146b2608d959aef970d6b4c140f07e2622476c1006d38fbdf3107963eb06d
|
|
@@ -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();
|
|
@@ -272,6 +296,379 @@
|
|
|
272
296
|
element.innerText = text;
|
|
273
297
|
}
|
|
274
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
|
+
|
|
275
672
|
// Markdown text insertion functionality
|
|
276
673
|
window.insertMarkdown = function(before, after) {
|
|
277
674
|
after = after || '';
|
|
@@ -284,62 +681,148 @@
|
|
|
284
681
|
try {
|
|
285
682
|
// Check if element is contenteditable
|
|
286
683
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
287
|
-
|
|
684
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
288
685
|
|
|
289
|
-
let
|
|
686
|
+
let lines = analyzeHtml(textarea.innerHTML.replace('\n \n ', ''));
|
|
290
687
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
start = textarea.selectionStart || 0;
|
|
300
|
-
end = textarea.selectionEnd || 0;
|
|
301
|
-
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
|
+
}
|
|
302
696
|
}
|
|
303
697
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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;
|
|
308
704
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
selectedText = selectedText.replace(/^[\s\n.,;:!?]+/, '');
|
|
314
|
-
}
|
|
315
|
-
while (/[\s\n.,;:!?]+$/.test(selectedText)) {
|
|
316
|
-
selectedText = selectedText.replace(/[\s\n.,;:!?]+$/, '');
|
|
705
|
+
if (line === "⹉") {
|
|
706
|
+
actualLength = 1; // 改行として1文字
|
|
707
|
+
} else {
|
|
708
|
+
actualLength = line.length;
|
|
317
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
|
+
});
|
|
318
731
|
}
|
|
319
732
|
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
const newText = before + selectedText + after;
|
|
733
|
+
// 選択範囲の取得
|
|
734
|
+
const selection = window.getSelection();
|
|
323
735
|
|
|
324
|
-
|
|
736
|
+
let selectLineMode = false;
|
|
737
|
+
let selectedLinePos;
|
|
325
738
|
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
739
|
+
if (!selection.rangeCount || selection.isCollapsed) {
|
|
740
|
+
// 行選択モード
|
|
741
|
+
selectLineMode = true;
|
|
742
|
+
selectedLinePos = getCurrentLineIndex(beginEndLenStrings);
|
|
330
743
|
}
|
|
331
744
|
|
|
332
|
-
|
|
333
|
-
const newCursorPos = selectedText.length > 0 ?
|
|
334
|
-
start + newText.length :
|
|
335
|
-
start + before.length;
|
|
745
|
+
const range = selection.getRangeAt(0);
|
|
336
746
|
|
|
337
|
-
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);
|
|
338
774
|
|
|
339
775
|
if (isContentEditable) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
826
|
}
|
|
344
827
|
|
|
345
828
|
// Fire input event
|
|
@@ -350,12 +833,25 @@
|
|
|
350
833
|
}
|
|
351
834
|
};
|
|
352
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
|
+
|
|
353
849
|
window.insertCode = function() {
|
|
354
850
|
const textarea = document.getElementById('editor_content');
|
|
355
851
|
|
|
356
852
|
// Check if element is contenteditable
|
|
357
853
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
358
|
-
|
|
854
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
359
855
|
|
|
360
856
|
let selectedText;
|
|
361
857
|
if (isContentEditable) {
|
|
@@ -388,7 +884,7 @@
|
|
|
388
884
|
// Switch to preview mode
|
|
389
885
|
// Check if element is contenteditable and get text
|
|
390
886
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
391
|
-
|
|
887
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
392
888
|
|
|
393
889
|
const markdownText = isContentEditable ?
|
|
394
890
|
(textarea.innerText || textarea.textContent || '') :
|
|
@@ -731,6 +1227,11 @@
|
|
|
731
1227
|
});
|
|
732
1228
|
}
|
|
733
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
|
+
|
|
734
1235
|
// Enhanced code function with block detection
|
|
735
1236
|
function applyCodeSmart() {
|
|
736
1237
|
const activeElement = document.activeElement;
|
|
@@ -767,6 +1268,7 @@
|
|
|
767
1268
|
window.applyList = applyList;
|
|
768
1269
|
window.applyOrderedList = applyOrderedList;
|
|
769
1270
|
window.applyBlockquote = applyBlockquote;
|
|
1271
|
+
window.applyTable = applyTable;
|
|
770
1272
|
|
|
771
1273
|
// Allowed script tag sources (whitelist)
|
|
772
1274
|
const ALLOWED_SCRIPT_SOURCES = [
|
|
@@ -1329,7 +1831,7 @@
|
|
|
1329
1831
|
|
|
1330
1832
|
// Check if element is contenteditable
|
|
1331
1833
|
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
1332
|
-
|
|
1834
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
1333
1835
|
|
|
1334
1836
|
if (isContentEditable) {
|
|
1335
1837
|
// For contenteditable elements
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<%
|
|
2
2
|
_form = locals[:form]
|
|
3
|
-
_content = locals[:content]
|
|
4
3
|
_options = {
|
|
5
4
|
lang: locals[:options][:lang] || :en,
|
|
6
5
|
preview: locals[:options][:preview] || true,
|
|
@@ -60,7 +59,7 @@
|
|
|
60
59
|
|
|
61
60
|
<div class="markdown-editor">
|
|
62
61
|
<%= render "ponkotsu_md_editor/toolbar", locals: { options: _options } %>
|
|
63
|
-
<%= render "ponkotsu_md_editor/input_area", locals: { form: _form,
|
|
62
|
+
<%= render "ponkotsu_md_editor/input_area", locals: { form: _form, attribute: locals[:attribute], options: _options } %>
|
|
64
63
|
</div>
|
|
65
64
|
<div class="form-text medium-contrast-text">
|
|
66
65
|
<strong><%= case _options[:lang]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<%
|
|
2
2
|
_form = locals[:form]
|
|
3
|
-
_content = locals[:content]
|
|
4
3
|
_placeholder = locals[:options][:placeholder]
|
|
5
4
|
%>
|
|
6
5
|
|
|
@@ -9,14 +8,14 @@
|
|
|
9
8
|
contenteditable="true"
|
|
10
9
|
data-field="content"
|
|
11
10
|
class="form-control markdown-textarea">
|
|
12
|
-
<%= raw
|
|
11
|
+
<%= raw locals[:attribute] %>
|
|
13
12
|
</div>
|
|
14
13
|
|
|
15
14
|
<div id="editor_content_placeholder">
|
|
16
15
|
<%= _placeholder %>
|
|
17
16
|
</div>
|
|
18
17
|
|
|
19
|
-
<%= _form.hidden_field
|
|
18
|
+
<%= _form.hidden_field locals[:attribute], id: "content_hidden_field" %>
|
|
20
19
|
|
|
21
20
|
<%= render "ponkotsu_md_editor/preview_area" %>
|
|
22
21
|
</div>
|
|
@@ -22,9 +22,9 @@ module PonkotsuMdEditor
|
|
|
22
22
|
# tools: [ :bold, :italic, :strikethrough ],
|
|
23
23
|
# placeholder: "This is Placeholder."
|
|
24
24
|
# }) %>
|
|
25
|
-
def markdown_editor(form,
|
|
25
|
+
def markdown_editor(form, attribute, options = {})
|
|
26
26
|
form = form[:form] if form.is_a?(Hash)
|
|
27
|
-
render "ponkotsu_md_editor/editor", locals: {
|
|
27
|
+
render "ponkotsu_md_editor/editor", locals: { attribute: attribute, form: form, options: options }
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|