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 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').should('have.value', '(hello)');
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('handles custom pairing correctly', () => {
171
+ it('autopairs existing custom quotes “ and ”', () => {
163
172
  cy.visit('/index.html');
164
173
 
165
- // Inject autopair immediately
166
- cy.document().then(doc => {
167
- const script = doc.createElement('script');
168
- script.type = 'module';
169
- script.textContent = `
170
- import autopair from './autopair.js';
171
- autopair(document.querySelector('textarea'), {
172
- '(': ')',
173
- '[': ']',
174
- '{': '}',
175
- "'": "'",
176
- '"': '"',
177
- '<': '>'
178
- });
179
- `;
180
- doc.body.appendChild(script);
181
- });
182
-
183
- // Type <
184
- cy.get('textarea').type('<');
185
-
186
- // Should autopair to <>
187
- cy.get('textarea').then($el => {
188
- expect($el.val()).to.eq('<>');
189
- expect($el[0].selectionStart).to.eq(1);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autopair",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Automatically closes parentheses and similar characters.",
5
5
  "main": "src/autopair.js",
6
6
  "author": "Dennis Hackethal <engineering@dennishackethal.com>",
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
- const { selectionStart: start, selectionEnd: end, value } = textarea;
17
+ let { selectionStart: start, selectionEnd: end, value } = textarea;
10
18
 
11
19
  // Typethrough
12
- if (start === end) {
13
- const next = value[end];
14
- const isClosing = Object.values(pairs).includes(evt.key);
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
- const left = value[start - 1];
26
- const right = value[start];
27
- const opening = Object.keys(pairs).find(k => pairs[k] === right);
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
- document.execCommand('insertText', false, '');
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
- const closing = pairs[evt.key];
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
- document.execCommand('insertText', false, evt.key + value.slice(start, end) + closing);
50
- textarea.selectionStart = start + 1;
51
- textarea.selectionEnd = end + 1;
55
+ insertText(evt.key + value.slice(start, end) + closing);
56
+ setCursor(end + 1);
57
+
52
58
  return;
53
59
  }
54
60
 
55
- const nextCharWhitelist = /[\s;})\]]/;
56
- const nextChar = value[end] || '';
57
- const prevChar = value[start - 1] || '';
58
- const insidePair = closing === nextChar;
59
- const safeNext = nextChar === '' || nextCharWhitelist.test(nextChar);
60
- const isSymmetric = evt.key === closing;
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 === end && prevChar === evt.key))) {
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
- textarea.selectionStart = textarea.selectionEnd = start;
69
- document.execCommand('insertText', false, evt.key + closing);
70
- textarea.selectionStart = textarea.selectionEnd = start + 1;
74
+
75
+ insertText(evt.key + closing);
76
+ setCursor(start + 1);
71
77
  };
72
78
 
73
79
  textarea.addEventListener('keydown', handler);