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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markdown-actions",
3
- "version": "1.0.2",
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",
@@ -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
- // Expand selection to line boundaries
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
- const value = textarea.value
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 multiline selections, expand to full lines
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
- // If there was no initial selection and we have a prefix, restore cursor position
163
- if (noInitialSelection && options.prefix) {
164
- // Check if we're toggling on or off
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 (isRemoving) {
169
- // When removing prefix, adjust cursor position
170
- result.selectionStart = Math.max(originalStart - options.prefix.length, textarea.selectionStart)
171
- result.selectionEnd = result.selectionStart
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
- // When adding prefix, adjust cursor position
174
- result.selectionStart = originalStart + options.prefix.length
175
- result.selectionEnd = result.selectionStart
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
- // Select current line
173
- const value = textarea.value
174
- let lineStart = originalStart
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
- if (noInitialSelection) {
213
- // Calculate where cursor should be after the operation
214
- const cursorOffsetInLine = originalStart - lineStart
215
-
216
- if (isRemoving) {
217
- // We're removing the header
218
- if (cursorOffsetInLine < existingPrefixLength) {
219
- // Cursor was in the header prefix, move to start of line
220
- selectionStart = lineStart
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
- // Cursor was in the text, move back by prefix length
223
- selectionStart = originalStart - existingPrefixLength
190
+ // Apply new header
191
+ newLine = style.prefix + cleanedLine
224
192
  }
225
- } else if (existingPrefixLength > 0) {
226
- // We're replacing an existing header
227
- if (cursorOffsetInLine < existingPrefixLength) {
228
- // Cursor was in the header prefix, keep it at the same position
229
- selectionStart = lineStart + Math.min(cursorOffsetInLine, style.prefix.length)
230
- } else {
231
- // Cursor was in the text, adjust by the difference in prefix lengths
232
- const prefixDiff = style.prefix.length - existingPrefixLength
233
- selectionStart = originalStart + prefixDiff
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
  }