fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

28
node_modules/@wordpress/dom/src/data-transfer.js generated vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* Gets all files from a DataTransfer object.
*
* @param {DataTransfer} dataTransfer DataTransfer object to inspect.
*
* @return {File[]} An array containing all files.
*/
export function getFilesFromDataTransfer( dataTransfer ) {
const files = Array.from( dataTransfer.files );
Array.from( dataTransfer.items ).forEach( ( item ) => {
const file = item.getAsFile();
if (
file &&
! files.find(
( { name, type, size } ) =>
name === file.name &&
type === file.type &&
size === file.size
)
) {
files.push( file );
}
} );
return files;
}

View File

@@ -0,0 +1,41 @@
/**
* Polyfill.
* Get a collapsed range for a given point.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint
*
* @param {DocumentMaybeWithCaretPositionFromPoint} doc The document of the range.
* @param {number} x Horizontal position within the current viewport.
* @param {number} y Vertical position within the current viewport.
*
* @return {Range | null} The best range for the given point.
*/
export default function caretRangeFromPoint( doc, x, y ) {
if ( doc.caretRangeFromPoint ) {
return doc.caretRangeFromPoint( x, y );
}
if ( ! doc.caretPositionFromPoint ) {
return null;
}
const point = doc.caretPositionFromPoint( x, y );
// If x or y are negative, outside viewport, or there is no text entry node.
// https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint
if ( ! point ) {
return null;
}
const range = doc.createRange();
range.setStart( point.offsetNode, point.offset );
range.collapse( true );
return range;
}
/**
* @typedef {{caretPositionFromPoint?: (x: number, y: number)=> CaretPosition | null} & Document } DocumentMaybeWithCaretPositionFromPoint
* @typedef {{ readonly offset: number; readonly offsetNode: Node; getClientRect(): DOMRect | null; }} CaretPosition
*/

186
node_modules/@wordpress/dom/src/dom/clean-node-list.js generated vendored Normal file
View File

@@ -0,0 +1,186 @@
/**
* Internal dependencies
*/
import isEmpty from './is-empty';
import remove from './remove';
import unwrap from './unwrap';
import { isPhrasingContent } from '../phrasing-content';
import insertAfter from './insert-after';
import isElement from './is-element';
const noop = () => {};
/* eslint-disable jsdoc/valid-types */
/**
* @typedef SchemaItem
* @property {string[]} [attributes] Attributes.
* @property {(string | RegExp)[]} [classes] Classnames or RegExp to test against.
* @property {'*' | { [tag: string]: SchemaItem }} [children] Child schemas.
* @property {string[]} [require] Selectors to test required children against. Leave empty or undefined if there are no requirements.
* @property {boolean} allowEmpty Whether to allow nodes without children.
* @property {(node: Node) => boolean} [isMatch] Function to test whether a node is a match. If left undefined any node will be assumed to match.
*/
/** @typedef {{ [tag: string]: SchemaItem }} Schema */
/* eslint-enable jsdoc/valid-types */
/**
* Given a schema, unwraps or removes nodes, attributes and classes on a node
* list.
*
* @param {NodeList} nodeList The nodeList to filter.
* @param {Document} doc The document of the nodeList.
* @param {Schema} schema An array of functions that can mutate with the provided node.
* @param {boolean} inline Whether to clean for inline mode.
*/
export default function cleanNodeList( nodeList, doc, schema, inline ) {
Array.from( nodeList ).forEach(
( /** @type {Node & { nextElementSibling?: unknown }} */ node ) => {
const tag = node.nodeName.toLowerCase();
// It's a valid child, if the tag exists in the schema without an isMatch
// function, or with an isMatch function that matches the node.
if (
schema.hasOwnProperty( tag ) &&
( ! schema[ tag ].isMatch || schema[ tag ].isMatch?.( node ) )
) {
if ( isElement( node ) ) {
const {
attributes = [],
classes = [],
children,
require = [],
allowEmpty,
} = schema[ tag ];
// If the node is empty and it's supposed to have children,
// remove the node.
if ( children && ! allowEmpty && isEmpty( node ) ) {
remove( node );
return;
}
if ( node.hasAttributes() ) {
// Strip invalid attributes.
Array.from( node.attributes ).forEach( ( { name } ) => {
if (
name !== 'class' &&
! attributes.includes( name )
) {
node.removeAttribute( name );
}
} );
// Strip invalid classes.
// In jsdom-jscore, 'node.classList' can be undefined.
// TODO: Explore patching this in jsdom-jscore.
if ( node.classList && node.classList.length ) {
const mattchers = classes.map( ( item ) => {
if ( typeof item === 'string' ) {
return (
/** @type {string} */ className
) => className === item;
} else if ( item instanceof RegExp ) {
return (
/** @type {string} */ className
) => item.test( className );
}
return noop;
} );
Array.from( node.classList ).forEach( ( name ) => {
if (
! mattchers.some( ( isMatch ) =>
isMatch( name )
)
) {
node.classList.remove( name );
}
} );
if ( ! node.classList.length ) {
node.removeAttribute( 'class' );
}
}
}
if ( node.hasChildNodes() ) {
// Do not filter any content.
if ( children === '*' ) {
return;
}
// Continue if the node is supposed to have children.
if ( children ) {
// If a parent requires certain children, but it does
// not have them, drop the parent and continue.
if (
require.length &&
! node.querySelector( require.join( ',' ) )
) {
cleanNodeList(
node.childNodes,
doc,
schema,
inline
);
unwrap( node );
// If the node is at the top, phrasing content, and
// contains children that are block content, unwrap
// the node because it is invalid.
} else if (
node.parentNode &&
node.parentNode.nodeName === 'BODY' &&
isPhrasingContent( node )
) {
cleanNodeList(
node.childNodes,
doc,
schema,
inline
);
if (
Array.from( node.childNodes ).some(
( child ) =>
! isPhrasingContent( child )
)
) {
unwrap( node );
}
} else {
cleanNodeList(
node.childNodes,
doc,
children,
inline
);
}
// Remove children if the node is not supposed to have any.
} else {
while ( node.firstChild ) {
remove( node.firstChild );
}
}
}
}
// Invalid child. Continue with schema at the same place and unwrap.
} else {
cleanNodeList( node.childNodes, doc, schema, inline );
// For inline mode, insert a line break when unwrapping nodes that
// are not phrasing content.
if (
inline &&
! isPhrasingContent( node ) &&
node.nextElementSibling
) {
insertAfter( doc.createElement( 'br' ), node );
}
unwrap( node );
}
}
);
}

View File

@@ -0,0 +1,24 @@
/**
* Internal dependencies
*/
import getRectangleFromRange from './get-rectangle-from-range';
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Get the rectangle for the selection in a container.
*
* @param {Window} win The window of the selection.
*
* @return {DOMRect | null} The rectangle.
*/
export default function computeCaretRect( win ) {
const selection = win.getSelection();
assertIsDefined( selection, 'selection' );
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
if ( ! range ) {
return null;
}
return getRectangleFromRange( range );
}

View File

@@ -0,0 +1,23 @@
/**
* Internal dependencies
*/
import isTextField from './is-text-field';
import isHTMLInputElement from './is-html-input-element';
import documentHasTextSelection from './document-has-text-selection';
/**
* Check whether the current document has a selection. This includes focus in
* input fields, textareas, and general rich-text selection.
*
* @param {Document} doc The document to check.
*
* @return {boolean} True if there is selection, false if not.
*/
export default function documentHasSelection( doc ) {
return (
!! doc.activeElement &&
( isHTMLInputElement( doc.activeElement ) ||
isTextField( doc.activeElement ) ||
documentHasTextSelection( doc ) )
);
}

View File

@@ -0,0 +1,23 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Check whether the current document has selected text. This applies to ranges
* of text in the document, and not selection inside `<input>` and `<textarea>`
* elements.
*
* See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#Related_objects.
*
* @param {Document} doc The document to check.
*
* @return {boolean} True if there is selection, false if not.
*/
export default function documentHasTextSelection( doc ) {
assertIsDefined( doc.defaultView, 'doc.defaultView' );
const selection = doc.defaultView.getSelection();
assertIsDefined( selection, 'selection' );
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
return !! range && ! range.collapsed;
}

View File

@@ -0,0 +1,22 @@
/**
* Internal dependencies
*/
import documentHasTextSelection from './document-has-text-selection';
import inputFieldHasUncollapsedSelection from './input-field-has-uncollapsed-selection';
/**
* Check whether the current document has any sort of (uncollapsed) selection.
* This includes ranges of text across elements and any selection inside
* textual `<input>` and `<textarea>` elements.
*
* @param {Document} doc The document to check.
*
* @return {boolean} Whether there is any recognizable text selection in the document.
*/
export default function documentHasUncollapsedSelection( doc ) {
return (
documentHasTextSelection( doc ) ||
( !! doc.activeElement &&
inputFieldHasUncollapsedSelection( doc.activeElement ) )
);
}

View File

@@ -0,0 +1,18 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/* eslint-disable jsdoc/valid-types */
/**
* @param {Element} element
* @return {ReturnType<Window['getComputedStyle']>} The computed style for the element.
*/
export default function getComputedStyle( element ) {
/* eslint-enable jsdoc/valid-types */
assertIsDefined(
element.ownerDocument.defaultView,
'element.ownerDocument.defaultView'
);
return element.ownerDocument.defaultView.getComputedStyle( element );
}

View File

