ponkotsu-md-editor 0.1.13 → 0.1.14
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 +179 -25
- 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: fbb4edacc4c389fd1e7b470381b07861c0534c41b48248cfd41bcb295d3a032d
|
|
4
|
+
data.tar.gz: deff4f5d9c78cf564d348cf4efad276ac60867271ebd65ec4311e5bc0bf20915
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aeb2396debdb11aafc9bc81efcaf672f6d31c48768a446a8c4120466928c71ebc3650c3ab1394ef581474f9053d55214e1ef54bfe740a257e10e0bc6f438b353
|
|
7
|
+
data.tar.gz: d8fdfde9fb05016c27214e22d415cac9dbba2ca57e0655bcfa2f7bb0e37da8165f439ccf50ae4b475d8e161cb3799e445b2f88cbb5c84cbf2bf1b0692a67f4f8
|
|
@@ -30,6 +30,81 @@
|
|
|
30
30
|
|
|
31
31
|
let isPreviewMode = false;
|
|
32
32
|
|
|
33
|
+
// contenteditable要素用の選択範囲取得・設定機能
|
|
34
|
+
function getContentEditableSelection(element) {
|
|
35
|
+
const selection = window.getSelection();
|
|
36
|
+
if (selection.rangeCount === 0) {
|
|
37
|
+
return { start: 0, end: 0, selectedText: '' };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const range = selection.getRangeAt(0);
|
|
41
|
+
|
|
42
|
+
// 要素内のテキスト全体を取得
|
|
43
|
+
const fullText = element.innerText || element.textContent || '';
|
|
44
|
+
|
|
45
|
+
// 選択範囲の開始位置を計算
|
|
46
|
+
const preRange = document.createRange();
|
|
47
|
+
preRange.selectNodeContents(element);
|
|
48
|
+
preRange.setEnd(range.startContainer, range.startOffset);
|
|
49
|
+
const start = (preRange.toString()).length;
|
|
50
|
+
|
|
51
|
+
// 選択範囲の終了位置を計算
|
|
52
|
+
const selectedText = range.toString();
|
|
53
|
+
const end = start + selectedText.length;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
start: start,
|
|
57
|
+
end: end,
|
|
58
|
+
selectedText: selectedText
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setContentEditableSelection(element, start, end) {
|
|
63
|
+
const textNodes = [];
|
|
64
|
+
const walker = document.createTreeWalker(
|
|
65
|
+
element,
|
|
66
|
+
NodeFilter.SHOW_TEXT,
|
|
67
|
+
null,
|
|
68
|
+
false
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
let node;
|
|
72
|
+
while (node = walker.nextNode()) {
|
|
73
|
+
textNodes.push(node);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let currentOffset = 0;
|
|
77
|
+
let startNode = null, startOffset = 0;
|
|
78
|
+
let endNode = null, endOffset = 0;
|
|
79
|
+
|
|
80
|
+
for (const textNode of textNodes) {
|
|
81
|
+
const nodeLength = textNode.textContent.length;
|
|
82
|
+
|
|
83
|
+
if (startNode === null && currentOffset + nodeLength >= start) {
|
|
84
|
+
startNode = textNode;
|
|
85
|
+
startOffset = start - currentOffset;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (endNode === null && currentOffset + nodeLength >= end) {
|
|
89
|
+
endNode = textNode;
|
|
90
|
+
endOffset = end - currentOffset;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
currentOffset += nodeLength;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (startNode && endNode) {
|
|
98
|
+
const range = document.createRange();
|
|
99
|
+
range.setStart(startNode, startOffset);
|
|
100
|
+
range.setEnd(endNode, endOffset);
|
|
101
|
+
|
|
102
|
+
const selection = window.getSelection();
|
|
103
|
+
selection.removeAllRanges();
|
|
104
|
+
selection.addRange(range);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
33
108
|
// Markdownテキスト挿入機能
|
|
34
109
|
window.insertMarkdown = function(before, after) {
|
|
35
110
|
after = after || '';
|
|
@@ -40,15 +115,40 @@
|
|
|
40
115
|
}
|
|
41
116
|
|
|
42
117
|
try {
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
118
|
+
// contenteditable要素かどうかを判定
|
|
119
|
+
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
120
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
121
|
+
|
|
122
|
+
let start, end, selectedText;
|
|
123
|
+
|
|
124
|
+
if (isContentEditable) {
|
|
125
|
+
// contenteditable要素の場合
|
|
126
|
+
const selection = getContentEditableSelection(textarea);
|
|
127
|
+
start = selection.start;
|
|
128
|
+
end = selection.end;
|
|
129
|
+
selectedText = selection.selectedText;
|
|
130
|
+
} else {
|
|
131
|
+
// 通常のtextarea要素の場合
|
|
132
|
+
start = textarea.selectionStart || 0;
|
|
133
|
+
end = textarea.selectionEnd || 0;
|
|
134
|
+
selectedText = textarea.value.substring(start, end);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const fullText = isContentEditable ?
|
|
138
|
+
(textarea.innerText || textarea.textContent || '') :
|
|
139
|
+
textarea.value;
|
|
46
140
|
|
|
47
|
-
const beforeText =
|
|
48
|
-
const afterText =
|
|
141
|
+
const beforeText = fullText.substring(0, start);
|
|
142
|
+
const afterText = fullText.substring(end);
|
|
49
143
|
const newText = before + selectedText + after;
|
|
50
144
|
|
|
51
|
-
|
|
145
|
+
const newFullText = beforeText + newText + afterText;
|
|
146
|
+
|
|
147
|
+
if (isContentEditable) {
|
|
148
|
+
textarea.innerText = newFullText;
|
|
149
|
+
} else {
|
|
150
|
+
textarea.value = newFullText;
|
|
151
|
+
}
|
|
52
152
|
|
|
53
153
|
// カーソル位置調整
|
|
54
154
|
const newCursorPos = selectedText.length > 0 ?
|
|
@@ -56,7 +156,10 @@
|
|
|
56
156
|
start + before.length;
|
|
57
157
|
|
|
58
158
|
textarea.focus();
|
|
59
|
-
|
|
159
|
+
|
|
160
|
+
if (isContentEditable) {
|
|
161
|
+
setContentEditableSelection(textarea, newCursorPos, newCursorPos);
|
|
162
|
+
} else if (textarea.setSelectionRange) {
|
|
60
163
|
textarea.setSelectionRange(newCursorPos, newCursorPos);
|
|
61
164
|
}
|
|
62
165
|
|
|
@@ -70,7 +173,18 @@
|
|
|
70
173
|
|
|
71
174
|
window.insertCode = function() {
|
|
72
175
|
const textarea = document.getElementById('editor_content');
|
|
73
|
-
|
|
176
|
+
|
|
177
|
+
// contenteditable要素かどうかを判定
|
|
178
|
+
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
179
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
180
|
+
|
|
181
|
+
let selectedText;
|
|
182
|
+
if (isContentEditable) {
|
|
183
|
+
const selection = getContentEditableSelection(textarea);
|
|
184
|
+
selectedText = selection.selectedText;
|
|
185
|
+
} else {
|
|
186
|
+
selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
|
|
187
|
+
}
|
|
74
188
|
|
|
75
189
|
if (selectedText.includes('\n')) {
|
|
76
190
|
// 改行が含まれている場合はコードブロック
|
|
@@ -93,7 +207,14 @@
|
|
|
93
207
|
|
|
94
208
|
if (isPreviewMode) {
|
|
95
209
|
// プレビューモードに切り替え
|
|
96
|
-
|
|
210
|
+
// contenteditable要素かどうかを判定してテキストを取得
|
|
211
|
+
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
212
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
213
|
+
|
|
214
|
+
const markdownText = isContentEditable ?
|
|
215
|
+
(textarea.innerText || textarea.textContent || '') :
|
|
216
|
+
(textarea.value || '');
|
|
217
|
+
|
|
97
218
|
const htmlContent = convertMarkdownToHtml(markdownText);
|
|
98
219
|
|
|
99
220
|
previewContainer.innerHTML = htmlContent;
|
|
@@ -676,25 +797,58 @@
|
|
|
676
797
|
window.insertTextAtCursor = function(textarea, text) {
|
|
677
798
|
if (!textarea || !text) return;
|
|
678
799
|
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
|
|
800
|
+
// contenteditable要素かどうかを判定
|
|
801
|
+
const isContentEditable = textarea.contentEditable === 'true' ||
|
|
802
|
+
textarea.getAttribute('contenteditable') === 'true';
|
|
803
|
+
|
|
804
|
+
if (isContentEditable) {
|
|
805
|
+
// contenteditable要素の場合
|
|
806
|
+
const selection = getContentEditableSelection(textarea);
|
|
807
|
+
const start = selection.start;
|
|
808
|
+
const end = selection.end;
|
|
809
|
+
const currentValue = textarea.innerText || textarea.textContent || '';
|
|
810
|
+
|
|
811
|
+
// 挿入位置の前後に改行を追加(必要に応じて)
|
|
812
|
+
let insertText = text;
|
|
813
|
+
if (start > 0 && currentValue[start - 1] !== '\n') {
|
|
814
|
+
insertText = '\n' + insertText;
|
|
815
|
+
}
|
|
816
|
+
if (end < currentValue.length && currentValue[end] !== '\n') {
|
|
817
|
+
insertText = insertText + '\n';
|
|
818
|
+
}
|
|
682
819
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
820
|
+
const newValue = currentValue.substring(0, start) + insertText + currentValue.substring(end);
|
|
821
|
+
textarea.innerText = newValue;
|
|
822
|
+
textarea.focus();
|
|
823
|
+
|
|
824
|
+
const newCursorPos = start + insertText.length;
|
|
825
|
+
setContentEditableSelection(textarea, newCursorPos, newCursorPos);
|
|
826
|
+
|
|
827
|
+
// 入力イベントを発火
|
|
828
|
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
|
691
829
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
830
|
+
} else {
|
|
831
|
+
// 通常のtextarea要素の場合
|
|
832
|
+
const start = textarea.selectionStart;
|
|
833
|
+
const end = textarea.selectionEnd;
|
|
834
|
+
const currentValue = textarea.value;
|
|
835
|
+
|
|
836
|
+
// 挿入位置の前後に改行を追加(必要に応じて)
|
|
837
|
+
let insertText = text;
|
|
838
|
+
if (start > 0 && currentValue[start - 1] !== '\n') {
|
|
839
|
+
insertText = '\n' + insertText;
|
|
840
|
+
}
|
|
841
|
+
if (end < currentValue.length && currentValue[end] !== '\n') {
|
|
842
|
+
insertText = insertText + '\n';
|
|
843
|
+
}
|
|
695
844
|
|
|
696
|
-
|
|
697
|
-
|
|
845
|
+
textarea.value = currentValue.substring(0, start) + insertText + currentValue.substring(end);
|
|
846
|
+
textarea.focus();
|
|
847
|
+
textarea.selectionStart = textarea.selectionEnd = start + insertText.length;
|
|
848
|
+
|
|
849
|
+
// Railsのフォームバリデーション用にchangeイベントを発火
|
|
850
|
+
textarea.dispatchEvent(new Event('change', { bubbles: true }));
|
|
851
|
+
}
|
|
698
852
|
}
|
|
699
853
|
|
|
700
854
|
// 遅延実行のためのデバウンス関数
|