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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6b3bd7d2ceb35ca37646c9f4991d81f239ac69a72cb5c0df9de2d0d33329405c
4
- data.tar.gz: 9546f35e09c7e7229fae0087d7e7d817aff6cdae76a76e6ac272fba2fbcb0c2d
3
+ metadata.gz: 8fe8a5ee54ef7b3d227b1544f1fe0ea4357f2e087035ef2d1325ed26f544e02a
4
+ data.tar.gz: fe092133a77d8f39136ed3d9bf62e9f180efa1eeb3602c1a37af57f0fb3d95b5
5
5
  SHA512:
6
- metadata.gz: 9244b36049b0d62ad1e1f8a2da6dbe74bd535176211a734ddf50039ec6525e0375b028474b283650fd74835eb8fff6280ce17d3ed9a2b2148a528078bfbf8df1
7
- data.tar.gz: 999ab7744606744c0a9f1cb55d1a04b538e608611f026341f8b3fb487aebf792d79b6fb924e1a286f7f454477f39d554f5cfcbefe3a354ae721f651ec48b6de2
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
- textarea.getAttribute('contenteditable') === 'true';
684
+ textarea.getAttribute('contenteditable') === 'true';
279
685
 
280
- let start, end, selectedText;
686
+ let lines = analyzeHtml(textarea.innerHTML.replace('\n \n ', ''));
281
687
 
282
- if (isContentEditable) {
283
- // For contenteditable elements
284
- const selection = getContentEditableSelection(textarea);
285
- start = selection.start;
286
- end = selection.end;
287
- selectedText = selection.selectedText;
288
- } else {
289
- // For regular textarea elements
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
- // fullTextの定義をここで行う
296
- const fullText = isContentEditable ?
297
- (textarea.innerText || textarea.textContent || '') :
298
- textarea.value;
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
- // Bold(**)の場合のみ、選択範囲の両端に余分な空白・改行・文末記号があれば除外
301
- if (before === '**' && after === '**' && selectedText.length > 0) {
302
- // 両端の「改行・空白・文末記号」をすべて除去(複数連続も対応)
303
- while (/^[\s\n.,;:!?]+/.test(selectedText)) {
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
- const beforeText = fullText.substring(0, start);
312
- const afterText = fullText.substring(end);
313
- const newText = before + selectedText + after;
733
+ // 選択範囲の取得
734
+ const selection = window.getSelection();
314
735
 
315
- const newFullText = beforeText + newText + afterText;
736
+ let selectLineMode = false;
737
+ let selectedLinePos;
316
738
 
317
- if (isContentEditable) {
318
- textarea.innerText = newFullText;
319
- } else {
320
- textarea.value = newFullText;
739
+ if (!selection.rangeCount || selection.isCollapsed) {
740
+ // 行選択モード
741
+ selectLineMode = true;
742
+ selectedLinePos = getCurrentLineIndex(beginEndLenStrings);
321
743
  }
322
744
 
323
- // Adjust cursor position
324
- const newCursorPos = selectedText.length > 0 ?
325
- start + newText.length :
326
- start + before.length;
745
+ const range = selection.getRangeAt(0);
327
746
 
328
- textarea.focus();
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
- setContentEditableSelection(textarea, newCursorPos, newCursorPos);
332
- } else if (textarea.setSelectionRange) {
333
- textarea.setSelectionRange(newCursorPos, newCursorPos);
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
- textarea.getAttribute('contenteditable') === 'true';
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
- textarea.getAttribute('contenteditable') === 'true';
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
- textarea.getAttribute('contenteditable') === 'true';
1834
+ textarea.getAttribute('contenteditable') === 'true';
1324
1835
 
1325
1836
  if (isContentEditable) {
1326
1837
  // For contenteditable elements
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PonkotsuMdEditor
4
- VERSION = "0.1.41"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ponkotsu-md-editor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.41
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dhq_boiler