@@ -0,0 +1,43 @@
/**
* Internal dependencies
*/
import getComputedStyle from './get-computed-style';
/**
* Returns the closest positioned element, or null under any of the conditions
* of the offsetParent specification. Unlike offsetParent, this function is not
* limited to HTMLElement and accepts any Node (e.g. Node.TEXT_NODE).
*
* @see https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent
*
* @param {Node} node Node from which to find offset parent.
*
* @return {Node | null} Offset parent.
*/
export default function getOffsetParent( node ) {
// Cannot retrieve computed style or offset parent only anything other than
// an element node, so find the closest element node.
let closestElement;
while ( ( closestElement = /** @type {Node} */ ( node.parentNode ) ) ) {
if ( closestElement.nodeType === closestElement.ELEMENT_NODE ) {
break;
}
}
if ( ! closestElement ) {
return null;
}
// If the closest element is already positioned, return it, as offsetParent
// does not otherwise consider the node itself.
if (
getComputedStyle( /** @type {Element} */ ( closestElement ) )
.position !== 'static'
) {
return closestElement;
}
// offsetParent is undocumented/draft.
return /** @type {Node & { offsetParent: Node }} */ ( closestElement )
.offsetParent;
}

View File

@@ -0,0 +1,19 @@
/**
* Gets the height of the range without ignoring zero width rectangles, which
* some browsers ignore when creating a union.
*
* @param {Range} range The range to check.
* @return {number | undefined} Height of the range or undefined if the range has no client rectangles.
*/
export default function getRangeHeight( range ) {
const rects = Array.from( range.getClientRects() );
if ( ! rects.length ) {
return;
}
const highestTop = Math.min( ...rects.map( ( { top } ) => top ) );
const lowestBottom = Math.max( ...rects.map( ( { bottom } ) => bottom ) );
return lowestBottom - highestTop;
}

View File

@@ -0,0 +1,112 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Get the rectangle of a given Range. Returns `null` if no suitable rectangle
* can be found.
*
* @param {Range} range The range.
*
* @return {DOMRect?} The rectangle.
*/
export default function getRectangleFromRange( range ) {
// For uncollapsed ranges, get the rectangle that bounds the contents of the
// range; this a rectangle enclosing the union of the bounding rectangles
// for all the elements in the range.
if ( ! range.collapsed ) {
const rects = Array.from( range.getClientRects() );
// If there's just a single rect, return it.
if ( rects.length === 1 ) {
return rects[ 0 ];
}
// Ignore tiny selection at the edge of a range.
const filteredRects = rects.filter( ( { width } ) => width > 1 );
// If it's full of tiny selections, return browser default.
if ( filteredRects.length === 0 ) {
return range.getBoundingClientRect();
}
if ( filteredRects.length === 1 ) {
return filteredRects[ 0 ];
}
let {
top: furthestTop,
bottom: furthestBottom,
left: furthestLeft,
right: furthestRight,
} = filteredRects[ 0 ];
for ( const { top, bottom, left, right } of filteredRects ) {
if ( top < furthestTop ) {
furthestTop = top;
}
if ( bottom > furthestBottom ) {
furthestBottom = bottom;
}
if ( left < furthestLeft ) {
furthestLeft = left;
}
if ( right > furthestRight ) {
furthestRight = right;
}
}
return new window.DOMRect(
furthestLeft,
furthestTop,
furthestRight - furthestLeft,
furthestBottom - furthestTop
);
}
const { startContainer } = range;
const { ownerDocument } = startContainer;
// Correct invalid "BR" ranges. The cannot contain any children.
if ( startContainer.nodeName === 'BR' ) {
const { parentNode } = startContainer;
assertIsDefined( parentNode, 'parentNode' );
const index = /** @type {Node[]} */ (
Array.from( parentNode.childNodes )
).indexOf( startContainer );
assertIsDefined( ownerDocument, 'ownerDocument' );
range = ownerDocument.createRange();
range.setStart( parentNode, index );
range.setEnd( parentNode, index );
}
const rects = range.getClientRects();
// If we have multiple rectangles for a collapsed range, there's no way to
// know which it is, so don't return anything.
if ( rects.length > 1 ) {
return null;
}
let rect = rects[ 0 ];
// If the collapsed range starts (and therefore ends) at an element node,
// `getClientRects` can be empty in some browsers. This can be resolved
// by adding a temporary text node with zero-width space to the range.
//
// See: https://stackoverflow.com/a/6847328/995445
if ( ! rect || rect.height === 0 ) {
assertIsDefined( ownerDocument, 'ownerDocument' );
const padNode = ownerDocument.createTextNode( '\u200b' );
// Do not modify the live range.
range = range.cloneRange();
range.insertNode( padNode );
rect = range.getClientRects()[ 0 ];
assertIsDefined( padNode.parentNode, 'padNode.parentNode' );
padNode.parentNode.removeChild( padNode );
}
return rect;
}

View File

@@ -0,0 +1,53 @@
/**
* Internal dependencies
*/
import getComputedStyle from './get-computed-style';
/**
* Given a DOM node, finds the closest scrollable container node or the node
* itself, if scrollable.
*
* @param {Element | null} node Node from which to start.
* @param {?string} direction Direction of scrollable container to search for ('vertical', 'horizontal', 'all').
* Defaults to 'vertical'.
* @return {Element | undefined} Scrollable container node, if found.
*/
export default function getScrollContainer( node, direction = 'vertical' ) {
if ( ! node ) {
return undefined;
}
if ( direction === 'vertical' || direction === 'all' ) {
// Scrollable if scrollable height exceeds displayed...
if ( node.scrollHeight > node.clientHeight ) {
// ...except when overflow is defined to be hidden or visible
const { overflowY } = getComputedStyle( node );
if ( /(auto|scroll)/.test( overflowY ) ) {
return node;
}
}
}
if ( direction === 'horizontal' || direction === 'all' ) {
// Scrollable if scrollable width exceeds displayed...
if ( node.scrollWidth > node.clientWidth ) {
// ...except when overflow is defined to be hidden or visible
const { overflowX } = getComputedStyle( node );
if ( /(auto|scroll)/.test( overflowX ) ) {
return node;
}
}
}
if ( node.ownerDocument === node.parentNode ) {
return node;
}
// Continue traversing.
return getScrollContainer(
/** @type {Element} */ ( node.parentNode ),
direction
);
}

View File

@@ -0,0 +1,38 @@
/**
* Internal dependencies
*/
import caretRangeFromPoint from './caret-range-from-point';
import getComputedStyle from './get-computed-style';
/**
* Get a collapsed range for a given point.
* Gives the container a temporary high z-index (above any UI).
* This is preferred over getting the UI nodes and set styles there.
*
* @param {Document} doc The document of the range.
* @param {number} x Horizontal position within the current viewport.
* @param {number} y Vertical position within the current viewport.
* @param {HTMLElement} container Container in which the range is expected to be found.
*
* @return {?Range} The best range for the given point.
*/
export default function hiddenCaretRangeFromPoint( doc, x, y, container ) {
const originalZIndex = container.style.zIndex;
const originalPosition = container.style.position;
const { position = 'static' } = getComputedStyle( container );
// A z-index only works if the element position is not static.
if ( position === 'static' ) {
container.style.position = 'relative';
}
container.style.zIndex = '10000';
const range = caretRangeFromPoint( doc, x, y );
container.style.zIndex = originalZIndex;
container.style.position = originalPosition;
return range;
}

26
node_modules/@wordpress/dom/src/dom/index.js generated vendored Normal file
View File

@@ -0,0 +1,26 @@
export { default as computeCaretRect } from './compute-caret-rect';
export { default as documentHasTextSelection } from './document-has-text-selection';
export { default as documentHasUncollapsedSelection } from './document-has-uncollapsed-selection';
export { default as documentHasSelection } from './document-has-selection';
export { default as getRectangleFromRange } from './get-rectangle-from-range';
export { default as getScrollContainer } from './get-scroll-container';
export { default as getOffsetParent } from './get-offset-parent';
export { default as isEntirelySelected } from './is-entirely-selected';
export { default as isFormElement } from './is-form-element';
export { default as isHorizontalEdge } from './is-horizontal-edge';
export { default as isNumberInput } from './is-number-input';
export { default as isTextField } from './is-text-field';
export { default as isVerticalEdge } from './is-vertical-edge';
export { default as placeCaretAtHorizontalEdge } from './place-caret-at-horizontal-edge';
export { default as placeCaretAtVerticalEdge } from './place-caret-at-vertical-edge';
export { default as replace } from './replace';
export { default as remove } from './remove';
export { default as insertAfter } from './insert-after';
export { default as unwrap } from './unwrap';
export { default as replaceTag } from './replace-tag';
export { default as wrap } from './wrap';
export { default as __unstableStripHTML } from './strip-html';
export { default as isEmpty } from './is-empty';
export { default as removeInvalidHTML } from './remove-invalid-html';
export { default as isRTL } from './is-rtl';
export { default as safeHTML } from './safe-html';

View File

@@ -0,0 +1,48 @@
/**
* Internal dependencies
*/
import isTextField from './is-text-field';
import isHTMLInputElement from './is-html-input-element';
/**
* Check whether the given input field or textarea contains a (uncollapsed)
* selection of text.
*
* CAVEAT: Only specific text-based HTML inputs support the selection APIs
* needed to determine whether they have a collapsed or uncollapsed selection.
* This function defaults to returning `true` when the selection cannot be
* inspected, such as with `<input type="time">`. The rationale is that this
* should cause the block editor to defer to the browser's native selection
* handling (e.g. copying and pasting), thereby reducing friction for the user.
*
* See: https://html.spec.whatwg.org/multipage/input.html#do-not-apply
*
* @param {Element} element The HTML element.
*
* @return {boolean} Whether the input/textareaa element has some "selection".
*/
export default function inputFieldHasUncollapsedSelection( element ) {
if ( ! isHTMLInputElement( element ) && ! isTextField( element ) ) {
return false;
}
// Safari throws a type error when trying to get `selectionStart` and
// `selectionEnd` on non-text <input> elements, so a try/catch construct is
// necessary.
try {
const { selectionStart, selectionEnd } =
/** @type {HTMLInputElement | HTMLTextAreaElement} */ ( element );
return (
// `null` means the input type doesn't implement selection, thus we
// cannot determine whether the selection is collapsed, so we
// default to true.
selectionStart === null ||
// when not null, compare the two points
selectionStart !== selectionEnd
);
} catch ( error ) {
// This is Safari's way of saying that the input type doesn't implement
// selection, so we default to true.
return true;
}
}

