@wordpress/dom 3.8.0 → 3.11.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +8 -9
  3. package/build/dom/clean-node-list.js +4 -2
  4. package/build/dom/clean-node-list.js.map +1 -1
  5. package/build/dom/document-has-selection.js +4 -4
  6. package/build/dom/document-has-selection.js.map +1 -1
  7. package/build/dom/document-has-uncollapsed-selection.js +4 -4
  8. package/build/dom/document-has-uncollapsed-selection.js.map +1 -1
  9. package/build/dom/get-rectangle-from-range.js.map +1 -1
  10. package/build/dom/input-field-has-uncollapsed-selection.js +24 -18
  11. package/build/dom/input-field-has-uncollapsed-selection.js.map +1 -1
  12. package/build/dom/is-edge.js.map +1 -1
  13. package/build/dom/is-empty.js.map +1 -1
  14. package/build/dom/is-html-input-element.js +1 -1
  15. package/build/dom/is-html-input-element.js.map +1 -1
  16. package/build/dom/is-number-input.js +14 -4
  17. package/build/dom/is-number-input.js.map +1 -1
  18. package/build/dom/is-text-field.js +1 -1
  19. package/build/dom/is-text-field.js.map +1 -1
  20. package/build-module/dom/clean-node-list.js +4 -1
  21. package/build-module/dom/clean-node-list.js.map +1 -1
  22. package/build-module/dom/document-has-selection.js +4 -4
  23. package/build-module/dom/document-has-selection.js.map +1 -1
  24. package/build-module/dom/document-has-uncollapsed-selection.js +4 -4
  25. package/build-module/dom/document-has-uncollapsed-selection.js.map +1 -1
  26. package/build-module/dom/get-rectangle-from-range.js.map +1 -1
  27. package/build-module/dom/input-field-has-uncollapsed-selection.js +24 -18
  28. package/build-module/dom/input-field-has-uncollapsed-selection.js.map +1 -1
  29. package/build-module/dom/is-edge.js.map +1 -1
  30. package/build-module/dom/is-empty.js.map +1 -1
  31. package/build-module/dom/is-html-input-element.js +1 -1
  32. package/build-module/dom/is-html-input-element.js.map +1 -1
  33. package/build-module/dom/is-number-input.js +13 -4
  34. package/build-module/dom/is-number-input.js.map +1 -1
  35. package/build-module/dom/is-text-field.js +1 -1
  36. package/build-module/dom/is-text-field.js.map +1 -1
  37. package/build-types/dom/clean-node-list.d.ts.map +1 -1
  38. package/build-types/dom/document-has-selection.d.ts +2 -2
  39. package/build-types/dom/document-has-uncollapsed-selection.d.ts +4 -4
  40. package/build-types/dom/input-field-has-uncollapsed-selection.d.ts +9 -5
  41. package/build-types/dom/input-field-has-uncollapsed-selection.d.ts.map +1 -1
  42. package/build-types/dom/is-number-input.d.ts +2 -3
  43. package/build-types/dom/is-number-input.d.ts.map +1 -1
  44. package/build-types/dom/is-text-field.d.ts.map +1 -1
  45. package/package.json +3 -2
  46. package/src/dom/clean-node-list.js +130 -125
  47. package/src/dom/document-has-selection.js +5 -5
  48. package/src/dom/document-has-uncollapsed-selection.js +4 -4
  49. package/src/dom/get-rectangle-from-range.js +3 -3
  50. package/src/dom/input-field-has-uncollapsed-selection.js +28 -22
  51. package/src/dom/is-edge.js +1 -1
  52. package/src/dom/is-empty.js +3 -3
  53. package/src/dom/is-html-input-element.js +1 -1
  54. package/src/dom/is-number-input.js +12 -4
  55. package/src/dom/is-text-field.js +2 -0
  56. package/src/test/dom.js +3 -23
  57. package/tsconfig.json +1 -3
  58. package/tsconfig.tsbuildinfo +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * External dependencies
3
3
  */
4
- import { includes, noop } from 'lodash';
4
+ import { includes } from 'lodash';
5
5
 
