autopair 1.2.2 → 1.2.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/README.md +4 -0
- package/cypress/e2e/autopair.cy.js +91 -28
- package/package.json +1 -1
- package/src/autopair.js +34 -28
package/README.md
CHANGED
|
@@ -63,6 +63,10 @@ npm install autopair
|
|
|
63
63
|
</script>
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
## Contributing
|
|
67
|
+
|
|
68
|
+
Report issues on the [Veritula issue tracker](https://veritula.com/discussions/autopair-js). Do not submit issues on GitHub.
|
|
69
|
+
|
|
66
70
|
## Development
|
|
67
71
|
|
|
68
72
|
Run a webserver and open `index.html`.
|
|
@@ -15,7 +15,11 @@ describe('autopair', () => {
|
|
|
15
15
|
cy.get('textarea').type('(');
|
|
16
16
|
|
|
17
17
|
// Check result
|
|
18
|
-
cy.get('textarea').
|
|
18
|
+
cy.get('textarea').then($el => {
|
|
19
|
+
expect($el.val()).to.eq('(hello)');
|
|
20
|
+
expect($el[0].selectionStart).to.eq(6); // cursor after )
|
|
21
|
+
expect($el[0].selectionEnd).to.eq(6);
|
|
22
|
+
});
|
|
19
23
|
});
|
|
20
24
|
|
|
21
25
|
it('types through closing character', () => {
|
|
@@ -93,6 +97,7 @@ describe('autopair', () => {
|
|
|
93
97
|
cy.get('textarea').then($el => {
|
|
94
98
|
expect($el.val()).to.eq('();');
|
|
95
99
|
expect($el[0].selectionStart).to.eq(1);
|
|
100
|
+
expect($el[0].selectionEnd).to.eq(1);
|
|
96
101
|
});
|
|
97
102
|
});
|
|
98
103
|
|
|
@@ -115,6 +120,7 @@ describe('autopair', () => {
|
|
|
115
120
|
cy.get('textarea').then($el => {
|
|
116
121
|
expect($el.val()).to.eq('(.');
|
|
117
122
|
expect($el[0].selectionStart).to.eq(1);
|
|
123
|
+
expect($el[0].selectionEnd).to.eq(1);
|
|
118
124
|
});
|
|
119
125
|
});
|
|
120
126
|
|
|
@@ -128,6 +134,7 @@ describe('autopair', () => {
|
|
|
128
134
|
cy.get('textarea').then($el => {
|
|
129
135
|
expect($el.val()).to.eq('((()))');
|
|
130
136
|
expect($el[0].selectionStart).to.eq(3); // cursor inside the innermost pair
|
|
137
|
+
expect($el[0].selectionEnd).to.eq(3);
|
|
131
138
|
});
|
|
132
139
|
});
|
|
133
140
|
|
|
@@ -141,6 +148,7 @@ describe('autopair', () => {
|
|
|
141
148
|
cy.get('textarea').then($el => {
|
|
142
149
|
expect($el.val()).to.eq("''");
|
|
143
150
|
expect($el[0].selectionStart).to.eq(1); // cursor inside
|
|
151
|
+
expect($el[0].selectionEnd).to.eq(1);
|
|
144
152
|
});
|
|
145
153
|
|
|
146
154
|
// Move cursor to the end
|
|
@@ -156,37 +164,92 @@ describe('autopair', () => {
|
|
|
156
164
|
cy.get('textarea').then($el => {
|
|
157
165
|
expect($el.val()).to.eq("'''");
|
|
158
166
|
expect($el[0].selectionStart).to.eq(3); // cursor after last '
|
|
167
|
+
expect($el[0].selectionEnd).to.eq(3);
|
|
159
168
|
});
|
|
160
169
|
});
|
|
161
170
|
|
|
162
|
-
it('
|
|
171
|
+
it('autopairs existing custom quotes “ and ”', () => {
|
|
163
172
|
cy.visit('/index.html');
|
|
164
173
|
|
|
165
|
-
//
|
|
166
|
-
cy.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
174
|
+
// Type opening custom quote
|
|
175
|
+
cy.get('textarea').type('“');
|
|
176
|
+
|
|
177
|
+
// Should autopair to “”
|
|
178
|
+
cy.get('textarea').then($el => {
|
|
179
|
+
expect($el.val()).to.eq('“”');
|
|
180
|
+
expect($el[0].selectionStart).to.eq(1); // cursor inside
|
|
181
|
+
expect($el[0].selectionEnd).to.eq(1);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('supports undo and redo after typing a pair', () => {
|
|
186
|
+
cy.visit('/index.html');
|
|
187
|
+
|
|
188
|
+
// Type a pair
|
|
189
|
+
cy.get('textarea').type('()');
|
|
190
|
+
|
|
191
|
+
// Undo
|
|
192
|
+
cy.get('textarea').then($el => {
|
|
193
|
+
$el[0].ownerDocument.execCommand('undo');
|
|
194
|
+
});
|
|
195
|
+
cy.get('textarea').should('have.value', '');
|
|
196
|
+
|
|
197
|
+
// Redo
|
|
198
|
+
cy.get('textarea').then($el => {
|
|
199
|
+
$el[0].ownerDocument.execCommand('redo');
|
|
200
|
+
});
|
|
201
|
+
cy.get('textarea').should('have.value', '()');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('does not autopair a symmetric character behind a word character', () => {
|
|
205
|
+
cy.visit('/index.html');
|
|
206
|
+
|
|
207
|
+
cy.get('textarea').type('hello');
|
|
208
|
+
cy.get('textarea').type("'");
|
|
209
|
+
|
|
210
|
+
cy.get('textarea').then($el => {
|
|
211
|
+
expect($el.val()).to.eq("hello'");
|
|
212
|
+
expect($el[0].selectionStart).to.eq(6);
|
|
213
|
+
expect($el[0].selectionEnd).to.eq(6);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('does autopair a symmetric character behind a non-word character', () => {
|
|
218
|
+
cy.visit('/index.html');
|
|
219
|
+
|
|
220
|
+
cy.get('textarea').type('hello.');
|
|
221
|
+
cy.get('textarea').type("'");
|
|
222
|
+
|
|
223
|
+
cy.get('textarea').then($el => {
|
|
224
|
+
expect($el.val()).to.eq("hello.''");
|
|
225
|
+
expect($el[0].selectionStart).to.eq(7);
|
|
226
|
+
expect($el[0].selectionEnd).to.eq(7);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('does autopair a symmetric character behind a newline', () => {
|
|
231
|
+
cy.visit('/index.html');
|
|
232
|
+
|
|
233
|
+
cy.get('textarea').type('\n');
|
|
234
|
+
cy.get('textarea').type("'");
|
|
235
|
+
|
|
236
|
+
cy.get('textarea').then($el => {
|
|
237
|
+
expect($el.val()).to.eq("\n''");
|
|
238
|
+
expect($el[0].selectionStart).to.eq(2);
|
|
239
|
+
expect($el[0].selectionEnd).to.eq(2);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('does autopair an asymmetric character behind a word character', () => {
|
|
244
|
+
cy.visit('/index.html');
|
|
245
|
+
|
|
246
|
+
cy.get('textarea').type('hello');
|
|
247
|
+
cy.get('textarea').type("(");
|
|
248
|
+
|
|
249
|
+
cy.get('textarea').then($el => {
|
|
250
|
+
expect($el.val()).to.eq("hello()");
|
|
251
|
+
expect($el[0].selectionStart).to.eq(6);
|
|
252
|
+
expect($el[0].selectionEnd).to.eq(6);
|
|
190
253
|
});
|
|
191
254
|
});
|
|
192
|
-
});
|
|
255
|
+
});
|
package/package.json
CHANGED
package/src/autopair.js
CHANGED
|
@@ -5,32 +5,38 @@ export default function autopair(textarea, pairs = {
|
|
|
5
5
|
"'": "'",
|
|
6
6
|
'"': '"'
|
|
7
7
|
}) {
|
|
8
|
+
let openings = Object.keys(pairs);
|
|
9
|
+
let closings = Object.values(pairs);
|
|
10
|
+
|
|
11
|
+
let insertText = text => document.execCommand('insertText', false, text);
|
|
12
|
+
let setCursor = pos => {
|
|
13
|
+
textarea.selectionStart = textarea.selectionEnd = pos;
|
|
14
|
+
};
|
|
15
|
+
|
|
8
16
|
let handler = evt => {
|
|
9
|
-
|
|
17
|
+
let { selectionStart: start, selectionEnd: end, value } = textarea;
|
|
10
18
|
|
|
11
19
|
// Typethrough
|
|
12
|
-
if (start === end) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (isClosing && next === evt.key) {
|
|
17
|
-
evt.preventDefault();
|
|
18
|
-
textarea.selectionStart = textarea.selectionEnd = end + 1;
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
20
|
+
if (start === end && closings.includes(evt.key) && value[end] === evt.key) {
|
|
21
|
+
evt.preventDefault();
|
|
22
|
+
setCursor(end + 1);
|
|
23
|
+
return;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
// Handle backspace inside a direct pair
|
|
24
27
|
if (evt.key === 'Backspace' && start === end && start > 0) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
let left = value[start - 1];
|
|
29
|
+
let right = value[start];
|
|
30
|
+
let opening = openings.find(k => pairs[k] === right);
|
|
31
|
+
|
|
28
32
|
if (left === opening) {
|
|
29
33
|
evt.preventDefault();
|
|
34
|
+
|
|
30
35
|
// Select the pair and delete in one go
|
|
31
36
|
textarea.selectionStart = start - 1;
|
|
32
37
|
textarea.selectionEnd = start + 1;
|
|
33
|
-
|
|
38
|
+
|
|
39
|
+
insertText('');
|
|
34
40
|
|
|
35
41
|
return;
|
|
36
42
|
}
|
|
@@ -38,7 +44,7 @@ export default function autopair(textarea, pairs = {
|
|
|
38
44
|
return; // normal backspace
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
let closing = pairs[evt.key];
|
|
42
48
|
if (!closing) return;
|
|
43
49
|
|
|
44
50
|
// Wrap selection if present
|
|
@@ -46,28 +52,28 @@ export default function autopair(textarea, pairs = {
|
|
|
46
52
|
evt.preventDefault();
|
|
47
53
|
textarea.selectionStart = start;
|
|
48
54
|
textarea.selectionEnd = end;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
insertText(evt.key + value.slice(start, end) + closing);
|
|
56
|
+
setCursor(end + 1);
|
|
57
|
+
|
|
52
58
|
return;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
let nextCharWhitelist = /[\s;})\]]/;
|
|
62
|
+
let nextChar = value[end] || '';
|
|
63
|
+
let prevChar = value[start - 1] || '';
|
|
64
|
+
let insidePair = closing === nextChar;
|
|
65
|
+
let safeNext = nextChar === '' || nextCharWhitelist.test(nextChar);
|
|
66
|
+
let isSymmetric = evt.key === closing;
|
|
61
67
|
|
|
62
68
|
// Autoclose only when allowed
|
|
63
|
-
if (!insidePair && (!safeNext || (isSymmetric && start
|
|
69
|
+
if (!insidePair && (!safeNext || (isSymmetric && (start !== end || prevChar === evt.key || /\w/.test(prevChar))))) {
|
|
64
70
|
return;
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
evt.preventDefault();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
|
|
75
|
+
insertText(evt.key + closing);
|
|
76
|
+
setCursor(start + 1);
|
|
71
77
|
};
|
|
72
78
|
|
|
73
79
|
textarea.addEventListener('keydown', handler);
|