17
node_modules/@wordpress/dom/src/dom/insert-after.js generated vendored Normal file
View File

@@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Given two DOM nodes, inserts the former in the DOM as the next sibling of
* the latter.
*
* @param {Node} newNode Node to be inserted.
* @param {Node} referenceNode Node after which to perform the insertion.
* @return {void}
*/
export default function insertAfter( newNode, referenceNode ) {
assertIsDefined( referenceNode.parentNode, 'referenceNode.parentNode' );
referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling );
}

127
node_modules/@wordpress/dom/src/dom/is-edge.js generated vendored Normal file
View File

@@ -0,0 +1,127 @@
/**
* Internal dependencies
*/
import isRTL from './is-rtl';
import getRangeHeight from './get-range-height';
import getRectangleFromRange from './get-rectangle-from-range';
import isSelectionForward from './is-selection-forward';
import hiddenCaretRangeFromPoint from './hidden-caret-range-from-point';
import { assertIsDefined } from '../utils/assert-is-defined';
import isInputOrTextArea from './is-input-or-text-area';
import { scrollIfNoRange } from './scroll-if-no-range';
/**
* Check whether the selection is at the edge of the container. Checks for
* horizontal position by default. Set `onlyVertical` to true to check only
* vertically.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse Set to true to check left, false to check right.
* @param {boolean} [onlyVertical=false] Set to true to check only vertical position.
*
* @return {boolean} True if at the edge, false if not.
*/
export default function isEdge( container, isReverse, onlyVertical = false ) {
if (
isInputOrTextArea( container ) &&
typeof container.selectionStart === 'number'
) {
if ( container.selectionStart !== container.selectionEnd ) {
return false;
}
if ( isReverse ) {
return container.selectionStart === 0;
}
return container.value.length === container.selectionStart;
}
if ( ! container.isContentEditable ) {
return true;
}
const { ownerDocument } = container;
const { defaultView } = ownerDocument;
assertIsDefined( defaultView, 'defaultView' );
const selection = defaultView.getSelection();
if ( ! selection || ! selection.rangeCount ) {
return false;
}
const range = selection.getRangeAt( 0 );
const collapsedRange = range.cloneRange();
const isForward = isSelectionForward( selection );
const isCollapsed = selection.isCollapsed;
// Collapse in direction of selection.
if ( ! isCollapsed ) {
collapsedRange.collapse( ! isForward );
}
const collapsedRangeRect = getRectangleFromRange( collapsedRange );
const rangeRect = getRectangleFromRange( range );
if ( ! collapsedRangeRect || ! rangeRect ) {
return false;
}
// Only consider the multiline selection at the edge if the direction is
// towards the edge. The selection is multiline if it is taller than the
// collapsed selection.
const rangeHeight = getRangeHeight( range );
if (
! isCollapsed &&
rangeHeight &&
rangeHeight > collapsedRangeRect.height &&
isForward === isReverse
) {
return false;
}
// In the case of RTL scripts, the horizontal edge is at the opposite side.
const isReverseDir = isRTL( container ) ? ! isReverse : isReverse;
const containerRect = container.getBoundingClientRect();
// To check if a selection is at the edge, we insert a test selection at the
// edge of the container and check if the selections have the same vertical
// or horizontal position. If they do, the selection is at the edge.
// This method proves to be better than a DOM-based calculation for the
// horizontal edge, since it ignores empty textnodes and a trailing line
// break element. In other words, we need to check visual positioning, not
// DOM positioning.
// It also proves better than using the computed style for the vertical
// edge, because we cannot know the padding and line height reliably in
// pixels. `getComputedStyle` may return a value with different units.
const x = isReverseDir ? containerRect.left + 1 : containerRect.right - 1;
const y = isReverse ? containerRect.top + 1 : containerRect.bottom - 1;
const testRange = scrollIfNoRange( container, isReverse, () =>
hiddenCaretRangeFromPoint( ownerDocument, x, y, container )
);
if ( ! testRange ) {
return false;
}
const testRect = getRectangleFromRange( testRange );
if ( ! testRect ) {
return false;
}
const verticalSide = isReverse ? 'top' : 'bottom';
const horizontalSide = isReverseDir ? 'left' : 'right';
const verticalDiff = testRect[ verticalSide ] - rangeRect[ verticalSide ];
const horizontalDiff =
testRect[ horizontalSide ] - collapsedRangeRect[ horizontalSide ];
// Allow the position to be 1px off.
const hasVerticalDiff = Math.abs( verticalDiff ) <= 1;
const hasHorizontalDiff = Math.abs( horizontalDiff ) <= 1;
return onlyVertical
? hasVerticalDiff
: hasVerticalDiff && hasHorizontalDiff;
}

9
node_modules/@wordpress/dom/src/dom/is-element.js generated vendored Normal file
View File

@@ -0,0 +1,9 @@
/* eslint-disable jsdoc/valid-types */
/**
* @param {Node | null | undefined} node
* @return {node is Element} True if node is an Element node
*/
export default function isElement( node ) {
/* eslint-enable jsdoc/valid-types */
return !! node && node.nodeType === node.ELEMENT_NODE;
}

28
node_modules/@wordpress/dom/src/dom/is-empty.js generated vendored Normal file
View File

@@ -0,0 +1,28 @@
/**
* Recursively checks if an element is empty. An element is not empty if it
* contains text or contains elements with attributes such as images.
*
* @param {Element} element The element to check.
*
* @return {boolean} Whether or not the element is empty.
*/
export default function isEmpty( element ) {
switch ( element.nodeType ) {
case element.TEXT_NODE:
// We cannot use \s since it includes special spaces which we want
// to preserve.
return /^[ \f\n\r\t\v\u00a0]*$/.test( element.nodeValue || '' );
case element.ELEMENT_NODE:
if ( element.hasAttributes() ) {
return false;
} else if ( ! element.hasChildNodes() ) {
return true;
}
return /** @type {Element[]} */ (
Array.from( element.childNodes )
).every( isEmpty );
default:
return true;
}
}

View File

@@ -0,0 +1,84 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
import isInputOrTextArea from './is-input-or-text-area';
/**
* Check whether the contents of the element have been entirely selected.
* Returns true if there is no possibility of selection.
*
* @param {HTMLElement} element The element to check.
*
* @return {boolean} True if entirely selected, false if not.
*/
export default function isEntirelySelected( element ) {
if ( isInputOrTextArea( element ) ) {
return (
element.selectionStart === 0 &&
element.value.length === element.selectionEnd
);
}
if ( ! element.isContentEditable ) {
return true;
}
const { ownerDocument } = element;
const { defaultView } = ownerDocument;
assertIsDefined( defaultView, 'defaultView' );
const selection = defaultView.getSelection();
assertIsDefined( selection, 'selection' );
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null;
if ( ! range ) {
return true;
}
const { startContainer, endContainer, startOffset, endOffset } = range;
if (
startContainer === element &&
endContainer === element &&
startOffset === 0 &&
endOffset === element.childNodes.length
) {
return true;
}
const lastChild = element.lastChild;
assertIsDefined( lastChild, 'lastChild' );
const endContainerContentLength =
endContainer.nodeType === endContainer.TEXT_NODE
? /** @type {Text} */ ( endContainer ).data.length
: endContainer.childNodes.length;
return (
isDeepChild( startContainer, element, 'firstChild' ) &&
isDeepChild( endContainer, element, 'lastChild' ) &&
startOffset === 0 &&
endOffset === endContainerContentLength
);
}
/**
* Check whether the contents of the element have been entirely selected.
* Returns true if there is no possibility of selection.
*
* @param {HTMLElement|Node} query The element to check.
* @param {HTMLElement} container The container that we suspect "query" may be a first or last child of.
* @param {"firstChild"|"lastChild"} propName "firstChild" or "lastChild"
*
* @return {boolean} True if query is a deep first/last child of container, false otherwise.
*/
function isDeepChild( query, container, propName ) {
/** @type {HTMLElement | ChildNode | null} */
let candidate = container;
do {
if ( query === candidate ) {
return true;
}
candidate = candidate[ propName ];
} while ( candidate );
return false;
}

24
node_modules/@wordpress/dom/src/dom/is-form-element.js generated vendored Normal file
View File

@@ -0,0 +1,24 @@
/**
* Internal dependencies
*/
import isInputOrTextArea from './is-input-or-text-area';
/**
*
* Detects if element is a form element.
*
* @param {Element} element The element to check.
*
* @return {boolean} True if form element and false otherwise.
*/
export default function isFormElement( element ) {
if ( ! element ) {
return false;
}
const { tagName } = element;
const checkForInputTextarea = isInputOrTextArea( element );
return (
checkForInputTextarea || tagName === 'BUTTON' || tagName === 'SELECT'
);
}

View File

@@ -0,0 +1,16 @@
/**
* Internal dependencies
*/
import isEdge from './is-edge';
/**
* Check whether the selection is horizontally at the edge of the container.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse Set to true to check left, false for right.
*
* @return {boolean} True if at the horizontal edge, false if not.
*/
export default function isHorizontalEdge( container, isReverse ) {
return isEdge( container, isReverse );
}

View File

@@ -0,0 +1,9 @@
/* eslint-disable jsdoc/valid-types */
/**
* @param {Node} node
* @return {node is HTMLInputElement} Whether the node is an HTMLInputElement.
*/
export default function isHTMLInputElement( node ) {
/* eslint-enable jsdoc/valid-types */
return node?.nodeName === 'INPUT';
}

View File