6
6
  /**
7
7
  * Internal dependencies
@@ -13,6 +13,8 @@ import { isPhrasingContent } from '../phrasing-content';
13
13
  import insertAfter from './insert-after';
14
14
  import isElement from './is-element';
15
15
 
16
+ const noop = () => {};
17
+
16
18
  /* eslint-disable jsdoc/valid-types */
17
19
  /**
18
20
  * @typedef SchemaItem
@@ -37,150 +39,153 @@ import isElement from './is-element';
37
39
  * @param {boolean} inline Whether to clean for inline mode.
38
40
  */
39
41
  export default function cleanNodeList( nodeList, doc, schema, inline ) {
40
- Array.from( nodeList ).forEach( (
41
- /** @type {Node & { nextElementSibling?: unknown }} */ node
42
- ) => {
43
- const tag = node.nodeName.toLowerCase();
44
-
45
- // It's a valid child, if the tag exists in the schema without an isMatch
46
- // function, or with an isMatch function that matches the node.
47
- if (
48
- schema.hasOwnProperty( tag ) &&
49
- ( ! schema[ tag ].isMatch || schema[ tag ].isMatch?.( node ) )
50
- ) {
51
- if ( isElement( node ) ) {
52
- const {
53
- attributes = [],
54
- classes = [],
55
- children,
56
- require = [],
57
- allowEmpty,
58
- } = schema[ tag ];
59
-
60
- // If the node is empty and it's supposed to have children,
61
- // remove the node.
62
- if ( children && ! allowEmpty && isEmpty( node ) ) {
63
- remove( node );
64
- return;
65
- }
66
-
67
- if ( node.hasAttributes() ) {
68
- // Strip invalid attributes.
69
- Array.from( node.attributes ).forEach( ( { name } ) => {
70
- if (
71
- name !== 'class' &&
72
- ! includes( attributes, name )
73
- ) {
74
- node.removeAttribute( name );
75
- }
76
- } );
77
-
78
- // Strip invalid classes.
79
- // In jsdom-jscore, 'node.classList' can be undefined.
80
- // TODO: Explore patching this in jsdom-jscore.
81
- if ( node.classList && node.classList.length ) {
82
- const mattchers = classes.map( ( item ) => {
83
- if ( typeof item === 'string' ) {
84
- return ( /** @type {string} */ className ) =>
85
- className === item;
86
- } else if ( item instanceof RegExp ) {
87
- return ( /** @type {string} */ className ) =>
88
- item.test( className );
89
- }
42
+ Array.from( nodeList ).forEach(
43
+ ( /** @type {Node & { nextElementSibling?: unknown }} */ node ) => {
44
+ const tag = node.nodeName.toLowerCase();
90
45
 
91
- return noop;
92
- } );
46
+ // It's a valid child, if the tag exists in the schema without an isMatch
47
+ // function, or with an isMatch function that matches the node.
48
+ if (
49
+ schema.hasOwnProperty( tag ) &&
50
+ ( ! schema[ tag ].isMatch || schema[ tag ].isMatch?.( node ) )
51
+ ) {
52
+ if ( isElement( node ) ) {
53
+ const {
54
+ attributes = [],
55
+ classes = [],
56
+ children,
57
+ require = [],
58
+ allowEmpty,
59
+ } = schema[ tag ];
60
+
61
+ // If the node is empty and it's supposed to have children,
62
+ // remove the node.
63
+ if ( children && ! allowEmpty && isEmpty( node ) ) {
64
+ remove( node );
65
+ return;
66
+ }
93
67
 
94
- Array.from( node.classList ).forEach( ( name ) => {
68
+ if ( node.hasAttributes() ) {
69
+ // Strip invalid attributes.
70
+ Array.from( node.attributes ).forEach( ( { name } ) => {
95
71
  if (
96
- ! mattchers.some( ( isMatch ) =>
97
- isMatch( name )
98
- )
72
+ name !== 'class' &&
73
+ ! includes( attributes, name )
99
74
  ) {
100
- node.classList.remove( name );
75
+ node.removeAttribute( name );
101
76
  }
102
77
  } );
103
78
 
104
- if ( ! node.classList.length ) {
105
- node.removeAttribute( 'class' );
79
+ // Strip invalid classes.
80
+ // In jsdom-jscore, 'node.classList' can be undefined.
81
+ // TODO: Explore patching this in jsdom-jscore.
82
+ if ( node.classList && node.classList.length ) {
83
+ const mattchers = classes.map( ( item ) => {
84
+ if ( typeof item === 'string' ) {
85
+ return (
86
+ /** @type {string} */ className
87
+ ) => className === item;
88
+ } else if ( item instanceof RegExp ) {
89
+ return (
90
+ /** @type {string} */ className
91
+ ) => item.test( className );
92
+ }
93
+
94
+ return noop;
95
+ } );
96
+
97
+ Array.from( node.classList ).forEach( ( name ) => {
98
+ if (
99
+ ! mattchers.some( ( isMatch ) =>
100
+ isMatch( name )
101
+ )
102
+ ) {
103
+ node.classList.remove( name );
104
+ }
105
+ } );
106
+
107
+ if ( ! node.classList.length ) {
108
+ node.removeAttribute( 'class' );
109
+ }
106
110
  }
107
111
  }
108
- }
109
-
110
- if ( node.hasChildNodes() ) {
111
- // Do not filter any content.
112
- if ( children === '*' ) {
113
- return;
114
- }
115
112
 
116
- // Continue if the node is supposed to have children.
117
- if ( children ) {
118
- // If a parent requires certain children, but it does
119
- // not have them, drop the parent and continue.
120
- if (
121
- require.length &&
122
- ! node.querySelector( require.join( ',' ) )
123
- ) {
124
- cleanNodeList(
125
- node.childNodes,
126
- doc,
127
- schema,
128
- inline
129
- );
130
- unwrap( node );
131
- // If the node is at the top, phrasing content, and
132
- // contains children that are block content, unwrap
133
- // the node because it is invalid.
134
- } else if (
135
- node.parentNode &&
136
- node.parentNode.nodeName === 'BODY' &&
137
- isPhrasingContent( node )
138
- ) {
139
- cleanNodeList(
140
- node.childNodes,
141
- doc,
142
- schema,
143
- inline
144
- );
113
+ if ( node.hasChildNodes() ) {
114
+ // Do not filter any content.
115
+ if ( children === '*' ) {
116
+ return;
117
+ }
145
118
 
119
+ // Continue if the node is supposed to have children.
120
+ if ( children ) {
121
+ // If a parent requires certain children, but it does
122
+ // not have them, drop the parent and continue.
146
123
  if (
147
- Array.from( node.childNodes ).some(
148
- ( child ) => ! isPhrasingContent( child )
149
- )
124
+ require.length &&
125
+ ! node.querySelector( require.join( ',' ) )
150
126
  ) {
127
+ cleanNodeList(
128
+ node.childNodes,
129
+ doc,
130
+ schema,
131
+ inline
132
+ );
151
133
  unwrap( node );
134
+ // If the node is at the top, phrasing content, and
135
+ // contains children that are block content, unwrap
136
+ // the node because it is invalid.
137
+ } else if (
138
+ node.parentNode &&
139
+ node.parentNode.nodeName === 'BODY' &&
140
+ isPhrasingContent( node )
141
+ ) {
142
+ cleanNodeList(
143
+ node.childNodes,
144
+ doc,
145
+ schema,
146
+ inline
147
+ );
148
+
149
+ if (
150
+ Array.from( node.childNodes ).some(
151
+ ( child ) =>
152
+ ! isPhrasingContent( child )
153
+ )
154
+ ) {
155
+ unwrap( node );
156
+ }
157
+ } else {
158
+ cleanNodeList(
159
+ node.childNodes,
160
+ doc,
161
+ children,
162
+ inline
163
+ );
152
164
  }
165
+ // Remove children if the node is not supposed to have any.
153
166
  } else {
154
- cleanNodeList(
155
- node.childNodes,
156
- doc,
157
- children,
158
- inline
159
- );
160
- }
161
- // Remove children if the node is not supposed to have any.
162
- } else {
163
- while ( node.firstChild ) {
164
- remove( node.firstChild );
167
+ while ( node.firstChild ) {
168
+ remove( node.firstChild );
169
+ }
165
170
  }
166
171
  }
167
172
  }
168
- }
169
- // Invalid child. Continue with schema at the same place and unwrap.
170
- } else {
171
- cleanNodeList( node.childNodes, doc, schema, inline );
173
+ // Invalid child. Continue with schema at the same place and unwrap.
174
+ } else {
175
+ cleanNodeList( node.childNodes, doc, schema, inline );
176
+
177
+ // For inline mode, insert a line break when unwrapping nodes that
178
+ // are not phrasing content.
179
+ if (
180
+ inline &&
181
+ ! isPhrasingContent( node ) &&
182
+ node.nextElementSibling
183
+ ) {
184
+ insertAfter( doc.createElement( 'br' ), node );
185
+ }
172
186
 
173
- // For inline mode, insert a line break when unwrapping nodes that
174
- // are not phrasing content.
175
- if (
176
- inline &&
177
- ! isPhrasingContent( node ) &&
178
- node.nextElementSibling
179
- ) {
180
- insertAfter( doc.createElement( 'br' ), node );
187
+ unwrap( node );
181
188
  }
182
-
183
- unwrap( node );
184
189
  }
185
- } );
190
+ );
186
191
  }
@@ -2,12 +2,12 @@
2
2
  * Internal dependencies
3
3
  */
4
4
  import isTextField from './is-text-field';
5
- import isNumberInput from './is-number-input';
5
+ import isHTMLInputElement from './is-html-input-element';
6
6
  import documentHasTextSelection from './document-has-text-selection';
7
7
 
8
8
  /**
9
- * Check whether the current document has a selection. This checks for both
10
- * focus in an input field and general text selection.
9
+ * Check whether the current document has a selection. This includes focus in
10
+ * input fields, textareas, and general rich-text selection.
11
11
  *
12
12
  * @param {Document} doc The document to check.
13
13
  *
@@ -16,8 +16,8 @@ import documentHasTextSelection from './document-has-text-selection';
16
16
  export default function documentHasSelection( doc ) {
17
17
  return (
18
18
  !! doc.activeElement &&
19
- ( isTextField( doc.activeElement ) ||
20
- isNumberInput( doc.activeElement ) ||
19
+ ( isHTMLInputElement( doc.activeElement ) ||
20
+ isTextField( doc.activeElement ) ||
21
21
  documentHasTextSelection( doc ) )
22
22
  );
23
23
  }
@@ -5,13 +5,13 @@ import documentHasTextSelection from './document-has-text-selection';
5
5
  import inputFieldHasUncollapsedSelection from './input-field-has-uncollapsed-selection';
6
6
 
7
7
  /**
8
- * Check whether the current document has any sort of selection. This includes
9
- * ranges of text across elements and any selection inside `<input>` and
10
- * `<textarea>` elements.
8
+ * Check whether the current document has any sort of (uncollapsed) selection.
9
+ * This includes ranges of text across elements and any selection inside
10
+ * textual `<input>` and `<textarea>` elements.
11
11
  *
12
12
  * @param {Document} doc The document to check.
13
13
  *
14
- * @return {boolean} Whether there is any sort of "selection" in the document.
14
+ * @return {boolean} Whether there is any recognizable text selection in the document.
15
15
  */
16
16
  export default function documentHasUncollapsedSelection( doc ) {
17
17
  return (
@@ -63,9 +63,9 @@ export default function getRectangleFromRange( range ) {
63
63
  if ( startContainer.nodeName === 'BR' ) {
64
64
  const { parentNode } = startContainer;
65
65
  assertIsDefined( parentNode, 'parentNode' );
66
- const index = /** @type {Node[]} */ ( Array.from(
67
- parentNode.childNodes
68
- ) ).indexOf( startContainer );
66
+ const index = /** @type {Node[]} */ (
67
+ Array.from( parentNode.childNodes )
68
+ ).indexOf( startContainer );
69
69
 
70
70
  assertIsDefined( ownerDocument, 'ownerDocument' );
71
71
  range = ownerDocument.createRange();
@@ -2,41 +2,47 @@
2
2
  * Internal dependencies
3
3
  */
4
4
  import isTextField from './is-text-field';
5
- import isNumberInput from './is-number-input';
5
+ import isHTMLInputElement from './is-html-input-element';
6
6
 
7
7
  /**
8
- * Check whether the given element, assumed an input field or textarea,
9
- * contains a (uncollapsed) selection of text.
8
+ * Check whether the given input field or textarea contains a (uncollapsed)
9
+ * selection of text.
10
10
  *
11
- * Note: this is perhaps an abuse of the term "selection", since these elements
12
- * manage selection differently and aren't covered by Selection#collapsed.
11
+ * CAVEAT: Only specific text-based HTML inputs support the selection APIs
12
+ * needed to determine whether they have a collapsed or uncollapsed selection.
13
+ * This function defaults to returning `true` when the selection cannot be
14
+ * inspected, such as with `<input type="time">`. The rationale is that this
15
+ * should cause the block editor to defer to the browser's native selection
16
+ * handling (e.g. copying and pasting), thereby reducing friction for the user.
13
17
  *
14
- * See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#Related_objects.
18
+ * See: https://html.spec.whatwg.org/multipage/input.html#do-not-apply
15
19
  *
16
20
  * @param {Element} element The HTML element.
17
21
  *
18
22
  * @return {boolean} Whether the input/textareaa element has some "selection".
19
23
  */
20
24
  export default function inputFieldHasUncollapsedSelection( element ) {
21
- if ( ! isTextField( element ) && ! isNumberInput( element ) ) {
25
+ if ( ! isHTMLInputElement( element ) && ! isTextField( element ) ) {
22
26
  return false;
23
27
  }
24
- try {
25
- const {
26
- selectionStart,
27
- selectionEnd,
28
- } = /** @type {HTMLInputElement | HTMLTextAreaElement} */ ( element );
29
28
 
30
- return selectionStart !== null && selectionStart !== selectionEnd;
29
+ // Safari throws a type error when trying to get `selectionStart` and
30
+ // `selectionEnd` on non-text <input> elements, so a try/catch construct is
31
+ // necessary.
32
+ try {
33
+ const { selectionStart, selectionEnd } =
34
+ /** @type {HTMLInputElement | HTMLTextAreaElement} */ ( element );
35
+ return (
36
+ // `null` means the input type doesn't implement selection, thus we
37
+ // cannot determine whether the selection is collapsed, so we
38
+ // default to true.
39
+ selectionStart === null ||
40
+ // when not null, compare the two points
41
+ selectionStart !== selectionEnd
42
+ );
31
43
  } catch ( error ) {
32
- // Safari throws an exception when trying to get `selectionStart`
33
- // on non-text <input> elements (which, understandably, don't
34
- // have the text selection API). We catch this via a try/catch
35
- // block, as opposed to a more explicit check of the element's
36
- // input types, because of Safari's non-standard behavior. This
37
- // also means we don't have to worry about the list of input
38
- // types that support `selectionStart` changing as the HTML spec
39
- // evolves over time.
40
- return false;
44
+ // This is Safari's way of saying that the input type doesn't implement
45
+ // selection, so we default to true.
46
+ return true;
41
47
  }
42
48
  }
@@ -36,7 +36,7 @@ export default function isEdge( container, isReverse, onlyVertical = false ) {
36
36
  return container.value.length === container.selectionStart;
37
37
  }
38
38
 
39
- if ( ! (/** @type {HTMLElement} */ ( container ).isContentEditable) ) {
39
+ if ( ! ( /** @type {HTMLElement} */ ( container ).isContentEditable ) ) {
40
40
  return true;
41
41
  }
42
42
 
@@ -19,9 +19,9 @@ export default function isEmpty( element ) {
19
19
  return true;
20
20
  }
21
21
 
22
- return /** @type {Element[]} */ ( Array.from(
23
- element.childNodes
24
- ) ).every( isEmpty );
22
+ return /** @type {Element[]} */ (
23
+ Array.from( element.childNodes )
24
+ ).every( isEmpty );
25
25
  default:
26
26
  return true;
27
27
  }
@@ -5,5 +5,5 @@
5
5
  */
6
6
  export default function isHTMLInputElement( node ) {
7
7
  /* eslint-enable jsdoc/valid-types */
8
- return !! node && node.nodeName === 'INPUT';
8
+ return node?.nodeName === 'INPUT';
9
9
  }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import deprecated from '@wordpress/deprecated';
5
+
1
6
  /**
2
7
  * Internal dependencies
3
8
  */
@@ -5,18 +10,21 @@ import isHTMLInputElement from './is-html-input-element';
5
10
 
6
11
  /* eslint-disable jsdoc/valid-types */
7
12
  /**
8
- * Check whether the given element is an input field of type number
9
- * and has a valueAsNumber
13
+ * Check whether the given element is an input field of type number.
10
14
  *
11
15
  * @param {Node} node The HTML node.
12
16
  *
13
- * @return {node is HTMLInputElement} True if the node is input and holds a number.
17
+ * @return {node is HTMLInputElement} True if the node is number input.
14
18
  */
15
19
  export default function isNumberInput( node ) {
20
+ deprecated( 'wp.dom.isNumberInput', {
21
+ since: '6.1',
22
+ version: '6.5',
23
+ } );
16
24
  /* eslint-enable jsdoc/valid-types */
17
25
  return (
18
26
  isHTMLInputElement( node ) &&
19
27
  node.type === 'number' &&
20
- !! node.valueAsNumber
28
+ ! isNaN( node.valueAsNumber )
21
29
  );
22
30
  }
@@ -26,6 +26,8 @@ export default function isTextField( node ) {
26
26
  'reset',
27
27
  'submit',
28
28
  'number',
29
+ 'email',
30
+ 'time',
29
31
  ];
30
32
  return (
31
33
  ( isHTMLInputElement( node ) &&
package/src/test/dom.js CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  isHorizontalEdge,
6
6
  placeCaretAtHorizontalEdge,
7
7
  isTextField,
8
- isNumberInput,
9
8
  removeInvalidHTML,
10
9
  isEmpty,
11
10
  } from '../dom';
@@ -125,6 +124,8 @@ describe( 'DOM', () => {
125
124
  'range',
126
125
  'reset',
127
126
  'submit',
127
+ 'email',
128
+ 'time',
128
129
  ];
129
130
 
130
131
  /**
@@ -132,13 +133,7 @@ describe( 'DOM', () => {
132
133
  *
133
134
  * @type {string[]}
134
135
  */
135
- const TEXT_INPUT_TYPES = [
136
- 'text',
137
- 'password',
138
- 'search',
139
- 'url',
140
- 'email',
141
- ];
136
+ const TEXT_INPUT_TYPES = [ 'text', 'password', 'search', 'url' ];
142
137
 
143
138
  it( 'should return false for non-text input elements', () => {
144
139
  NON_TEXT_INPUT_TYPES.forEach( ( type ) => {
@@ -164,21 +159,6 @@ describe( 'DOM', () => {
164
159
  );
165
160
  } );
166
161
 
167
- it( 'should return false for empty input element of type number', () => {
168
- const input = document.createElement( 'input' );
169
- input.type = 'number';
170
-
171
- expect( isNumberInput( input ) ).toBe( false );
172
- } );
173
-
174
- it( 'should return true for an input element of type number', () => {
175
- const input = document.createElement( 'input' );
176
- input.type = 'number';
177
- input.valueAsNumber = 23;
178
-
179
- expect( isNumberInput( input ) ).toBe( true );
180
- } );
181
-
182
162
  it( 'should return true for a contenteditable element', () => {
183
163
  const div = document.createElement( 'div' );
184
164
 
package/tsconfig.json CHANGED
@@ -5,7 +5,5 @@
5
5
  "declarationDir": "build-types",
6
6
  "types": [ "gutenberg-env" ]
7
7
  },
8
- "include": [
9
- "src/**/*"
10
- ]
8
+ "include": [ "src/**/*" ]
11
9
  }