markdown-actions 1.0.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.
- package/LICENSE +33 -0
- package/README.md +379 -0
- package/dist/markdown-actions.esm.js +625 -0
- package/dist/markdown-actions.esm.js.map +7 -0
- package/dist/markdown-actions.js +646 -0
- package/dist/markdown-actions.js.map +7 -0
- package/dist/markdown-actions.min.js +19 -0
- package/package.json +45 -0
- package/src/core/detection.js +169 -0
- package/src/core/formats.js +76 -0
- package/src/core/insertion-new.js +66 -0
- package/src/core/insertion.js +79 -0
- package/src/core/selection-new.js +137 -0
- package/src/core/selection.js +117 -0
- package/src/core/style-args.js +33 -0
- package/src/core/style-text.js +30 -0
- package/src/index-new.js +260 -0
- package/src/index.js +229 -0
- package/src/operations/block-new.js +109 -0
- package/src/operations/block.js +139 -0
- package/src/operations/list-new.js +137 -0
- package/src/operations/list.js +152 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selection utilities - EXACT match to GitHub's implementation
|
|
3
|
+
* Based on markdown-toolbar-element index.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if string contains multiple lines
|
|
8
|
+
* GitHub: index.ts line 419-421
|
|
9
|
+
*/
|
|
10
|
+
export function isMultipleLines(string) {
|
|
11
|
+
return string.trim().split('\n').length > 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Repeat string n times
|
|
16
|
+
* GitHub: index.ts line 423-425
|
|
17
|
+
*/
|
|
18
|
+
function repeat(string, n) {
|
|
19
|
+
return Array(n + 1).join(string)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find word selection start
|
|
24
|
+
* GitHub: index.ts line 427-433
|
|
25
|
+
*/
|
|
26
|
+
export function wordSelectionStart(text, i) {
|
|
27
|
+
let index = i
|
|
28
|
+
while (text[index] && text[index - 1] != null && !text[index - 1].match(/\s/)) {
|
|
29
|
+
index--
|
|
30
|
+
}
|
|
31
|
+
return index
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find word selection end
|
|
36
|
+
* GitHub: index.ts line 435-442
|
|
37
|
+
*/
|
|
38
|
+
export function wordSelectionEnd(text, i, multiline) {
|
|
39
|
+
let index = i
|
|
40
|
+
const breakpoint = multiline ? /\n/ : /\s/
|
|
41
|
+
while (text[index] && !text[index].match(breakpoint)) {
|
|
42
|
+
index++
|
|
43
|
+
}
|
|
44
|
+
return index
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Expand selection to line boundaries
|
|
49
|
+
* GitHub: index.ts line 502-515
|
|
50
|
+
* CRITICAL: This modifies the textarea selection directly!
|
|
51
|
+
*/
|
|
52
|
+
export function expandSelectionToLine(textarea) {
|
|
53
|
+
const lines = textarea.value.split('\n')
|
|
54
|
+
let counter = 0
|
|
55
|
+
for (let index = 0; index < lines.length; index++) {
|
|
56
|
+
const lineLength = lines[index].length + 1
|
|
57
|
+
if (textarea.selectionStart >= counter && textarea.selectionStart < counter + lineLength) {
|
|
58
|
+
textarea.selectionStart = counter
|
|
59
|
+
}
|
|
60
|
+
if (textarea.selectionEnd >= counter && textarea.selectionEnd < counter + lineLength) {
|
|
61
|
+
textarea.selectionEnd = counter + lineLength - 1
|
|
62
|
+
}
|
|
63
|
+
counter += lineLength
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Expand selected text
|
|
69
|
+
* GitHub: index.ts line 517-537
|
|
70
|
+
* CRITICAL: This modifies the textarea selection to include format markers if they exist
|
|
71
|
+
*/
|
|
72
|
+
export function expandSelectedText(textarea, prefixToUse, suffixToUse, multiline = false) {
|
|
73
|
+
if (textarea.selectionStart === textarea.selectionEnd) {
|
|
74
|
+
textarea.selectionStart = wordSelectionStart(textarea.value, textarea.selectionStart)
|
|
75
|
+
textarea.selectionEnd = wordSelectionEnd(textarea.value, textarea.selectionEnd, multiline)
|
|
76
|
+
} else {
|
|
77
|
+
const expandedSelectionStart = textarea.selectionStart - prefixToUse.length
|
|
78
|
+
const expandedSelectionEnd = textarea.selectionEnd + suffixToUse.length
|
|
79
|
+
const beginsWithPrefix = textarea.value.slice(expandedSelectionStart, textarea.selectionStart) === prefixToUse
|
|
80
|
+
const endsWithSuffix = textarea.value.slice(textarea.selectionEnd, expandedSelectionEnd) === suffixToUse
|
|
81
|
+
if (beginsWithPrefix && endsWithSuffix) {
|
|
82
|
+
textarea.selectionStart = expandedSelectionStart
|
|
83
|
+
textarea.selectionEnd = expandedSelectionEnd
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Calculate newlines to surround selected text
|
|
91
|
+
* GitHub: index.ts line 544-573
|
|
92
|
+
*/
|
|
93
|
+
export function newlinesToSurroundSelectedText(textarea) {
|
|
94
|
+
const beforeSelection = textarea.value.slice(0, textarea.selectionStart)
|
|
95
|
+
const afterSelection = textarea.value.slice(textarea.selectionEnd)
|
|
96
|
+
|
|
97
|
+
const breaksBefore = beforeSelection.match(/\n*$/)
|
|
98
|
+
const breaksAfter = afterSelection.match(/^\n*/)
|
|
99
|
+
const newlinesBeforeSelection = breaksBefore ? breaksBefore[0].length : 0
|
|
100
|
+
const newlinesAfterSelection = breaksAfter ? breaksAfter[0].length : 0
|
|
101
|
+
|
|
102
|
+
let newlinesToAppend
|
|
103
|
+
let newlinesToPrepend
|
|
104
|
+
|
|
105
|
+
if (beforeSelection.match(/\S/) && newlinesBeforeSelection < 2) {
|
|
106
|
+
newlinesToAppend = repeat('\n', 2 - newlinesBeforeSelection)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (afterSelection.match(/\S/) && newlinesAfterSelection < 2) {
|
|
110
|
+
newlinesToPrepend = repeat('\n', 2 - newlinesAfterSelection)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (newlinesToAppend == null) {
|
|
114
|
+
newlinesToAppend = ''
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (newlinesToPrepend == null) {
|
|
118
|
+
newlinesToPrepend = ''
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { newlinesToAppend, newlinesToPrepend }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Preserve selection helper (not in GitHub's but useful)
|
|
126
|
+
*/
|
|
127
|
+
export function preserveSelection(textarea, callback) {
|
|
128
|
+
const start = textarea.selectionStart
|
|
129
|
+
const end = textarea.selectionEnd
|
|
130
|
+
const scrollTop = textarea.scrollTop
|
|
131
|
+
|
|
132
|
+
callback()
|
|
133
|
+
|
|
134
|
+
textarea.selectionStart = start
|
|
135
|
+
textarea.selectionEnd = end
|
|
136
|
+
textarea.scrollTop = scrollTop
|
|
137
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core selection utilities extracted and adapted from GitHub's markdown-toolbar-element
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if string contains multiple lines
|
|
7
|
+
*/
|
|
8
|
+
export function isMultipleLines(string) {
|
|
9
|
+
return string.trim().split('\n').length > 1
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find the start of the word at position i
|
|
14
|
+
*/
|
|
15
|
+
export function wordSelectionStart(text, i) {
|
|
16
|
+
let index = i
|
|
17
|
+
while (text[index] && text[index - 1] != null && !text[index - 1].match(/\s/)) {
|
|
18
|
+
index--
|
|
19
|
+
}
|
|
20
|
+
return index
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Find the end of the word at position i
|
|
25
|
+
*/
|
|
26
|
+
export function wordSelectionEnd(text, i, multiline) {
|
|
27
|
+
let index = i
|
|
28
|
+
const breakpoint = multiline ? /\n/ : /\s/
|
|
29
|
+
while (text[index] && !text[index].match(breakpoint)) {
|
|
30
|
+
index++
|
|
31
|
+
}
|
|
32
|
+
return index
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Expand selection to line boundaries
|
|
37
|
+
*/
|
|
38
|
+
export function expandSelectionToLine(textarea) {
|
|
39
|
+
const lines = textarea.value.split('\n')
|
|
40
|
+
let counter = 0
|
|
41
|
+
for (let index = 0; index < lines.length; index++) {
|
|
42
|
+
const lineLength = lines[index].length + 1
|
|
43
|
+
if (textarea.selectionStart >= counter && textarea.selectionStart < counter + lineLength) {
|
|
44
|
+
textarea.selectionStart = counter
|
|
45
|
+
}
|
|
46
|
+
if (textarea.selectionEnd >= counter && textarea.selectionEnd < counter + lineLength) {
|
|
47
|
+
// For the last line, don't go past the actual text length
|
|
48
|
+
if (index === lines.length - 1) {
|
|
49
|
+
textarea.selectionEnd = Math.min(counter + lines[index].length, textarea.value.length)
|
|
50
|
+
} else {
|
|
51
|
+
textarea.selectionEnd = counter + lineLength - 1
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
counter += lineLength
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Expand selected text with smart boundary detection
|
|
60
|
+
*/
|
|
61
|
+
export function expandSelectedText(textarea, prefixToUse, suffixToUse, multiline = false) {
|
|
62
|
+
if (textarea.selectionStart === textarea.selectionEnd) {
|
|
63
|
+
textarea.selectionStart = wordSelectionStart(textarea.value, textarea.selectionStart)
|
|
64
|
+
textarea.selectionEnd = wordSelectionEnd(textarea.value, textarea.selectionEnd, multiline)
|
|
65
|
+
} else {
|
|
66
|
+
const expandedSelectionStart = textarea.selectionStart - prefixToUse.length
|
|
67
|
+
const expandedSelectionEnd = textarea.selectionEnd + suffixToUse.length
|
|
68
|
+
const beginsWithPrefix = textarea.value.slice(expandedSelectionStart, textarea.selectionStart) === prefixToUse
|
|
69
|
+
const endsWithSuffix = textarea.value.slice(textarea.selectionEnd, expandedSelectionEnd) === suffixToUse
|
|
70
|
+
if (beginsWithPrefix && endsWithSuffix) {
|
|
71
|
+
textarea.selectionStart = expandedSelectionStart
|
|
72
|
+
textarea.selectionEnd = expandedSelectionEnd
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Calculate newlines needed to surround selected text
|
|
80
|
+
*/
|
|
81
|
+
export function newlinesToSurroundSelectedText(textarea) {
|
|
82
|
+
const beforeSelection = textarea.value.slice(0, textarea.selectionStart)
|
|
83
|
+
const afterSelection = textarea.value.slice(textarea.selectionEnd)
|
|
84
|
+
|
|
85
|
+
const breaksBefore = beforeSelection.match(/\n*$/)
|
|
86
|
+
const breaksAfter = afterSelection.match(/^\n*/)
|
|
87
|
+
const newlinesBeforeSelection = breaksBefore ? breaksBefore[0].length : 0
|
|
88
|
+
const newlinesAfterSelection = breaksAfter ? breaksAfter[0].length : 0
|
|
89
|
+
|
|
90
|
+
let newlinesToAppend = ''
|
|
91
|
+
let newlinesToPrepend = ''
|
|
92
|
+
|
|
93
|
+
if (beforeSelection.match(/\S/) && newlinesBeforeSelection < 2) {
|
|
94
|
+
newlinesToAppend = '\n'.repeat(2 - newlinesBeforeSelection)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (afterSelection.match(/\S/) && newlinesAfterSelection < 2) {
|
|
98
|
+
newlinesToPrepend = '\n'.repeat(2 - newlinesAfterSelection)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { newlinesToAppend, newlinesToPrepend }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Utility to preserve selection during operations
|
|
106
|
+
*/
|
|
107
|
+
export function preserveSelection(textarea, callback) {
|
|
108
|
+
const start = textarea.selectionStart
|
|
109
|
+
const end = textarea.selectionEnd
|
|
110
|
+
const scrollTop = textarea.scrollTop
|
|
111
|
+
|
|
112
|
+
callback()
|
|
113
|
+
|
|
114
|
+
textarea.selectionStart = start
|
|
115
|
+
textarea.selectionEnd = end
|
|
116
|
+
textarea.scrollTop = scrollTop
|
|
117
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style arguments type definition and defaults
|
|
3
|
+
* Matches GitHub's implementation exactly
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get default style arguments
|
|
8
|
+
* MUST match GitHub's defaults exactly (index.ts lines 805-818)
|
|
9
|
+
*/
|
|
10
|
+
export function getDefaultStyleArgs() {
|
|
11
|
+
return {
|
|
12
|
+
prefix: '',
|
|
13
|
+
suffix: '',
|
|
14
|
+
blockPrefix: '',
|
|
15
|
+
blockSuffix: '',
|
|
16
|
+
multiline: false,
|
|
17
|
+
replaceNext: '',
|
|
18
|
+
prefixSpace: false,
|
|
19
|
+
scanFor: '',
|
|
20
|
+
surroundWithNewlines: false,
|
|
21
|
+
orderedList: false,
|
|
22
|
+
unorderedList: false,
|
|
23
|
+
trimFirst: false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Merge style with defaults
|
|
29
|
+
* Ensures ALL properties are present
|
|
30
|
+
*/
|
|
31
|
+
export function mergeStyleWithDefaults(style) {
|
|
32
|
+
return { ...getDefaultStyleArgs(), ...style }
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main text styling orchestration
|
|
3
|
+
* EXACT match to GitHub's implementation
|
|
4
|
+
* Based on markdown-toolbar-element index.ts lines 487-500
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isMultipleLines } from './selection-new.js'
|
|
8
|
+
import { insertText } from './insertion-new.js'
|
|
9
|
+
import { blockStyle, multilineStyle } from '../operations/block-new.js'
|
|
10
|
+
import { listStyle } from '../operations/list-new.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Style selected text in textarea
|
|
14
|
+
* GitHub: index.ts lines 487-500
|
|
15
|
+
* CRITICAL: This is the main orchestrator that decides which operation to use
|
|
16
|
+
*/
|
|
17
|
+
export function styleSelectedText(textarea, styleArgs) {
|
|
18
|
+
const text = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)
|
|
19
|
+
|
|
20
|
+
let result
|
|
21
|
+
if (styleArgs.orderedList || styleArgs.unorderedList) {
|
|
22
|
+
result = listStyle(textarea, styleArgs)
|
|
23
|
+
} else if (styleArgs.multiline && isMultipleLines(text)) {
|
|
24
|
+
result = multilineStyle(textarea, styleArgs)
|
|
25
|
+
} else {
|
|
26
|
+
result = blockStyle(textarea, styleArgs)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
insertText(textarea, result)
|
|
30
|
+
}
|
package/src/index-new.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* markdown-actions - Lightweight markdown toolbar functionality
|
|
3
|
+
* Exact implementation matching GitHub's markdown-toolbar-element
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mergeStyleWithDefaults } from './core/style-args.js'
|
|
7
|
+
import { styleSelectedText } from './core/style-text.js'
|
|
8
|
+
import { setUndoMethod } from './core/insertion-new.js'
|
|
9
|
+
import { preserveSelection, expandSelectionToLine } from './core/selection-new.js'
|
|
10
|
+
import { getActiveFormats as getActive, hasFormat as has } from './core/detection.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format configurations matching GitHub's manualStyles
|
|
14
|
+
* GitHub: index.ts lines 86-118
|
|
15
|
+
*/
|
|
16
|
+
const FORMATS = {
|
|
17
|
+
bold: { prefix: '**', suffix: '**', trimFirst: true },
|
|
18
|
+
italic: { prefix: '_', suffix: '_', trimFirst: true },
|
|
19
|
+
code: {
|
|
20
|
+
prefix: '`',
|
|
21
|
+
suffix: '`',
|
|
22
|
+
blockPrefix: '```',
|
|
23
|
+
blockSuffix: '```'
|
|
24
|
+
},
|
|
25
|
+
link: {
|
|
26
|
+
prefix: '[',
|
|
27
|
+
suffix: '](url)',
|
|
28
|
+
replaceNext: 'url',
|
|
29
|
+
scanFor: 'https?://'
|
|
30
|
+
},
|
|
31
|
+
quote: {
|
|
32
|
+
prefix: '> ',
|
|
33
|
+
multiline: true,
|
|
34
|
+
surroundWithNewlines: true
|
|
35
|
+
},
|
|
36
|
+
bulletList: {
|
|
37
|
+
prefix: '- ',
|
|
38
|
+
multiline: true,
|
|
39
|
+
unorderedList: true
|
|
40
|
+
},
|
|
41
|
+
numberedList: {
|
|
42
|
+
prefix: '1. ',
|
|
43
|
+
multiline: true,
|
|
44
|
+
orderedList: true
|
|
45
|
+
},
|
|
46
|
+
header1: { prefix: '# ' },
|
|
47
|
+
header2: { prefix: '## ' },
|
|
48
|
+
header3: { prefix: '### ' },
|
|
49
|
+
header4: { prefix: '#### ' },
|
|
50
|
+
header5: { prefix: '##### ' },
|
|
51
|
+
header6: { prefix: '###### ' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Apply style to textarea
|
|
56
|
+
* Mimics GitHub's applyStyle function (index.ts lines 801-827)
|
|
57
|
+
*/
|
|
58
|
+
function applyStyle(textarea, format) {
|
|
59
|
+
const style = mergeStyleWithDefaults(format)
|
|
60
|
+
textarea.focus()
|
|
61
|
+
styleSelectedText(textarea, style)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Toggle bold formatting
|
|
66
|
+
*/
|
|
67
|
+
export function toggleBold(textarea) {
|
|
68
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
69
|
+
applyStyle(textarea, FORMATS.bold)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Toggle italic formatting
|
|
74
|
+
*/
|
|
75
|
+
export function toggleItalic(textarea) {
|
|
76
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
77
|
+
applyStyle(textarea, FORMATS.italic)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Toggle code formatting
|
|
82
|
+
*/
|
|
83
|
+
export function toggleCode(textarea) {
|
|
84
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
85
|
+
applyStyle(textarea, FORMATS.code)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Insert or toggle link formatting
|
|
90
|
+
*/
|
|
91
|
+
export function insertLink(textarea, options = {}) {
|
|
92
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
93
|
+
|
|
94
|
+
let format = { ...FORMATS.link }
|
|
95
|
+
|
|
96
|
+
// Override with custom URL if provided
|
|
97
|
+
if (options.url) {
|
|
98
|
+
format.suffix = `](${options.url})`
|
|
99
|
+
format.replaceNext = ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle custom text if provided
|
|
103
|
+
if (options.text && !textarea.value.slice(textarea.selectionStart, textarea.selectionEnd)) {
|
|
104
|
+
const pos = textarea.selectionStart
|
|
105
|
+
textarea.value = textarea.value.slice(0, pos) + options.text + textarea.value.slice(pos)
|
|
106
|
+
textarea.selectionStart = pos
|
|
107
|
+
textarea.selectionEnd = pos + options.text.length
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
applyStyle(textarea, format)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Toggle bullet list formatting
|
|
115
|
+
*/
|
|
116
|
+
export function toggleBulletList(textarea) {
|
|
117
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
118
|
+
applyStyle(textarea, FORMATS.bulletList)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Toggle numbered list formatting
|
|
123
|
+
*/
|
|
124
|
+
export function toggleNumberedList(textarea) {
|
|
125
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
126
|
+
applyStyle(textarea, FORMATS.numberedList)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Toggle quote formatting
|
|
131
|
+
*/
|
|
132
|
+
export function toggleQuote(textarea) {
|
|
133
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
134
|
+
applyStyle(textarea, FORMATS.quote)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Insert header with specific level
|
|
139
|
+
*/
|
|
140
|
+
export function insertHeader(textarea, level = 1) {
|
|
141
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
142
|
+
if (level < 1 || level > 6) level = 1
|
|
143
|
+
|
|
144
|
+
const headerKey = `header${level}`
|
|
145
|
+
const format = FORMATS[headerKey] || FORMATS.header1
|
|
146
|
+
|
|
147
|
+
// Select current line
|
|
148
|
+
const value = textarea.value
|
|
149
|
+
const start = textarea.selectionStart
|
|
150
|
+
let lineStart = start
|
|
151
|
+
let lineEnd = start
|
|
152
|
+
|
|
153
|
+
// Find line boundaries
|
|
154
|
+
while (lineStart > 0 && value[lineStart - 1] !== '\n') {
|
|
155
|
+
lineStart--
|
|
156
|
+
}
|
|
157
|
+
while (lineEnd < value.length && value[lineEnd] !== '\n') {
|
|
158
|
+
lineEnd++
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
textarea.selectionStart = lineStart
|
|
162
|
+
textarea.selectionEnd = lineEnd
|
|
163
|
+
|
|
164
|
+
const currentLine = value.slice(lineStart, lineEnd)
|
|
165
|
+
|
|
166
|
+
// Remove any existing header formatting
|
|
167
|
+
const cleanedLine = currentLine.replace(/^#{1,6}\s*/, '')
|
|
168
|
+
|
|
169
|
+
// Apply new header
|
|
170
|
+
const newLine = format.prefix + cleanedLine
|
|
171
|
+
|
|
172
|
+
// Use our style system
|
|
173
|
+
const tempSelection = textarea.selectionStart
|
|
174
|
+
textarea.value = value.slice(0, lineStart) + newLine + value.slice(lineEnd)
|
|
175
|
+
textarea.selectionStart = lineStart + format.prefix.length
|
|
176
|
+
textarea.selectionEnd = lineStart + newLine.length
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get active formats at cursor position
|
|
181
|
+
*/
|
|
182
|
+
export function getActiveFormats(textarea) {
|
|
183
|
+
return getActive(textarea)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if format is active at cursor
|
|
188
|
+
*/
|
|
189
|
+
export function hasFormat(textarea, format) {
|
|
190
|
+
return has(textarea, format)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Expand selection based on options
|
|
195
|
+
*/
|
|
196
|
+
export function expandSelection(textarea, options = {}) {
|
|
197
|
+
if (!textarea) return
|
|
198
|
+
|
|
199
|
+
const { toWord, toLine } = options
|
|
200
|
+
const { selectionStart, selectionEnd, value } = textarea
|
|
201
|
+
|
|
202
|
+
if (toLine) {
|
|
203
|
+
expandSelectionToLine(textarea)
|
|
204
|
+
} else if (toWord && selectionStart === selectionEnd) {
|
|
205
|
+
// Find word boundaries
|
|
206
|
+
let start = selectionStart
|
|
207
|
+
let end = selectionEnd
|
|
208
|
+
|
|
209
|
+
// Move start back to word boundary
|
|
210
|
+
while (start > 0 && !/\s/.test(value[start - 1])) {
|
|
211
|
+
start--
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Move end forward to word boundary
|
|
215
|
+
while (end < value.length && !/\s/.test(value[end])) {
|
|
216
|
+
end++
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
textarea.selectionStart = start
|
|
220
|
+
textarea.selectionEnd = end
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Apply custom format
|
|
226
|
+
*/
|
|
227
|
+
export function applyCustomFormat(textarea, format) {
|
|
228
|
+
if (!textarea || textarea.disabled || textarea.readOnly) return
|
|
229
|
+
applyStyle(textarea, format)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Preserve selection during callback
|
|
234
|
+
*/
|
|
235
|
+
export { preserveSelection }
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Configure undo method
|
|
239
|
+
*/
|
|
240
|
+
export { setUndoMethod }
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Default export with all functions
|
|
244
|
+
*/
|
|
245
|
+
export default {
|
|
246
|
+
toggleBold,
|
|
247
|
+
toggleItalic,
|
|
248
|
+
toggleCode,
|
|
249
|
+
insertLink,
|
|
250
|
+
toggleBulletList,
|
|
251
|
+
toggleNumberedList,
|
|
252
|
+
toggleQuote,
|
|
253
|
+
insertHeader,
|
|
254
|
+
getActiveFormats,
|
|
255
|
+
hasFormat,
|
|
256
|
+
expandSelection,
|
|
257
|
+
applyCustomFormat,
|
|
258
|
+
preserveSelection,
|
|
259
|
+
setUndoMethod
|
|
260
|
+
}
|