@@ -0,0 +1,9 @@
/* eslint-disable jsdoc/valid-types */
/**
* @param {Element} element
* @return {element is HTMLInputElement | HTMLTextAreaElement} Whether the element is an input or textarea
*/
export default function isInputOrTextArea( element ) {
/* eslint-enable jsdoc/valid-types */
return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA';
}

30
node_modules/@wordpress/dom/src/dom/is-number-input.js generated vendored Normal file
View File

@@ -0,0 +1,30 @@
/**
* WordPress dependencies
*/
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import isHTMLInputElement from './is-html-input-element';
/* eslint-disable jsdoc/valid-types */
/**
* Check whether the given element is an input field of type number.
*
* @param {Node} node The HTML node.
*
* @return {node is HTMLInputElement} True if the node is number input.
*/
export default function isNumberInput( node ) {
deprecated( 'wp.dom.isNumberInput', {
since: '6.1',
version: '6.5',
} );
/* eslint-enable jsdoc/valid-types */
return (
isHTMLInputElement( node ) &&
node.type === 'number' &&
! isNaN( node.valueAsNumber )
);
}

15
node_modules/@wordpress/dom/src/dom/is-rtl.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* Internal dependencies
*/
import getComputedStyle from './get-computed-style';
/**
* Whether the element's text direction is right-to-left.
*
* @param {Element} element The element to check.
*
* @return {boolean} True if rtl, false if ltr.
*/
export default function isRTL( element ) {
return getComputedStyle( element ).direction === 'rtl';
}

View File

@@ -0,0 +1,45 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Returns true if the given selection object is in the forward direction, or
* false otherwise.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
*
* @param {Selection} selection Selection object to check.
*
* @return {boolean} Whether the selection is forward.
*/
export default function isSelectionForward( selection ) {
const { anchorNode, focusNode, anchorOffset, focusOffset } = selection;
assertIsDefined( anchorNode, 'anchorNode' );
assertIsDefined( focusNode, 'focusNode' );
const position = anchorNode.compareDocumentPosition( focusNode );
// Disable reason: `Node#compareDocumentPosition` returns a bitmask value,
// so bitwise operators are intended.
/* eslint-disable no-bitwise */
// Compare whether anchor node precedes focus node. If focus node (where
// end of selection occurs) is after the anchor node, it is forward.
if ( position & anchorNode.DOCUMENT_POSITION_PRECEDING ) {
return false;
}
if ( position & anchorNode.DOCUMENT_POSITION_FOLLOWING ) {
return true;
}
/* eslint-enable no-bitwise */
// `compareDocumentPosition` returns 0 when passed the same node, in which
// case compare offsets.
if ( position === 0 ) {
return anchorOffset <= focusOffset;
}
// This should never be reached, but return true as default case.
return true;
}

39
node_modules/@wordpress/dom/src/dom/is-text-field.js generated vendored Normal file
View File

@@ -0,0 +1,39 @@
/**
* Internal dependencies
*/
import isHTMLInputElement from './is-html-input-element';
/* eslint-disable jsdoc/valid-types */
/**
* Check whether the given element is a text field, where text field is defined
* by the ability to select within the input, or that it is contenteditable.
*
* See: https://html.spec.whatwg.org/#textFieldSelection
*
* @param {Node} node The HTML element.
* @return {node is HTMLElement} True if the element is an text field, false if not.
*/
export default function isTextField( node ) {
/* eslint-enable jsdoc/valid-types */
const nonTextInputs = [
'button',
'checkbox',
'hidden',
'file',
'radio',
'image',
'range',
'reset',
'submit',
'number',
'email',
'time',
];
return (
( isHTMLInputElement( node ) &&
node.type &&
! nonTextInputs.includes( node.type ) ) ||
node.nodeName === 'TEXTAREA' ||
/** @type {HTMLElement} */ ( node ).contentEditable === 'true'
);
}

View File

@@ -0,0 +1,16 @@
/**
* Internal dependencies
*/
import isEdge from './is-edge';
/**
* Check whether the selection is vertically at the edge of the container.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse Set to true to check top, false for bottom.
*
* @return {boolean} True if at the vertical edge, false if not.
*/
export default function isVerticalEdge( container, isReverse ) {
return isEdge( container, isReverse, true );
}

View File

@@ -0,0 +1,89 @@
/**
* Internal dependencies
*/
import hiddenCaretRangeFromPoint from './hidden-caret-range-from-point';
import { assertIsDefined } from '../utils/assert-is-defined';
import isInputOrTextArea from './is-input-or-text-area';
import isRTL from './is-rtl';
import { scrollIfNoRange } from './scroll-if-no-range';
/**
* Gets the range to place.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for end, false for start.
* @param {number|undefined} x X coordinate to vertically position.
*
* @return {Range|null} The range to place.
*/
function getRange( container, isReverse, x ) {
const { ownerDocument } = container;
// In the case of RTL scripts, the horizontal edge is at the opposite side.
const isReverseDir = isRTL( container ) ? ! isReverse : isReverse;
const containerRect = container.getBoundingClientRect();
// When placing at the end (isReverse), find the closest range to the bottom
// right corner. When placing at the start, to the top left corner.
// Ensure x is defined and within the container's boundaries. When it's
// exactly at the boundary, it's not considered within the boundaries.
if ( x === undefined ) {
x = isReverse ? containerRect.right - 1 : containerRect.left + 1;
} else if ( x <= containerRect.left ) {
x = containerRect.left + 1;
} else if ( x >= containerRect.right ) {
x = containerRect.right - 1;
}
const y = isReverseDir ? containerRect.bottom - 1 : containerRect.top + 1;
return hiddenCaretRangeFromPoint( ownerDocument, x, y, container );
}
/**
* Places the caret at start or end of a given element.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for end, false for start.
* @param {number|undefined} x X coordinate to vertically position.
*/
export default function placeCaretAtEdge( container, isReverse, x ) {
if ( ! container ) {
return;
}
container.focus();
if ( isInputOrTextArea( container ) ) {
// The element may not support selection setting.
if ( typeof container.selectionStart !== 'number' ) {
return;
}
if ( isReverse ) {
container.selectionStart = container.value.length;
container.selectionEnd = container.value.length;
} else {
container.selectionStart = 0;
container.selectionEnd = 0;
}
return;
}
if ( ! container.isContentEditable ) {
return;
}
const range = scrollIfNoRange( container, isReverse, () =>
getRange( container, isReverse, x )
);
if ( ! range ) {
return;
}
const { ownerDocument } = container;
const { defaultView } = ownerDocument;
assertIsDefined( defaultView, 'defaultView' );
const selection = defaultView.getSelection();
assertIsDefined( selection, 'selection' );
selection.removeAllRanges();
selection.addRange( range );
}

View File

@@ -0,0 +1,14 @@
/**
* Internal dependencies
*/
import placeCaretAtEdge from './place-caret-at-edge';
/**
* Places the caret at start or end of a given element.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for end, false for start.
*/
export default function placeCaretAtHorizontalEdge( container, isReverse ) {
return placeCaretAtEdge( container, isReverse, undefined );
}

View File

@@ -0,0 +1,15 @@
/**
* Internal dependencies
*/
import placeCaretAtEdge from './place-caret-at-edge';
/**
* Places the caret at the top or bottom of a given element.
*
* @param {HTMLElement} container Focusable element.
* @param {boolean} isReverse True for bottom, false for top.
* @param {DOMRect} [rect] The rectangle to position the caret with.
*/
export default function placeCaretAtVerticalEdge( container, isReverse, rect ) {
return placeCaretAtEdge( container, isReverse, rect?.left );
}

View File

@@ -0,0 +1,23 @@
/**
* Internal dependencies
*/
import cleanNodeList from './clean-node-list';
/**
* Given a schema, unwraps or removes nodes, attributes and classes on HTML.
*
* @param {string} HTML The HTML to clean up.
* @param {import('./clean-node-list').Schema} schema Schema for the HTML.
* @param {boolean} inline Whether to clean for inline mode.
*
* @return {string} The cleaned up HTML.
*/
export default function removeInvalidHTML( HTML, schema, inline ) {
const doc = document.implementation.createHTMLDocument( '' );
doc.body.innerHTML = HTML;
cleanNodeList( doc.body.childNodes, doc, schema, inline );
return doc.body.innerHTML;
}

15
node_modules/@wordpress/dom/src/dom/remove.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Given a DOM node, removes it from the DOM.
*
* @param {Node} node Node to be removed.
* @return {void}
*/
export default function remove( node ) {
assertIsDefined( node.parentNode, 'node.parentNode' );
node.parentNode.removeChild( node );
}

25
node_modules/@wordpress/dom/src/dom/replace-tag.js generated vendored Normal file
View File

@@ -0,0 +1,25 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Replaces the given node with a new node with the given tag name.
*
* @param {Element} node The node to replace
* @param {string} tagName The new tag name.
*
* @return {Element} The new node.
*/
export default function replaceTag( node, tagName ) {
const newNode = node.ownerDocument.createElement( tagName );
while ( node.firstChild ) {
newNode.appendChild( node.firstChild );
}
assertIsDefined( node.parentNode, 'node.parentNode' );
node.parentNode.replaceChild( newNode, node );
return newNode;
}

19
node_modules/@wordpress/dom/src/dom/replace.js generated vendored Normal file
View File

@@ -0,0 +1,19 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
import insertAfter from './insert-after';
import remove from './remove';
/**
* Given two DOM nodes, replaces the former with the latter in the DOM.
*
* @param {Element} processedNode Node to be removed.
* @param {Element} newNode Node to be inserted in its place.
* @return {void}
*/
export default function replace( processedNode, newNode ) {
assertIsDefined( processedNode.parentNode, 'processedNode.parentNode' );
insertAfter( newNode, processedNode.parentNode );
remove( processedNode );
}

38
node_modules/@wordpress/dom/src/dom/safe-html.js generated vendored Normal file
View File

