markdown-actions 1.0.2 → 1.0.4
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/dist/markdown-actions.esm.js +74 -71
- package/dist/markdown-actions.esm.js.map +2 -2
- package/dist/markdown-actions.js +74 -71
- package/dist/markdown-actions.js.map +2 -2
- package/dist/markdown-actions.min.js +18 -20
- package/package.json +1 -1
- package/src/core/selection.js +45 -22
- package/src/index.js +63 -88
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markdown-actions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "A lightweight, zero-dependency JavaScript library for adding markdown formatting functionality to any textarea element",
|
|
5
5
|
"main": "dist/markdown-actions.js",
|
|
6
6
|
"module": "dist/markdown-actions.esm.js",
|
package/src/core/selection.js
CHANGED
|
@@ -123,26 +123,28 @@ export function preserveSelection(textarea, callback) {
|
|
|
123
123
|
* @param {Function} operation - The operation to perform (receives textarea and returns result)
|
|
124
124
|
* @param {Object} options - Options for the operation
|
|
125
125
|
* @param {string} options.prefix - The prefix being added/removed (for cursor adjustment)
|
|
126
|
+
* @param {Function} options.adjustSelection - Custom selection adjustment function
|
|
126
127
|
* @returns {Object} The result from the operation with adjusted cursor position
|
|
127
128
|
*/
|
|
128
129
|
export function applyLineOperation(textarea, operation, options = {}) {
|
|
129
|
-
// Save original cursor position before any expansion
|
|
130
|
+
// Save original cursor position AND selection before any expansion
|
|
130
131
|
const originalStart = textarea.selectionStart
|
|
131
132
|
const originalEnd = textarea.selectionEnd
|
|
132
133
|
const noInitialSelection = originalStart === originalEnd
|
|
133
134
|
|
|
134
|
-
//
|
|
135
|
+
// Store the line start position to calculate offset later
|
|
136
|
+
const value = textarea.value
|
|
137
|
+
let lineStart = originalStart
|
|
138
|
+
|
|
139
|
+
// Find start of the line containing the selection start
|
|
140
|
+
while (lineStart > 0 && value[lineStart - 1] !== '\n') {
|
|
141
|
+
lineStart--
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Expand selection to line boundaries for the operation
|
|
135
145
|
if (noInitialSelection) {
|
|
136
146
|
// Expand to current line when no selection
|
|
137
|
-
|
|
138
|
-
const start = originalStart
|
|
139
|
-
let lineStart = start
|
|
140
|
-
let lineEnd = start
|
|
141
|
-
|
|
142
|
-
// Find start of current line
|
|
143
|
-
while (lineStart > 0 && value[lineStart - 1] !== '\n') {
|
|
144
|
-
lineStart--
|
|
145
|
-
}
|
|
147
|
+
let lineEnd = originalStart
|
|
146
148
|
|
|
147
149
|
// Find end of current line
|
|
148
150
|
while (lineEnd < value.length && value[lineEnd] !== '\n') {
|
|
@@ -152,27 +154,48 @@ export function applyLineOperation(textarea, operation, options = {}) {
|
|
|
152
154
|
textarea.selectionStart = lineStart
|
|
153
155
|
textarea.selectionEnd = lineEnd
|
|
154
156
|
} else {
|
|
155
|
-
// For
|
|
157
|
+
// For selections, expand to full lines
|
|
156
158
|
expandSelectionToLine(textarea)
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
// Apply the operation
|
|
160
162
|
const result = operation(textarea)
|
|
161
163
|
|
|
162
|
-
//
|
|
163
|
-
if (
|
|
164
|
-
//
|
|
164
|
+
// Restore original selection/cursor with prefix adjustment
|
|
165
|
+
if (options.adjustSelection) {
|
|
166
|
+
// Use custom selection adjustment logic
|
|
167
|
+
const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
168
|
+
const isRemoving = selectedText.startsWith(options.prefix)
|
|
169
|
+
const adjusted = options.adjustSelection(isRemoving, originalStart, originalEnd, lineStart)
|
|
170
|
+
result.selectionStart = adjusted.start
|
|
171
|
+
result.selectionEnd = adjusted.end
|
|
172
|
+
} else if (options.prefix) {
|
|
173
|
+
// Use default prefix-based adjustment
|
|
165
174
|
const selectedText = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
166
175
|
const isRemoving = selectedText.startsWith(options.prefix)
|
|
167
176
|
|
|
168
|
-
if (
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
|
|
177
|
+
if (noInitialSelection) {
|
|
178
|
+
// No selection - just restore cursor position
|
|
179
|
+
if (isRemoving) {
|
|
180
|
+
// When removing prefix, adjust cursor position
|
|
181
|
+
result.selectionStart = Math.max(originalStart - options.prefix.length, lineStart)
|
|
182
|
+
result.selectionEnd = result.selectionStart
|
|
183
|
+
} else {
|
|
184
|
+
// When adding prefix, adjust cursor position
|
|
185
|
+
result.selectionStart = originalStart + options.prefix.length
|
|
186
|
+
result.selectionEnd = result.selectionStart
|
|
187
|
+
}
|
|
172
188
|
} else {
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
189
|
+
// Had a selection - restore it with adjustment
|
|
190
|
+
if (isRemoving) {
|
|
191
|
+
// When removing prefix, shift selection back
|
|
192
|
+
result.selectionStart = Math.max(originalStart - options.prefix.length, lineStart)
|
|
193
|
+
result.selectionEnd = Math.max(originalEnd - options.prefix.length, lineStart)
|
|
194
|
+
} else {
|
|
195
|
+
// When adding prefix, shift selection forward
|
|
196
|
+
result.selectionStart = originalStart + options.prefix.length
|
|
197
|
+
result.selectionEnd = originalEnd + options.prefix.length
|
|
198
|
+
}
|
|
176
199
|
}
|
|
177
200
|
}
|
|
178
201
|
|
package/src/index.js
CHANGED
|
@@ -161,102 +161,77 @@ export function insertHeader(textarea, level = 1, toggle = false) {
|
|
|
161
161
|
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
162
162
|
if (level < 1 || level > 6) level = 1
|
|
163
163
|
|
|
164
|
-
// Save original cursor position before any expansion
|
|
165
|
-
const originalStart = textarea.selectionStart
|
|
166
|
-
const originalEnd = textarea.selectionEnd
|
|
167
|
-
const noInitialSelection = originalStart === originalEnd
|
|
168
|
-
|
|
169
164
|
const headerKey = `header${level === 1 ? '1' : level}`
|
|
170
165
|
const style = mergeWithDefaults(FORMATS[headerKey] || FORMATS.header1)
|
|
171
166
|
|
|
172
|
-
//
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
let lineEnd = originalStart
|
|
176
|
-
|
|
177
|
-
// Find line boundaries
|
|
178
|
-
while (lineStart > 0 && value[lineStart - 1] !== '\n') {
|
|
179
|
-
lineStart--
|
|
180
|
-
}
|
|
181
|
-
while (lineEnd < value.length && value[lineEnd] !== '\n') {
|
|
182
|
-
lineEnd++
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
textarea.selectionStart = lineStart
|
|
186
|
-
textarea.selectionEnd = lineEnd
|
|
187
|
-
|
|
188
|
-
const currentLine = value.slice(lineStart, lineEnd)
|
|
189
|
-
|
|
190
|
-
// Check if we're removing or changing existing header
|
|
191
|
-
const existingHeaderMatch = currentLine.match(/^(#{1,6})\s*/)
|
|
192
|
-
const existingPrefixLength = existingHeaderMatch ? existingHeaderMatch[0].length : 0
|
|
193
|
-
const existingLevel = existingHeaderMatch ? existingHeaderMatch[1].length : 0
|
|
194
|
-
|
|
195
|
-
// Remove any existing header formatting
|
|
196
|
-
const cleanedLine = currentLine.replace(/^#{1,6}\s*/, '')
|
|
197
|
-
|
|
198
|
-
let newLine
|
|
199
|
-
let isRemoving = false
|
|
200
|
-
|
|
201
|
-
// If toggle mode and current line has the same header level, remove it
|
|
202
|
-
if (toggle && existingLevel === level) {
|
|
203
|
-
newLine = cleanedLine
|
|
204
|
-
isRemoving = true
|
|
205
|
-
} else {
|
|
206
|
-
// Apply new header
|
|
207
|
-
newLine = style.prefix + cleanedLine
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let selectionStart, selectionEnd
|
|
167
|
+
// Save original positions before any operations
|
|
168
|
+
const originalStart = textarea.selectionStart
|
|
169
|
+
const originalEnd = textarea.selectionEnd
|
|
211
170
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if
|
|
219
|
-
|
|
220
|
-
|
|
171
|
+
// Use applyLineOperation for consistent behavior
|
|
172
|
+
const result = applyLineOperation(
|
|
173
|
+
textarea,
|
|
174
|
+
(ta) => {
|
|
175
|
+
const currentLine = ta.value.slice(ta.selectionStart, ta.selectionEnd)
|
|
176
|
+
|
|
177
|
+
// Check if we're removing or changing existing header
|
|
178
|
+
const existingHeaderMatch = currentLine.match(/^(#{1,6})\s*/)
|
|
179
|
+
const existingLevel = existingHeaderMatch ? existingHeaderMatch[1].length : 0
|
|
180
|
+
|
|
181
|
+
// Remove any existing header formatting
|
|
182
|
+
const cleanedLine = currentLine.replace(/^#{1,6}\s*/, '')
|
|
183
|
+
|
|
184
|
+
let newLine
|
|
185
|
+
|
|
186
|
+
// If toggle mode and current line has the same header level, remove it
|
|
187
|
+
if (toggle && existingLevel === level) {
|
|
188
|
+
newLine = cleanedLine
|
|
221
189
|
} else {
|
|
222
|
-
//
|
|
223
|
-
|
|
190
|
+
// Apply new header
|
|
191
|
+
newLine = style.prefix + cleanedLine
|
|
224
192
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
text: newLine,
|
|
196
|
+
selectionStart: ta.selectionStart,
|
|
197
|
+
selectionEnd: ta.selectionEnd
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
prefix: style.prefix,
|
|
202
|
+
// Custom handler for header-specific logic
|
|
203
|
+
adjustSelection: (isRemoving, selStart, selEnd, lineStart) => {
|
|
204
|
+
const currentLine = textarea.value.slice(lineStart, textarea.selectionEnd)
|
|
205
|
+
const existingHeaderMatch = currentLine.match(/^(#{1,6})\s*/)
|
|
206
|
+
const existingPrefixLength = existingHeaderMatch ? existingHeaderMatch[0].length : 0
|
|
207
|
+
const existingLevel = existingHeaderMatch ? existingHeaderMatch[1].length : 0
|
|
208
|
+
|
|
209
|
+
// Check if we're actually removing (toggle off)
|
|
210
|
+
const actuallyRemoving = toggle && existingLevel === level
|
|
211
|
+
|
|
212
|
+
if (actuallyRemoving) {
|
|
213
|
+
// Removing the header entirely
|
|
214
|
+
return {
|
|
215
|
+
start: Math.max(selStart - existingPrefixLength, lineStart),
|
|
216
|
+
end: Math.max(selEnd - existingPrefixLength, lineStart)
|
|
217
|
+
}
|
|
218
|
+
} else if (existingPrefixLength > 0) {
|
|
219
|
+
// Replacing existing header with new one
|
|
220
|
+
const prefixDiff = style.prefix.length - existingPrefixLength
|
|
221
|
+
return {
|
|
222
|
+
start: selStart + prefixDiff,
|
|
223
|
+
end: selEnd + prefixDiff
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
// Adding new header
|
|
227
|
+
return {
|
|
228
|
+
start: selStart + style.prefix.length,
|
|
229
|
+
end: selEnd + style.prefix.length
|
|
230
|
+
}
|
|
231
|
+
}
|
|
234
232
|
}
|
|
235
|
-
} else {
|
|
236
|
-
// We're adding a new header
|
|
237
|
-
// Move cursor by the length of the new prefix
|
|
238
|
-
selectionStart = originalStart + style.prefix.length
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Ensure cursor doesn't go past end of line
|
|
242
|
-
selectionStart = Math.min(selectionStart, lineStart + newLine.length)
|
|
243
|
-
selectionEnd = selectionStart
|
|
244
|
-
} else {
|
|
245
|
-
// Keep selection if there was one
|
|
246
|
-
if (isRemoving) {
|
|
247
|
-
selectionStart = lineStart
|
|
248
|
-
selectionEnd = lineStart + newLine.length
|
|
249
|
-
} else {
|
|
250
|
-
selectionStart = lineStart + style.prefix.length
|
|
251
|
-
selectionEnd = lineStart + newLine.length
|
|
252
233
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const result = {
|
|
256
|
-
text: newLine,
|
|
257
|
-
selectionStart,
|
|
258
|
-
selectionEnd
|
|
259
|
-
}
|
|
234
|
+
)
|
|
260
235
|
|
|
261
236
|
insertText(textarea, result)
|
|
262
237
|
}
|