@@ -0,0 +1,38 @@
/**
* Internal dependencies
*/
import remove from './remove';
/**
* Strips scripts and on* attributes from HTML.
*
* @param {string} html HTML to sanitize.
*
* @return {string} The sanitized HTML.
*/
export default function safeHTML( html ) {
const { body } = document.implementation.createHTMLDocument( '' );
body.innerHTML = html;
const elements = body.getElementsByTagName( '*' );
let elementIndex = elements.length;
while ( elementIndex-- ) {
const element = elements[ elementIndex ];
if ( element.tagName === 'SCRIPT' ) {
remove( element );
} else {
let attributeIndex = element.attributes.length;
while ( attributeIndex-- ) {
const { name: key } = element.attributes[ attributeIndex ];
if ( key.startsWith( 'on' ) ) {
element.removeAttribute( key );
}
}
}
}
return body.innerHTML;
}

View File

@@ -0,0 +1,34 @@
/**
* If no range range can be created or it is outside the container, the element
* may be out of view, so scroll it into view and try again.
*
* @param {HTMLElement} container The container to scroll.
* @param {boolean} alignToTop True to align to top, false to bottom.
* @param {Function} callback The callback to create the range.
*
* @return {?Range} The range returned by the callback.
*/
export function scrollIfNoRange( container, alignToTop, callback ) {
let range = callback();
// If no range range can be created or it is outside the container, the
// element may be out of view.
if (
! range ||
! range.startContainer ||
! container.contains( range.startContainer )
) {
container.scrollIntoView( alignToTop );
range = callback();
if (
! range ||
! range.startContainer ||
! container.contains( range.startContainer )
) {
return null;
}
}
return range;
}

21
node_modules/@wordpress/dom/src/dom/strip-html.js generated vendored Normal file
View File

@@ -0,0 +1,21 @@
/**
* Internal dependencies
*/
import safeHTML from './safe-html';
/**
* Removes any HTML tags from the provided string.
*
* @param {string} html The string containing html.
*
* @return {string} The text content with any html removed.
*/
export default function stripHTML( html ) {
// Remove any script tags or on* attributes otherwise their *contents* will be left
// in place following removal of HTML tags.
html = safeHTML( html );
const doc = document.implementation.createHTMLDocument( '' );
doc.body.innerHTML = html;
return doc.body.textContent || '';
}

31
node_modules/@wordpress/dom/src/dom/test/safe-html.js generated vendored Normal file
View File

@@ -0,0 +1,31 @@
/**
* Internal dependencies
*/
import safeHTML from '../safe-html';
describe( 'safeHTML', () => {
it( 'should strip on* attributes', () => {
const input = '<img src="" onerror="alert(\'1\')" onload="">';
const output = '<img src="">';
expect( safeHTML( input ) ).toBe( output );
} );
it( 'should strip on* attributes with spacing', () => {
const input = '<img src="" onerror = "alert(\'1\')" onload = "">';
const output = '<img src="">';
expect( safeHTML( input ) ).toBe( output );
} );
it( 'should strip nested on* attributes', () => {
const input =
'<p><strong><img src="" onerror="alert(\'1\')"></strong></p>';
const output = '<p><strong><img src=""></strong></p>';
expect( safeHTML( input ) ).toBe( output );
} );
it( 'should strip script tags', () => {
const input = '<script>alert("1")</script><script></script>';
const output = '';
expect( safeHTML( input ) ).toBe( output );
} );
} );

64
node_modules/@wordpress/dom/src/dom/test/strip-html.js generated vendored Normal file
View File

@@ -0,0 +1,64 @@
/**
* Internal dependencies
*/
import stripHTML from '../strip-html';
describe( 'stripHTML', () => {
it( 'should strip valid HTML, scripts and on attributes', () => {
const input = `<strong onClick="alert('and on attributes')">Here is some text</strong> that contains <em>HTML markup</em><script>alert("and scripts")</script>.`;
const output = 'Here is some text that contains HTML markup.';
expect( stripHTML( input ) ).toBe( output );
} );
it( 'should strip invalid HTML, scripts and on attributes', () => {
const input = `<strong onClick="alert('and on attributes')">Here is some text</em> <p></div>that contains HTML markup</p><script>alert("and scripts")</script>.`;
const output = 'Here is some text that contains HTML markup.';
expect( stripHTML( input ) ).toBe( output );
} );
describe( 'whitespace preservation', () => {
it( 'should preserve leading spaces', () => {
const input =
' <strong>Here is some text</strong> with <em>leading spaces</em>.';
const output = ' Here is some text with leading spaces.';
expect( stripHTML( input ) ).toBe( output );
} );
it( 'should preserve leading spaces with HTML', () => {
const input =
'<strong> Here is some text</strong> with <em>leading spaces</em>.';
const output = ' Here is some text with leading spaces.';
expect( stripHTML( input ) ).toBe( output );
} );
it( 'should preserve trailing spaces with HTML', () => {
const input =
'<strong>Here is some text</strong> with <em>trailing spaces</em>. ';
const output = 'Here is some text with trailing spaces. ';
expect( stripHTML( input ) ).toBe( output );
} );
it( 'should preserve consecutive spaces within string', () => {
const input =
'<strong>Here is some text</strong> with <em>a lot of spaces inside</em>.';
const output =
'Here is some text with a lot of spaces inside.';
expect( stripHTML( input ) ).toBe( output );
} );
it( 'should preserve new lines in multi-line HTML string', () => {
const input = `<div>
Here is some
<em>text</em>
with new lines
</div>`;
const output = `
Here is some
text
with new lines
`;
expect( stripHTML( input ) ).toBe( output );
} );
} );
} );

23
node_modules/@wordpress/dom/src/dom/unwrap.js generated vendored Normal file
View File

@@ -0,0 +1,23 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Unwrap the given node. This means any child nodes are moved to the parent.
*
* @param {Node} node The node to unwrap.
*
* @return {void}
*/
export default function unwrap( node ) {
const parent = node.parentNode;
assertIsDefined( parent, 'node.parentNode' );
while ( node.firstChild ) {
parent.insertBefore( node.firstChild, node );
}
parent.removeChild( node );
}

16
node_modules/@wordpress/dom/src/dom/wrap.js generated vendored Normal file
View File

@@ -0,0 +1,16 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
/**
* Wraps the given node with a new node with the given tag name.
*
* @param {Element} newNode The node to insert.
* @param {Element} referenceNode The node to wrap.
*/
export default function wrap( newNode, referenceNode ) {
assertIsDefined( referenceNode.parentNode, 'referenceNode.parentNode' );
referenceNode.parentNode.insertBefore( newNode, referenceNode );
newNode.appendChild( referenceNode );
}

118
node_modules/@wordpress/dom/src/focusable.js generated vendored Normal file
View File

@@ -0,0 +1,118 @@
/**
* References:
*
* Focusable:
* - https://www.w3.org/TR/html5/editing.html#focus-management
*
* Sequential focus navigation:
* - https://www.w3.org/TR/html5/editing.html#sequential-focus-navigation-and-the-tabindex-attribute
*
* Disabled elements:
* - https://www.w3.org/TR/html5/disabled-elements.html#disabled-elements
*
* getClientRects algorithm (requiring layout box):
* - https://www.w3.org/TR/cssom-view-1/#extension-to-the-element-interface
*
* AREA elements associated with an IMG:
* - https://w3c.github.io/html/editing.html#data-model
*/
/**
* Returns a CSS selector used to query for focusable elements.
*
* @param {boolean} sequential If set, only query elements that are sequentially
* focusable. Non-interactive elements with a
* negative `tabindex` are focusable but not
* sequentially focusable.
* https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute
*
* @return {string} CSS selector.
*/
function buildSelector( sequential ) {
return [
sequential ? '[tabindex]:not([tabindex^="-"])' : '[tabindex]',
'a[href]',
'button:not([disabled])',
'input:not([type="hidden"]):not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'iframe:not([tabindex^="-"])',
'object',
'embed',
'area[href]',
'[contenteditable]:not([contenteditable=false])',
].join( ',' );
}
/**
* Returns true if the specified element is visible (i.e. neither display: none
* nor visibility: hidden).
*
* @param {HTMLElement} element DOM element to test.
*
* @return {boolean} Whether element is visible.
*/
function isVisible( element ) {
return (
element.offsetWidth > 0 ||
element.offsetHeight > 0 ||
element.getClientRects().length > 0
);
}
/**
* Returns true if the specified area element is a valid focusable element, or
* false otherwise. Area is only focusable if within a map where a named map
* referenced by an image somewhere in the document.
*
* @param {HTMLAreaElement} element DOM area element to test.
*
* @return {boolean} Whether area element is valid for focus.
*/
function isValidFocusableArea( element ) {
/** @type {HTMLMapElement | null} */
const map = element.closest( 'map[name]' );
if ( ! map ) {
return false;
}
/** @type {HTMLImageElement | null} */
const img = element.ownerDocument.querySelector(
'img[usemap="#' + map.name + '"]'
);
return !! img && isVisible( img );
}
/**
* Returns all focusable elements within a given context.
*
* @param {Element} context Element in which to search.
* @param {Object} options
* @param {boolean} [options.sequential] If set, only return elements that are
* sequentially focusable.
* Non-interactive elements with a
* negative `tabindex` are focusable but
* not sequentially focusable.
* https://html.spec.whatwg.org/multipage/interaction.html#the-tabindex-attribute
*
* @return {HTMLElement[]} Focusable elements.
*/
export function find( context, { sequential = false } = {} ) {
/** @type {NodeListOf<HTMLElement>} */
const elements = context.querySelectorAll( buildSelector( sequential ) );
return Array.from( elements ).filter( ( element ) => {
if ( ! isVisible( element ) ) {
return false;
}
const { nodeName } = element;
if ( 'AREA' === nodeName ) {
return isValidFocusableArea(
/** @type {HTMLAreaElement} */ ( element )
);
}
return true;
} );
}

15
node_modules/@wordpress/dom/src/index.js generated vendored Normal file
View File

@@ -0,0 +1,15 @@
/**
* Internal dependencies
*/
import * as focusable from './focusable';
import * as tabbable from './tabbable';
/**
* Object grouping `focusable` and `tabbable` utils
* under the keys with the same name.
*/
export const focus = { focusable, tabbable };
export * from './dom';
export * from './phrasing-content';
export * from './data-transfer';

198
node_modules/@wordpress/dom/src/phrasing-content.js generated vendored Normal file
View File

@@ -0,0 +1,198 @@
/**
* All phrasing content elements.
*
* @see https://www.w3.org/TR/2011/WD-html5-20110525/content-models.html#phrasing-content-0
*/
/**
* @typedef {Record<string,SemanticElementDefinition>} ContentSchema
*/
/**
* @typedef SemanticElementDefinition
* @property {string[]} [attributes] Content attributes
* @property {ContentSchema} [children] Content attributes
*/
/**
* All text-level semantic elements.
*
* @see https://html.spec.whatwg.org/multipage/text-level-semantics.html
*
* @type {ContentSchema}
*/
const textContentSchema = {
strong: {},
em: {},
s: {},
del: {},
ins: {},
a: { attributes: [ 'href', 'target', 'rel', 'id' ] },
code: {},
abbr: { attributes: [ 'title' ] },
sub: {},
sup: {},
br: {},
small: {},
// To do: fix blockquote.
// cite: {},
q: { attributes: [ 'cite' ] },
dfn: { attributes: [ 'title' ] },
data: { attributes: [ 'value' ] },
time: { attributes: [ 'datetime' ] },
var: {},
samp: {},
kbd: {},
i: {},
b: {},
u: {},
mark: {},
ruby: {},
rt: {},
rp: {},
bdi: { attributes: [ 'dir' ] },
bdo: { attributes: [ 'dir' ] },
wbr: {},
'#text': {},
};
// Recursion is needed.
// Possible: strong > em > strong.
// Impossible: strong > strong.
const excludedElements = [ '#text', 'br' ];
Object.keys( textContentSchema )
.filter( ( element ) => ! excludedElements.includes( element ) )
.forEach( ( tag ) => {
const { [ tag ]: removedTag, ...restSchema } = textContentSchema;
textContentSchema[ tag ].children = restSchema;
} );
/**
* Embedded content elements.
*
* @see https://www.w3.org/TR/2011/WD-html5-20110525/content-models.html#embedded-content-0
*
* @type {ContentSchema}
*/
const embeddedContentSchema = {
audio: {
attributes: [
'src',
'preload',
'autoplay',
'mediagroup',
'loop',
'muted',
],
},
canvas: { attributes: [ 'width', 'height' ] },
embed: { attributes: [ 'src', 'type', 'width', 'height' ] },
img: {
attributes: [
'alt',
'src',
'srcset',
'usemap',
'ismap',
'width',
'height',
],
},
object: {
attributes: [
'data',
'type',
'name',
'usemap',
'form',
'width',
'height',
],
},
video: {
attributes: [
'src',
'poster',
'preload',
'playsinline',
'autoplay',
'mediagroup',
'loop',
'muted',
'controls',
'width',
'height',
],
},
};
/**
* Phrasing content elements.
*
* @see https://www.w3.org/TR/2011/WD-html5-20110525/content-models.html#phrasing-content-0
*/
const phrasingContentSchema = {
...textContentSchema,
...embeddedContentSchema,
};
/**
* Get schema of possible paths for phrasing content.
*
* @see https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content
*
* @param {string} [context] Set to "paste" to exclude invisible elements and
* sensitive data.
*
* @return {Partial<ContentSchema>} Schema.
*/
export function getPhrasingContentSchema( context ) {
if ( context !== 'paste' ) {
return phrasingContentSchema;
}
/**
* @type {Partial<ContentSchema>}
*/
const {
u, // Used to mark misspelling. Shouldn't be pasted.
abbr, // Invisible.
data, // Invisible.
time, // Invisible.
wbr, // Invisible.
bdi, // Invisible.
bdo, // Invisible.
...remainingContentSchema
} = {
...phrasingContentSchema,
// We shouldn't paste potentially sensitive information which is not
// visible to the user when pasted, so strip the attributes.
ins: { children: phrasingContentSchema.ins.children },
del: { children: phrasingContentSchema.del.children },
};
return remainingContentSchema;
}
/**
* Find out whether or not the given node is phrasing content.
*
* @see https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Content_categories#Phrasing_content
*
* @param {Node} node The node to test.
*
* @return {boolean} True if phrasing content, false if not.
*/
export function isPhrasingContent( node ) {
const tag = node.nodeName.toLowerCase();
return getPhrasingContentSchema().hasOwnProperty( tag ) || tag === 'span';
}
/**
* @param {Node} node
* @return {boolean} Node is text content
*/
export function isTextContent( node ) {
const tag = node.nodeName.toLowerCase();
return textContentSchema.hasOwnProperty( tag ) || tag === 'span';
}

187
node_modules/@wordpress/dom/src/tabbable.js generated vendored Normal file
View File

@@ -0,0 +1,187 @@
/**
* Internal dependencies
*/
import { find as findFocusable } from './focusable';
/**
* Returns the tab index of the given element. In contrast with the tabIndex
* property, this normalizes the default (0) to avoid browser inconsistencies,
* operating under the assumption that this function is only ever called with a
* focusable node.
*
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1190261
*
* @param {Element} element Element from which to retrieve.
*
* @return {number} Tab index of element (default 0).
*/
function getTabIndex( element ) {
const tabIndex = element.getAttribute( 'tabindex' );
return tabIndex === null ? 0 : parseInt( tabIndex, 10 );
}
/**
* Returns true if the specified element is tabbable, or false otherwise.
*
* @param {Element} element Element to test.
*
* @return {boolean} Whether element is tabbable.
*/
export function isTabbableIndex( element ) {
return getTabIndex( element ) !== -1;
}
/** @typedef {HTMLElement & { type?: string, checked?: boolean, name?: string }} MaybeHTMLInputElement */
/**
* Returns a stateful reducer function which constructs a filtered array of
* tabbable elements, where at most one radio input is selected for a given
* name, giving priority to checked input, falling back to the first
* encountered.
*
* @return {(acc: MaybeHTMLInputElement[], el: MaybeHTMLInputElement) => MaybeHTMLInputElement[]} Radio group collapse reducer.
*/
function createStatefulCollapseRadioGroup() {
/** @type {Record<string, MaybeHTMLInputElement>} */
const CHOSEN_RADIO_BY_NAME = {};
return function collapseRadioGroup(
/** @type {MaybeHTMLInputElement[]} */ result,
/** @type {MaybeHTMLInputElement} */ element
) {
const { nodeName, type, checked, name } = element;
// For all non-radio tabbables, construct to array by concatenating.
if ( nodeName !== 'INPUT' || type !== 'radio' || ! name ) {
return result.concat( element );
}
const hasChosen = CHOSEN_RADIO_BY_NAME.hasOwnProperty( name );
// Omit by skipping concatenation if the radio element is not chosen.
const isChosen = checked || ! hasChosen;
if ( ! isChosen ) {
return result;
}
// At this point, if there had been a chosen element, the current
// element is checked and should take priority. Retroactively remove
// the element which had previously been considered the chosen one.
if ( hasChosen ) {
const hadChosenElement = CHOSEN_RADIO_BY_NAME[ name ];
result = result.filter( ( e ) => e !== hadChosenElement );
}
CHOSEN_RADIO_BY_NAME[ name ] = element;
return result.concat( element );
};
}
/**
* An array map callback, returning an object with the element value and its
* array index location as properties. This is used to emulate a proper stable
* sort where equal tabIndex should be left in order of their occurrence in the
* document.
*
* @param {HTMLElement} element Element.
* @param {number} index Array index of element.
*
* @return {{ element: HTMLElement, index: number }} Mapped object with element, index.
*/
function mapElementToObjectTabbable( element, index ) {
return { element, index };
}
/**
* An array map callback, returning an element of the given mapped object's
* element value.
*
* @param {{ element: HTMLElement }} object Mapped object with element.
*
* @return {HTMLElement} Mapped object element.
*/
function mapObjectTabbableToElement( object ) {
return object.element;
}
/**
* A sort comparator function used in comparing two objects of mapped elements.
*
* @see mapElementToObjectTabbable
*
* @param {{ element: HTMLElement, index: number }} a First object to compare.
* @param {{ element: HTMLElement, index: number }} b Second object to compare.
*
* @return {number} Comparator result.
*/
function compareObjectTabbables( a, b ) {
const aTabIndex = getTabIndex( a.element );
const bTabIndex = getTabIndex( b.element );
if ( aTabIndex === bTabIndex ) {
return a.index - b.index;
}
return aTabIndex - bTabIndex;
}
/**
* Givin focusable elements, filters out tabbable element.
*
* @param {HTMLElement[]} focusables Focusable elements to filter.
*
* @return {HTMLElement[]} Tabbable elements.
*/
function filterTabbable( focusables ) {
return focusables
.filter( isTabbableIndex )
.map( mapElementToObjectTabbable )
.sort( compareObjectTabbables )
.map( mapObjectTabbableToElement )
.reduce( createStatefulCollapseRadioGroup(), [] );
}
/**
* @param {Element} context
* @return {HTMLElement[]} Tabbable elements within the context.
*/
export function find( context ) {
return filterTabbable( findFocusable( context ) );
}
/**
* Given a focusable element, find the preceding tabbable element.
*
* @param {Element} element The focusable element before which to look. Defaults
* to the active element.
*
* @return {HTMLElement|undefined} Preceding tabbable element.
*/
export function findPrevious( element ) {
return filterTabbable( findFocusable( element.ownerDocument.body ) )
.reverse()
.find(
( focusable ) =>
// eslint-disable-next-line no-bitwise
element.compareDocumentPosition( focusable ) &
element.DOCUMENT_POSITION_PRECEDING
);
}
/**
* Given a focusable element, find the next tabbable element.
*
* @param {Element} element The focusable element after which to look. Defaults
* to the active element.
*
* @return {HTMLElement|undefined} Next tabbable element.
*/
export function findNext( element ) {
return filterTabbable( findFocusable( element.ownerDocument.body ) ).find(
( focusable ) =>
// eslint-disable-next-line no-bitwise
element.compareDocumentPosition( focusable ) &
element.DOCUMENT_POSITION_FOLLOWING
);
}

327
node_modules/@wordpress/dom/src/test/dom.js generated vendored Normal file
View File

@@ -0,0 +1,327 @@
/**
* Internal dependencies
*/
import {
isHorizontalEdge,
placeCaretAtHorizontalEdge,
isTextField,
removeInvalidHTML,
isEmpty,
} from '../dom';
import { getPhrasingContentSchema } from '../phrasing-content';
describe( 'DOM', () => {
let parent;
beforeEach( () => {
parent = document.createElement( 'div' );
document.body.appendChild( parent );
} );
afterEach( () => {
parent.remove();
} );
describe( 'isHorizontalEdge', () => {
it( 'should return true for empty input', () => {
const input = document.createElement( 'input' );
parent.appendChild( input );
input.focus();
expect( isHorizontalEdge( input, true ) ).toBe( true );
expect( isHorizontalEdge( input, false ) ).toBe( true );
} );
it( 'should return the right values if we focus the end of the input', () => {
const input = document.createElement( 'input' );
parent.appendChild( input );
input.value = 'value';
input.focus();
input.selectionStart = 5;
input.selectionEnd = 5;
expect( isHorizontalEdge( input, true ) ).toBe( false );
expect( isHorizontalEdge( input, false ) ).toBe( true );
} );
it( 'should return the right values if we focus the start of the input', () => {
const input = document.createElement( 'input' );
parent.appendChild( input );
input.value = 'value';
input.focus();
input.selectionStart = 0;
input.selectionEnd = 0;
expect( isHorizontalEdge( input, true ) ).toBe( true );
expect( isHorizontalEdge( input, false ) ).toBe( false );
} );
it( 'should return false if were not at the edge', () => {
const input = document.createElement( 'input' );
parent.appendChild( input );
input.value = 'value';
input.focus();
input.selectionStart = 3;
input.selectionEnd = 3;
expect( isHorizontalEdge( input, true ) ).toBe( false );
expect( isHorizontalEdge( input, false ) ).toBe( false );
} );
it( 'should return false if the selection is not collapseds', () => {
const input = document.createElement( 'input' );
parent.appendChild( input );
input.value = 'value';
input.focus();
input.selectionStart = 0;
input.selectionEnd = 5;
expect( isHorizontalEdge( input, true ) ).toBe( false );
expect( isHorizontalEdge( input, false ) ).toBe( false );
} );
it( 'should always return true for non content editabless', () => {
const div = document.createElement( 'div' );
parent.appendChild( div );
expect( isHorizontalEdge( div, true ) ).toBe( true );
expect( isHorizontalEdge( div, false ) ).toBe( true );
} );
it( 'should return true for input types that do not have selection ranges', () => {
const input = document.createElement( 'input' );
input.setAttribute( 'type', 'checkbox' );
parent.appendChild( input );
expect( isHorizontalEdge( input, true ) ).toBe( true );
expect( isHorizontalEdge( input, false ) ).toBe( true );
} );
} );
describe( 'placeCaretAtHorizontalEdge', () => {
it( 'should place caret at the start of the input', () => {
const input = document.createElement( 'input' );
input.value = 'value';
placeCaretAtHorizontalEdge( input, true );
expect( isHorizontalEdge( input, false ) ).toBe( true );
} );
it( 'should place caret at the end of the input', () => {
const input = document.createElement( 'input' );
input.value = 'value';
placeCaretAtHorizontalEdge( input, false );
expect( isHorizontalEdge( input, true ) ).toBe( true );
} );
} );
describe( 'isTextField', () => {
/**
* A sampling of input types expected not to be text eligible.
*
* @type {string[]}
*/
const NON_TEXT_INPUT_TYPES = [
'button',
'checkbox',
'hidden',
'file',
'radio',
'image',
'range',
'reset',
'submit',
'email',
'time',
];
/**
* A sampling of input types expected to be text eligible.
*
* @type {string[]}
*/
const TEXT_INPUT_TYPES = [ 'text', 'password', 'search', 'url' ];
it( 'should return false for non-text input elements', () => {
NON_TEXT_INPUT_TYPES.forEach( ( type ) => {
const input = document.createElement( 'input' );
input.type = type;
expect( isTextField( input ) ).toBe( false );
} );
} );
it( 'should return true for text input elements', () => {
TEXT_INPUT_TYPES.forEach( ( type ) => {
const input = document.createElement( 'input' );
input.type = type;
expect( isTextField( input ) ).toBe( true );
} );
} );
it( 'should return true for an textarea element', () => {
expect( isTextField( document.createElement( 'textarea' ) ) ).toBe(
true
);
} );
it( 'should return true for a contenteditable element', () => {
const div = document.createElement( 'div' );
div.contentEditable = 'true';
expect( isTextField( div ) ).toBe( true );
} );
it( 'should return true for a normal div element', () => {
expect( isTextField( document.createElement( 'div' ) ) ).toBe(
false
);
} );
} );
} );
describe( 'removeInvalidHTML', () => {
const phrasingContentSchema = getPhrasingContentSchema();
const schema = {
p: {
children: phrasingContentSchema,
},
figure: {
require: [ 'img' ],
children: {
img: {
attributes: [ 'src', 'alt' ],
classes: [ 'alignleft' ],
},
figcaption: {
children: phrasingContentSchema,
},
},
},
...phrasingContentSchema,
};
it( 'should leave plain text alone', () => {
const input = 'test';
expect( removeInvalidHTML( input, schema ) ).toBe( input );
} );
it( 'should leave valid phrasing content alone', () => {
const input = '<strong>test</strong>';
expect( removeInvalidHTML( input, schema ) ).toBe( input );
} );
it( 'should remove unrecognised tags from phrasing content', () => {
const input = '<strong><div>test</div></strong>';
const output = '<strong>test</strong>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should remove unwanted whitespace outside phrasing content', () => {
const input = '<figure><img src=""> </figure>';
const output = '<figure><img src=""></figure>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should remove attributes', () => {
const input = '<p class="test">test</p>';
const output = '<p>test</p>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should remove id attributes', () => {
const input = '<p id="foo">test</p>';
const output = '<p>test</p>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should remove multiple attributes', () => {
const input = '<p class="test" id="test">test</p>';
const output = '<p>test</p>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should deep remove attributes', () => {
const input = '<p class="test">test <em id="test">test</em></p>';
const output = '<p>test <em>test</em></p>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should remove data-* attributes', () => {
const input = '<p data-reactid="1">test</p>';
const output = '<p>test</p>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should keep some attributes', () => {
const input = '<a href="#keep" target="_blank">test</a>';
const output = '<a href="#keep" target="_blank">test</a>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should keep some classes', () => {
const input = '<figure><img class="alignleft test" src=""></figure>';
const output = '<figure><img class="alignleft" src=""></figure>';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should remove empty nodes that should have children', () => {
const input = '<figure> </figure>';
const output = '';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should break up block content with phrasing schema', () => {
const input = '<p>test</p><p>test</p>';
const output = 'test<br>test';
expect( removeInvalidHTML( input, phrasingContentSchema, true ) ).toBe(
output
);
} );
it( 'should unwrap node that does not satisfy require', () => {
const input =
'<figure><p>test</p><figcaption>test</figcaption></figure>';
const output = '<p>test</p>test';
expect( removeInvalidHTML( input, schema ) ).toBe( output );
} );
it( 'should remove invalid phrasing content', () => {
const input = '<strong><p>test</p></strong>';
const output = '<p>test</p>';
expect( removeInvalidHTML( input, schema ) ).toEqual( output );
} );
} );
describe( 'isEmpty', () => {
function isEmptyHTML( HTML ) {
const doc = document.implementation.createHTMLDocument( '' );
doc.body.innerHTML = HTML;
return isEmpty( doc.body );
}
it( 'should return true for empty element', () => {
expect( isEmptyHTML( '' ) ).toBe( true );
} );
it( 'should return true for element with only whitespace', () => {
expect( isEmptyHTML( ' ' ) ).toBe( true );
} );
it( 'should return true for element with non breaking space', () => {
expect( isEmptyHTML( '&nbsp;' ) ).toBe( true );
} );
it( 'should return true for element with BR', () => {
expect( isEmptyHTML( '<br>' ) ).toBe( true );
} );
it( 'should return true for element with empty element', () => {
expect( isEmptyHTML( '<em></em>' ) ).toBe( true );
} );
it( 'should return false for element with image', () => {
expect( isEmptyHTML( '<img src="">' ) ).toBe( false );
} );
it( 'should return true for element with mixed empty pieces', () => {
expect( isEmptyHTML( ' <br><br><em>&nbsp; </em>' ) ).toBe( true );
} );
} );

161
node_modules/@wordpress/dom/src/test/focusable.js generated vendored Normal file
View File

@@ -0,0 +1,161 @@
/**
* Internal dependencies
*/
import createElement from './utils/create-element';
import { find } from '../focusable';
describe( 'focusable', () => {
beforeEach( () => {
document.body.innerHTML = '';
} );
describe( 'find()', () => {
it( 'returns empty array if no children', () => {
const node = createElement( 'div' );
expect( find( node ) ).toEqual( [] );
} );
it( 'returns empty array if no focusable children', () => {
const node = createElement( 'div' );
node.appendChild( createElement( 'div' ) );
expect( find( node ) ).toEqual( [] );
} );
it( 'returns array of focusable children', () => {
const node = createElement( 'div' );
node.appendChild( createElement( 'input' ) );
const focusable = find( node );
expect( focusable ).toHaveLength( 1 );
expect( focusable[ 0 ].nodeName ).toBe( 'INPUT' );
} );
it( 'finds nested focusable child', () => {
const node = createElement( 'div' );
node.appendChild( createElement( 'div' ) );
node.firstChild.appendChild( createElement( 'input' ) );
const focusable = find( node );
expect( focusable ).toHaveLength( 1 );
expect( focusable[ 0 ].nodeName ).toBe( 'INPUT' );
} );
it( 'finds link with no href but tabindex', () => {
const node = createElement( 'div' );
const link = createElement( 'a' );
link.tabIndex = 0;
node.appendChild( link );
expect( find( node ) ).toEqual( [ link ] );
} );
it( 'finds valid area focusable', () => {
const map = createElement( 'map' );
map.name = 'testfocus';
const area = createElement( 'area' );
area.href = '';
map.appendChild( area );
const img = createElement( 'img' );
img.setAttribute( 'usemap', '#testfocus' );
document.body.appendChild( map );
document.body.appendChild( img );
const focusable = find( map );
expect( focusable ).toHaveLength( 1 );
expect( focusable[ 0 ].nodeName ).toBe( 'AREA' );
} );
it( 'ignores invalid area focusable', () => {
const map = createElement( 'map' );
map.name = 'testfocus';
const area = createElement( 'area' );
area.href = '';
map.appendChild( area );
const img = createElement( 'img' );
img.setAttribute( 'usemap', '#testfocus' );
img.style.display = 'none';
document.body.appendChild( map );
document.body.appendChild( img );
expect( find( map ) ).toEqual( [] );
} );
it( 'finds contenteditable', () => {
const node = createElement( 'div' );
const div = createElement( 'div' );
node.appendChild( div );
div.setAttribute( 'contenteditable', '' );
expect( find( node ) ).toEqual( [ div ] );
div.setAttribute( 'contenteditable', 'true' );
expect( find( node ) ).toEqual( [ div ] );
} );
it( 'ignores contenteditable=false', () => {
const node = createElement( 'div' );
const div = createElement( 'div' );
node.appendChild( div );
div.setAttribute( 'contenteditable', 'false' );
expect( find( node ) ).toEqual( [] );
} );
it( 'ignores invisible inputs', () => {
const node = createElement( 'div' );
const input = createElement( 'input' );
node.appendChild( input );
input.style.visibility = 'hidden';
expect( find( node ) ).toEqual( [] );
input.style.visibility = 'visible';
input.style.display = 'none';
expect( find( node ) ).toEqual( [] );
input.style.display = 'inline-block';
const focusable = find( node );
expect( focusable ).toHaveLength( 1 );
expect( focusable[ 0 ].nodeName ).toBe( 'INPUT' );
} );
it( 'ignores inputs in invisible ancestors', () => {
const node = createElement( 'div' );
const input = createElement( 'input' );
node.appendChild( input );
node.style.visibility = 'hidden';
expect( find( node ) ).toEqual( [] );
node.style.visibility = 'visible';
node.style.display = 'none';
expect( find( node ) ).toEqual( [] );
node.style.display = 'block';
const focusable = find( node );
expect( focusable ).toHaveLength( 1 );
expect( focusable[ 0 ].nodeName ).toBe( 'INPUT' );
} );
it( 'does not return context even if focusable', () => {
const node = createElement( 'div' );
node.tabIndex = 0;
expect( find( node ) ).toEqual( [] );
} );
it( 'limits found focusables to specific context', () => {
const node = createElement( 'div' );
node.appendChild( createElement( 'div' ) );
document.body.appendChild( node );
document.body.appendChild( createElement( 'input' ) );
expect( find( node ) ).toEqual( [] );
} );
} );
} );

117
node_modules/@wordpress/dom/src/test/tabbable.js generated vendored Normal file
View File

@@ -0,0 +1,117 @@
/**
* Internal dependencies
*/
import createElement from './utils/create-element';
import { find } from '../tabbable';
describe( 'tabbable', () => {
beforeEach( () => {
document.body.innerHTML = '';
} );
describe( 'find()', () => {
it( 'returns focusables in order of tabindex', () => {
const node = createElement( 'div' );
const absent = createElement( 'input' );
absent.tabIndex = -1;
const first = createElement( 'input' );
const second = createElement( 'span' );
second.tabIndex = 0;
const third = createElement( 'input' );
third.tabIndex = 1;
node.appendChild( third );
node.appendChild( first );
node.appendChild( second );
node.appendChild( absent );
const tabbables = find( node );
expect( tabbables ).toEqual( [ first, second, third ] );
} );
it( 'consolidates radio group to the first, if unchecked', () => {
const node = createElement( 'div' );
const firstRadio = createElement( 'input' );
firstRadio.type = 'radio';
firstRadio.name = 'a';
firstRadio.value = 'firstRadio';
const secondRadio = createElement( 'input' );
secondRadio.type = 'radio';
secondRadio.name = 'a';
secondRadio.value = 'secondRadio';
const text = createElement( 'input' );
text.type = 'text';
text.name = 'b';
const thirdRadio = createElement( 'input' );
thirdRadio.type = 'radio';
thirdRadio.name = 'a';
thirdRadio.value = 'thirdRadio';
const fourthRadio = createElement( 'input' );
fourthRadio.type = 'radio';
fourthRadio.name = 'b';
fourthRadio.value = 'fourthRadio';
const fifthRadio = createElement( 'input' );
fifthRadio.type = 'radio';
fifthRadio.name = 'b';
fifthRadio.value = 'fifthRadio';
node.appendChild( firstRadio );
node.appendChild( secondRadio );
node.appendChild( text );
node.appendChild( thirdRadio );
node.appendChild( fourthRadio );
node.appendChild( fifthRadio );
const tabbables = find( node );
expect( tabbables ).toEqual( [ firstRadio, text, fourthRadio ] );
} );
it( 'consolidates radio group to the checked', () => {
const node = createElement( 'div' );
const firstRadio = createElement( 'input' );
firstRadio.type = 'radio';
firstRadio.name = 'a';
firstRadio.value = 'firstRadio';
const secondRadio = createElement( 'input' );
secondRadio.type = 'radio';
secondRadio.name = 'a';
secondRadio.value = 'secondRadio';
const text = createElement( 'input' );
text.type = 'text';
text.name = 'b';
const thirdRadio = createElement( 'input' );
thirdRadio.type = 'radio';
thirdRadio.name = 'a';
thirdRadio.value = 'thirdRadio';
thirdRadio.checked = true;
node.appendChild( firstRadio );
node.appendChild( secondRadio );
node.appendChild( text );
node.appendChild( thirdRadio );
const tabbables = find( node );
expect( tabbables ).toEqual( [ text, thirdRadio ] );
} );
it( 'not consolidate unnamed radio inputs', () => {
const node = createElement( 'div' );
const firstRadio = createElement( 'input' );
firstRadio.type = 'radio';
firstRadio.value = 'firstRadio';
const text = createElement( 'input' );
text.type = 'text';
text.name = 'b';
const secondRadio = createElement( 'input' );
secondRadio.type = 'radio';
secondRadio.value = 'secondRadio';
node.appendChild( firstRadio );
node.appendChild( text );
node.appendChild( secondRadio );
const tabbables = find( node );
expect( tabbables ).toEqual( [ firstRadio, text, secondRadio ] );
} );
} );
} );

25
node_modules/@wordpress/dom/src/test/utils.js generated vendored Normal file
View File

@@ -0,0 +1,25 @@
/**
* Internal dependencies
*/
import { assertIsDefined } from '../utils/assert-is-defined';
describe( 'assertIsDefined', () => {
it( 'should throw if the variable is null', () => {
expect( () => assertIsDefined( null, 'val' ) ).toThrow(
"Expected 'val' to be defined, but received null"
);
} );
it( 'should throw if the variable is undefined', () => {
expect( () => assertIsDefined( undefined, 'val' ) ).toThrow(
"Expected 'val' to be defined, but received undefined"
);
} );
it.each( [ 0, '', NaN, -0, 1, new String(), {}, [], false, Infinity ] )(
'should not throw if the value is %s',
( val ) => {
expect( () => assertIsDefined( val, 'val' ) ).not.toThrow();
}
);
} );

View File

@@ -0,0 +1,55 @@
/**
* Given an element type, returns an HTMLElement with an emulated layout,
* since JSDOM does have its own internal layout engine.
*
* @param {string} type Element type.
*
* @return {HTMLElement} Layout-emulated element.
*/
export default function createElement( type ) {
const element = document.createElement( type );
const ifNotHidden = ( value, elseValue ) =>
function () {
let isHidden = false;
let node = this;
do {
isHidden =
node.style.display === 'none' ||
node.style.visibility === 'hidden';
node = node.parentNode;
} while (
! isHidden &&
node &&
node.nodeType === node.ELEMENT_NODE
);
return isHidden ? elseValue : value;
};
Object.defineProperties( element, {
offsetHeight: {
get: ifNotHidden( 10, 0 ),
},
offsetWidth: {
get: ifNotHidden( 10, 0 ),
},
} );
element.getClientRects = ifNotHidden(
[
{
width: 10,
height: 10,
top: 0,
right: 10,
bottom: 10,
left: 0,
},
],
[]
);
return element;
}

View File

@@ -0,0 +1,13 @@
export function assertIsDefined< T >(
val: T,
name: string
): asserts val is NonNullable< T > {
if (
process.env.NODE_ENV !== 'production' &&
( val === undefined || val === null )
) {
throw new Error(
`Expected '${ name }' to be defined, but received ${ val }`
);
}
}