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

View File

@@ -0,0 +1,42 @@
/**
* Internal dependencies
*/
import { toHTMLString } from '../../to-html-string';
import { isCollapsed } from '../../is-collapsed';
import { slice } from '../../slice';
import { getTextContent } from '../../get-text-content';
export default (props => element => {
function onCopy(event) {
const {
record
} = props.current;
const {
ownerDocument
} = element;
if (isCollapsed(record.current) || !element.contains(ownerDocument.activeElement)) {
return;
}
const selectedRecord = slice(record.current);
const plainText = getTextContent(selectedRecord);
const html = toHTMLString({
value: selectedRecord
});
event.clipboardData.setData('text/plain', plainText);
event.clipboardData.setData('text/html', html);
event.clipboardData.setData('rich-text', 'true');
event.preventDefault();
if (event.type === 'cut') {
ownerDocument.execCommand('delete');
}
}
const {
defaultView
} = element.ownerDocument;
defaultView.addEventListener('copy', onCopy);
defaultView.addEventListener('cut', onCopy);
return () => {
defaultView.removeEventListener('copy', onCopy);
defaultView.removeEventListener('cut', onCopy);
};
});
//# sourceMappingURL=copy-handler.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["toHTMLString","isCollapsed","slice","getTextContent","props","element","onCopy","event","record","current","ownerDocument","contains","activeElement","selectedRecord","plainText","html","value","clipboardData","setData","preventDefault","type","execCommand","defaultView","addEventListener","removeEventListener"],"sources":["@wordpress/rich-text/src/component/event-listeners/copy-handler.js"],"sourcesContent":["/**\n * Internal dependencies\n */\nimport { toHTMLString } from '../../to-html-string';\nimport { isCollapsed } from '../../is-collapsed';\nimport { slice } from '../../slice';\nimport { getTextContent } from '../../get-text-content';\n\nexport default ( props ) => ( element ) => {\n\tfunction onCopy( event ) {\n\t\tconst { record } = props.current;\n\t\tconst { ownerDocument } = element;\n\t\tif (\n\t\t\tisCollapsed( record.current ) ||\n\t\t\t! element.contains( ownerDocument.activeElement )\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst selectedRecord = slice( record.current );\n\t\tconst plainText = getTextContent( selectedRecord );\n\t\tconst html = toHTMLString( { value: selectedRecord } );\n\t\tevent.clipboardData.setData( 'text/plain', plainText );\n\t\tevent.clipboardData.setData( 'text/html', html );\n\t\tevent.clipboardData.setData( 'rich-text', 'true' );\n\t\tevent.preventDefault();\n\n\t\tif ( event.type === 'cut' ) {\n\t\t\townerDocument.execCommand( 'delete' );\n\t\t}\n\t}\n\n\tconst { defaultView } = element.ownerDocument;\n\n\tdefaultView.addEventListener( 'copy', onCopy );\n\tdefaultView.addEventListener( 'cut', onCopy );\n\treturn () => {\n\t\tdefaultView.removeEventListener( 'copy', onCopy );\n\t\tdefaultView.removeEventListener( 'cut', onCopy );\n\t};\n};\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,YAAY,QAAQ,sBAAsB;AACnD,SAASC,WAAW,QAAQ,oBAAoB;AAChD,SAASC,KAAK,QAAQ,aAAa;AACnC,SAASC,cAAc,QAAQ,wBAAwB;AAEvD,gBAAiBC,KAAK,IAAQC,OAAO,IAAM;EAC1C,SAASC,MAAMA,CAAEC,KAAK,EAAG;IACxB,MAAM;MAAEC;IAAO,CAAC,GAAGJ,KAAK,CAACK,OAAO;IAChC,MAAM;MAAEC;IAAc,CAAC,GAAGL,OAAO;IACjC,IACCJ,WAAW,CAAEO,MAAM,CAACC,OAAQ,CAAC,IAC7B,CAAEJ,OAAO,CAACM,QAAQ,CAAED,aAAa,CAACE,aAAc,CAAC,EAChD;MACD;IACD;IAEA,MAAMC,cAAc,GAAGX,KAAK,CAAEM,MAAM,CAACC,OAAQ,CAAC;IAC9C,MAAMK,SAAS,GAAGX,cAAc,CAAEU,cAAe,CAAC;IAClD,MAAME,IAAI,GAAGf,YAAY,CAAE;MAAEgB,KAAK,EAAEH;IAAe,CAAE,CAAC;IACtDN,KAAK,CAACU,aAAa,CAACC,OAAO,CAAE,YAAY,EAAEJ,SAAU,CAAC;IACtDP,KAAK,CAACU,aAAa,CAACC,OAAO,CAAE,WAAW,EAAEH,IAAK,CAAC;IAChDR,KAAK,CAACU,aAAa,CAACC,OAAO,CAAE,WAAW,EAAE,MAAO,CAAC;IAClDX,KAAK,CAACY,cAAc,CAAC,CAAC;IAEtB,IAAKZ,KAAK,CAACa,IAAI,KAAK,KAAK,EAAG;MAC3BV,aAAa,CAACW,WAAW,CAAE,QAAS,CAAC;IACtC;EACD;EAEA,MAAM;IAAEC;EAAY,CAAC,GAAGjB,OAAO,CAACK,aAAa;EAE7CY,WAAW,CAACC,gBAAgB,CAAE,MAAM,EAAEjB,MAAO,CAAC;EAC9CgB,WAAW,CAACC,gBAAgB,CAAE,KAAK,EAAEjB,MAAO,CAAC;EAC7C,OAAO,MAAM;IACZgB,WAAW,CAACE,mBAAmB,CAAE,MAAM,EAAElB,MAAO,CAAC;IACjDgB,WAAW,CAACE,mBAAmB,CAAE,KAAK,EAAElB,MAAO,CAAC;EACjD,CAAC;AACF,CAAC","ignoreList":[]}

View File

@@ -0,0 +1,43 @@
/**
* WordPress dependencies
*/
import { BACKSPACE, DELETE } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import { remove } from '../../remove';
export default (props => element => {
function onKeyDown(event) {
const {
keyCode
} = event;
const {
createRecord,
handleChange
} = props.current;
if (event.defaultPrevented) {
return;
}
if (keyCode !== DELETE && keyCode !== BACKSPACE) {
return;
}
const currentValue = createRecord();
const {
start,
end,
text
} = currentValue;
// Always handle full content deletion ourselves.
if (start === 0 && end !== 0 && end === text.length) {
handleChange(remove(currentValue));
event.preventDefault();
}
}
element.addEventListener('keydown', onKeyDown);
return () => {
element.removeEventListener('keydown', onKeyDown);
};
});
//# sourceMappingURL=delete.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["BACKSPACE","DELETE","remove","props","element","onKeyDown","event","keyCode","createRecord","handleChange","current","defaultPrevented","currentValue","start","end","text","length","preventDefault","addEventListener","removeEventListener"],"sources":["@wordpress/rich-text/src/component/event-listeners/delete.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { BACKSPACE, DELETE } from '@wordpress/keycodes';\n\n/**\n * Internal dependencies\n */\nimport { remove } from '../../remove';\n\nexport default ( props ) => ( element ) => {\n\tfunction onKeyDown( event ) {\n\t\tconst { keyCode } = event;\n\t\tconst { createRecord, handleChange } = props.current;\n\n\t\tif ( event.defaultPrevented ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( keyCode !== DELETE && keyCode !== BACKSPACE ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst currentValue = createRecord();\n\t\tconst { start, end, text } = currentValue;\n\n\t\t// Always handle full content deletion ourselves.\n\t\tif ( start === 0 && end !== 0 && end === text.length ) {\n\t\t\thandleChange( remove( currentValue ) );\n\t\t\tevent.preventDefault();\n\t\t}\n\t}\n\n\telement.addEventListener( 'keydown', onKeyDown );\n\treturn () => {\n\t\telement.removeEventListener( 'keydown', onKeyDown );\n\t};\n};\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,SAAS,EAAEC,MAAM,QAAQ,qBAAqB;;AAEvD;AACA;AACA;AACA,SAASC,MAAM,QAAQ,cAAc;AAErC,gBAAiBC,KAAK,IAAQC,OAAO,IAAM;EAC1C,SAASC,SAASA,CAAEC,KAAK,EAAG;IAC3B,MAAM;MAAEC;IAAQ,CAAC,GAAGD,KAAK;IACzB,MAAM;MAAEE,YAAY;MAAEC;IAAa,CAAC,GAAGN,KAAK,CAACO,OAAO;IAEpD,IAAKJ,KAAK,CAACK,gBAAgB,EAAG;MAC7B;IACD;IAEA,IAAKJ,OAAO,KAAKN,MAAM,IAAIM,OAAO,KAAKP,SAAS,EAAG;MAClD;IACD;IAEA,MAAMY,YAAY,GAAGJ,YAAY,CAAC,CAAC;IACnC,MAAM;MAAEK,KAAK;MAAEC,GAAG;MAAEC;IAAK,CAAC,GAAGH,YAAY;;IAEzC;IACA,IAAKC,KAAK,KAAK,CAAC,IAAIC,GAAG,KAAK,CAAC,IAAIA,GAAG,KAAKC,IAAI,CAACC,MAAM,EAAG;MACtDP,YAAY,CAAEP,MAAM,CAAEU,YAAa,CAAE,CAAC;MACtCN,KAAK,CAACW,cAAc,CAAC,CAAC;IACvB;EACD;EAEAb,OAAO,CAACc,gBAAgB,CAAE,SAAS,EAAEb,SAAU,CAAC;EAChD,OAAO,MAAM;IACZD,OAAO,CAACe,mBAAmB,CAAE,SAAS,EAAEd,SAAU,CAAC;EACpD,CAAC;AACF,CAAC","ignoreList":[]}

View File

@@ -0,0 +1,101 @@
/**
* WordPress dependencies
*/
import { LEFT, RIGHT } from '@wordpress/keycodes';
/**
* Internal dependencies
*/
import { isCollapsed } from '../../is-collapsed';
const EMPTY_ACTIVE_FORMATS = [];
export default (props => element => {
function onKeyDown(event) {
const {
keyCode,
shiftKey,
altKey,
metaKey,
ctrlKey
} = event;
if (
// Only override left and right keys without modifiers pressed.
shiftKey || altKey || metaKey || ctrlKey || keyCode !== LEFT && keyCode !== RIGHT) {
return;
}
const {
record,
applyRecord,
forceRender
} = props.current;
const {
text,
formats,
start,
end,
activeFormats: currentActiveFormats = []
} = record.current;
const collapsed = isCollapsed(record.current);
const {
ownerDocument
} = element;
const {
defaultView
} = ownerDocument;
// To do: ideally, we should look at visual position instead.
const {
direction
} = defaultView.getComputedStyle(element);
const reverseKey = direction === 'rtl' ? RIGHT : LEFT;
const isReverse = event.keyCode === reverseKey;
// If the selection is collapsed and at the very start, do nothing if
// navigating backward.
// If the selection is collapsed and at the very end, do nothing if
// navigating forward.
if (collapsed && currentActiveFormats.length === 0) {
if (start === 0 && isReverse) {
return;
}
if (end === text.length && !isReverse) {
return;
}
}
// If the selection is not collapsed, let the browser handle collapsing
// the selection for now. Later we could expand this logic to set
// boundary positions if needed.
if (!collapsed) {
return;
}
const formatsBefore = formats[start - 1] || EMPTY_ACTIVE_FORMATS;
const formatsAfter = formats[start] || EMPTY_ACTIVE_FORMATS;
const destination = isReverse ? formatsBefore : formatsAfter;
const isIncreasing = currentActiveFormats.every((format, index) => format === destination[index]);
let newActiveFormatsLength = currentActiveFormats.length;
if (!isIncreasing) {
newActiveFormatsLength--;
} else if (newActiveFormatsLength < destination.length) {
newActiveFormatsLength++;
}
if (newActiveFormatsLength === currentActiveFormats.length) {
record.current._newActiveFormats = destination;
return;
}
event.preventDefault();
const origin = isReverse ? formatsAfter : formatsBefore;
const source = isIncreasing ? destination : origin;
const newActiveFormats = source.slice(0, newActiveFormatsLength);
const newValue = {
...record.current,
activeFormats: newActiveFormats
};
record.current = newValue;
applyRecord(newValue);
forceRender();
}
element.addEventListener('keydown', onKeyDown);
return () => {
element.removeEventListener('keydown', onKeyDown);
};
});
//# sourceMappingURL=format-boundaries.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,28 @@
/**
* WordPress dependencies
*/
import { useMemo, useRef } from '@wordpress/element';
import { useRefEffect } from '@wordpress/compose';
/**
* Internal dependencies
*/
import copyHandler from './copy-handler';
import selectObject from './select-object';
import formatBoundaries from './format-boundaries';
import deleteHandler from './delete';
import inputAndSelection from './input-and-selection';
import selectionChangeCompat from './selection-change-compat';
const allEventListeners = [copyHandler, selectObject, formatBoundaries, deleteHandler, inputAndSelection, selectionChangeCompat];
export function useEventListeners(props) {
const propsRef = useRef(props);
propsRef.current = props;
const refEffects = useMemo(() => allEventListeners.map(refEffect => refEffect(propsRef)), [propsRef]);
return useRefEffect(element => {
const cleanups = refEffects.map(effect => effect(element));
return () => {
cleanups.forEach(cleanup => cleanup());
};
}, [refEffects]);
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["useMemo","useRef","useRefEffect","copyHandler","selectObject","formatBoundaries","deleteHandler","inputAndSelection","selectionChangeCompat","allEventListeners","useEventListeners","props","propsRef","current","refEffects","map","refEffect","element","cleanups","effect","forEach","cleanup"],"sources":["@wordpress/rich-text/src/component/event-listeners/index.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { useMemo, useRef } from '@wordpress/element';\nimport { useRefEffect } from '@wordpress/compose';\n\n/**\n * Internal dependencies\n */\nimport copyHandler from './copy-handler';\nimport selectObject from './select-object';\nimport formatBoundaries from './format-boundaries';\nimport deleteHandler from './delete';\nimport inputAndSelection from './input-and-selection';\nimport selectionChangeCompat from './selection-change-compat';\n\nconst allEventListeners = [\n\tcopyHandler,\n\tselectObject,\n\tformatBoundaries,\n\tdeleteHandler,\n\tinputAndSelection,\n\tselectionChangeCompat,\n];\n\nexport function useEventListeners( props ) {\n\tconst propsRef = useRef( props );\n\tpropsRef.current = props;\n\tconst refEffects = useMemo(\n\t\t() => allEventListeners.map( ( refEffect ) => refEffect( propsRef ) ),\n\t\t[ propsRef ]\n\t);\n\n\treturn useRefEffect(\n\t\t( element ) => {\n\t\t\tconst cleanups = refEffects.map( ( effect ) => effect( element ) );\n\t\t\treturn () => {\n\t\t\t\tcleanups.forEach( ( cleanup ) => cleanup() );\n\t\t\t};\n\t\t},\n\t\t[ refEffects ]\n\t);\n}\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,OAAO,EAAEC,MAAM,QAAQ,oBAAoB;AACpD,SAASC,YAAY,QAAQ,oBAAoB;;AAEjD;AACA;AACA;AACA,OAAOC,WAAW,MAAM,gBAAgB;AACxC,OAAOC,YAAY,MAAM,iBAAiB;AAC1C,OAAOC,gBAAgB,MAAM,qBAAqB;AAClD,OAAOC,aAAa,MAAM,UAAU;AACpC,OAAOC,iBAAiB,MAAM,uBAAuB;AACrD,OAAOC,qBAAqB,MAAM,2BAA2B;AAE7D,MAAMC,iBAAiB,GAAG,CACzBN,WAAW,EACXC,YAAY,EACZC,gBAAgB,EAChBC,aAAa,EACbC,iBAAiB,EACjBC,qBAAqB,CACrB;AAED,OAAO,SAASE,iBAAiBA,CAAEC,KAAK,EAAG;EAC1C,MAAMC,QAAQ,GAAGX,MAAM,CAAEU,KAAM,CAAC;EAChCC,QAAQ,CAACC,OAAO,GAAGF,KAAK;EACxB,MAAMG,UAAU,GAAGd,OAAO,CACzB,MAAMS,iBAAiB,CAACM,GAAG,CAAIC,SAAS,IAAMA,SAAS,CAAEJ,QAAS,CAAE,CAAC,EACrE,CAAEA,QAAQ,CACX,CAAC;EAED,OAAOV,YAAY,CAChBe,OAAO,IAAM;IACd,MAAMC,QAAQ,GAAGJ,UAAU,CAACC,GAAG,CAAII,MAAM,IAAMA,MAAM,CAAEF,OAAQ,CAAE,CAAC;IAClE,OAAO,MAAM;MACZC,QAAQ,CAACE,OAAO,CAAIC,OAAO,IAAMA,OAAO,CAAC,CAAE,CAAC;IAC7C,CAAC;EACF,CAAC,EACD,CAAEP,UAAU,CACb,CAAC;AACF","ignoreList":[]}

View File

@@ -0,0 +1,235 @@
/**
* Internal dependencies
*/
import { getActiveFormats } from '../../get-active-formats';
import { updateFormats } from '../../update-formats';
/**
* All inserting input types that would insert HTML into the DOM.
*
* @see https://www.w3.org/TR/input-events-2/#interface-InputEvent-Attributes
*
* @type {Set}
*/
const INSERTION_INPUT_TYPES_TO_IGNORE = new Set(['insertParagraph', 'insertOrderedList', 'insertUnorderedList', 'insertHorizontalRule', 'insertLink']);
const EMPTY_ACTIVE_FORMATS = [];
const PLACEHOLDER_ATTR_NAME = 'data-rich-text-placeholder';
/**
* If the selection is set on the placeholder element, collapse the selection to
* the start (before the placeholder).
*
* @param {Window} defaultView
*/
function fixPlaceholderSelection(defaultView) {
const selection = defaultView.getSelection();
const {
anchorNode,
anchorOffset
} = selection;
if (anchorNode.nodeType !== anchorNode.ELEMENT_NODE) {
return;
}
const targetNode = anchorNode.childNodes[anchorOffset];
if (!targetNode || targetNode.nodeType !== targetNode.ELEMENT_NODE || !targetNode.hasAttribute(PLACEHOLDER_ATTR_NAME)) {
return;
}
selection.collapseToStart();
}
export default (props => element => {
const {
ownerDocument
} = element;
const {
defaultView
} = ownerDocument;
let isComposing = false;
function onInput(event) {
// Do not trigger a change if characters are being composed. Browsers
// will usually emit a final `input` event when the characters are
// composed. As of December 2019, Safari doesn't support
// nativeEvent.isComposing.
if (isComposing) {
return;
}
let inputType;
if (event) {
inputType = event.inputType;
}
const {
record,
applyRecord,
createRecord,
handleChange
} = props.current;
// The browser formatted something or tried to insert HTML. Overwrite
// it. It will be handled later by the format library if needed.
if (inputType && (inputType.indexOf('format') === 0 || INSERTION_INPUT_TYPES_TO_IGNORE.has(inputType))) {
applyRecord(record.current);
return;
}
const currentValue = createRecord();
const {
start,
activeFormats: oldActiveFormats = []
} = record.current;
// Update the formats between the last and new caret position.
const change = updateFormats({
value: currentValue,
start,
end: currentValue.start,
formats: oldActiveFormats
});
handleChange(change);
}
/**
* Syncs the selection to local state. A callback for the `selectionchange`
* event.
*/
function handleSelectionChange() {
const {
record,
applyRecord,
createRecord,
onSelectionChange
} = props.current;
// Check if the implementor disabled editing. `contentEditable` does
// disable input, but not text selection, so we must ignore selection
// changes.
if (element.contentEditable !== 'true') {
return;
}
// Ensure the active element is the rich text element.
if (ownerDocument.activeElement !== element) {
// If it is not, we can stop listening for selection changes. We
// resume listening when the element is focused.
ownerDocument.removeEventListener('selectionchange', handleSelectionChange);
return;
}
// In case of a keyboard event, ignore selection changes during
// composition.
if (isComposing) {
return;
}
const {
start,
end,
text
} = createRecord();
const oldRecord = record.current;
// Fallback mechanism for IE11, which doesn't support the input event.
// Any input results in a selection change.
if (text !== oldRecord.text) {
onInput();
return;
}
if (start === oldRecord.start && end === oldRecord.end) {
// Sometimes the browser may set the selection on the placeholder
// element, in which case the caret is not visible. We need to set
// the caret before the placeholder if that's the case.
if (oldRecord.text.length === 0 && start === 0) {
fixPlaceholderSelection(defaultView);
}
return;
}
const newValue = {
...oldRecord,
start,
end,
// _newActiveFormats may be set on arrow key navigation to control
// the right boundary position. If undefined, getActiveFormats will
// give the active formats according to the browser.
activeFormats: oldRecord._newActiveFormats,
_newActiveFormats: undefined
};
const newActiveFormats = getActiveFormats(newValue, EMPTY_ACTIVE_FORMATS);
// Update the value with the new active formats.
newValue.activeFormats = newActiveFormats;
// It is important that the internal value is updated first,
// otherwise the value will be wrong on render!
record.current = newValue;
applyRecord(newValue, {
domOnly: true
});
onSelectionChange(start, end);
}
function onCompositionStart() {
isComposing = true;
// Do not update the selection when characters are being composed as
// this rerenders the component and might destroy internal browser
// editing state.
ownerDocument.removeEventListener('selectionchange', handleSelectionChange);
// Remove the placeholder. Since the rich text value doesn't update
// during composition, the placeholder doesn't get removed. There's no
// need to re-add it, when the value is updated on compositionend it
// will be re-added when the value is empty.
element.querySelector(`[${PLACEHOLDER_ATTR_NAME}]`)?.remove();
}
function onCompositionEnd() {
isComposing = false;
// Ensure the value is up-to-date for browsers that don't emit a final
// input event after composition.
onInput({
inputType: 'insertText'
});
// Tracking selection changes can be resumed.
ownerDocument.addEventListener('selectionchange', handleSelectionChange);
}
function onFocus() {
const {
record,
isSelected,
onSelectionChange,
applyRecord
} = props.current;
// When the whole editor is editable, let writing flow handle
// selection.
if (element.parentElement.closest('[contenteditable="true"]')) {
return;
}
if (!isSelected) {
// We know for certain that on focus, the old selection is invalid.
// It will be recalculated on the next mouseup, keyup, or touchend
// event.
const index = undefined;
record.current = {
...record.current,
start: index,
end: index,
activeFormats: EMPTY_ACTIVE_FORMATS
};
} else {
applyRecord(record.current, {
domOnly: true
});
}
onSelectionChange(record.current.start, record.current.end);
// There is no selection change event when the element is focused, so
// we need to manually trigger it. The selection is also not available
// yet in this call stack.
window.queueMicrotask(handleSelectionChange);
ownerDocument.addEventListener('selectionchange', handleSelectionChange);
}
element.addEventListener('input', onInput);
element.addEventListener('compositionstart', onCompositionStart);
element.addEventListener('compositionend', onCompositionEnd);
element.addEventListener('focus', onFocus);
return () => {
element.removeEventListener('input', onInput);
element.removeEventListener('compositionstart', onCompositionStart);
element.removeEventListener('compositionend', onCompositionEnd);
element.removeEventListener('focus', onFocus);
};
});
//# sourceMappingURL=input-and-selection.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,46 @@
export default (() => element => {
function onClick(event) {
const {
target
} = event;
// If the child element has no text content, it must be an object.
if (target === element || target.textContent && target.isContentEditable) {
return;
}
const {
ownerDocument
} = target;
const {
defaultView
} = ownerDocument;
const selection = defaultView.getSelection();
// If it's already selected, do nothing and let default behavior happen.
// This means it's "click-through".
if (selection.containsNode(target)) {
return;
}
const range = ownerDocument.createRange();
// If the target is within a non editable element, select the non
// editable element.
const nodeToSelect = target.isContentEditable ? target : target.closest('[contenteditable]');
range.selectNode(nodeToSelect);
selection.removeAllRanges();
selection.addRange(range);
event.preventDefault();
}
function onFocusIn(event) {
// When there is incoming focus from a link, select the object.
if (event.relatedTarget && !element.contains(event.relatedTarget) && event.relatedTarget.tagName === 'A') {
onClick(event);
}
}
element.addEventListener('click', onClick);
element.addEventListener('focusin', onFocusIn);
return () => {
element.removeEventListener('click', onClick);
element.removeEventListener('focusin', onFocusIn);
};
});
//# sourceMappingURL=select-object.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["element","onClick","event","target","textContent","isContentEditable","ownerDocument","defaultView","selection","getSelection","containsNode","range","createRange","nodeToSelect","closest","selectNode","removeAllRanges","addRange","preventDefault","onFocusIn","relatedTarget","contains","tagName","addEventListener","removeEventListener"],"sources":["@wordpress/rich-text/src/component/event-listeners/select-object.js"],"sourcesContent":["export default () => ( element ) => {\n\tfunction onClick( event ) {\n\t\tconst { target } = event;\n\n\t\t// If the child element has no text content, it must be an object.\n\t\tif (\n\t\t\ttarget === element ||\n\t\t\t( target.textContent && target.isContentEditable )\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { ownerDocument } = target;\n\t\tconst { defaultView } = ownerDocument;\n\t\tconst selection = defaultView.getSelection();\n\n\t\t// If it's already selected, do nothing and let default behavior happen.\n\t\t// This means it's \"click-through\".\n\t\tif ( selection.containsNode( target ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst range = ownerDocument.createRange();\n\t\t// If the target is within a non editable element, select the non\n\t\t// editable element.\n\t\tconst nodeToSelect = target.isContentEditable\n\t\t\t? target\n\t\t\t: target.closest( '[contenteditable]' );\n\n\t\trange.selectNode( nodeToSelect );\n\t\tselection.removeAllRanges();\n\t\tselection.addRange( range );\n\n\t\tevent.preventDefault();\n\t}\n\n\tfunction onFocusIn( event ) {\n\t\t// When there is incoming focus from a link, select the object.\n\t\tif (\n\t\t\tevent.relatedTarget &&\n\t\t\t! element.contains( event.relatedTarget ) &&\n\t\t\tevent.relatedTarget.tagName === 'A'\n\t\t) {\n\t\t\tonClick( event );\n\t\t}\n\t}\n\n\telement.addEventListener( 'click', onClick );\n\telement.addEventListener( 'focusin', onFocusIn );\n\treturn () => {\n\t\telement.removeEventListener( 'click', onClick );\n\t\telement.removeEventListener( 'focusin', onFocusIn );\n\t};\n};\n"],"mappings":"AAAA,gBAAe,MAAQA,OAAO,IAAM;EACnC,SAASC,OAAOA,CAAEC,KAAK,EAAG;IACzB,MAAM;MAAEC;IAAO,CAAC,GAAGD,KAAK;;IAExB;IACA,IACCC,MAAM,KAAKH,OAAO,IAChBG,MAAM,CAACC,WAAW,IAAID,MAAM,CAACE,iBAAmB,EACjD;MACD;IACD;IAEA,MAAM;MAAEC;IAAc,CAAC,GAAGH,MAAM;IAChC,MAAM;MAAEI;IAAY,CAAC,GAAGD,aAAa;IACrC,MAAME,SAAS,GAAGD,WAAW,CAACE,YAAY,CAAC,CAAC;;IAE5C;IACA;IACA,IAAKD,SAAS,CAACE,YAAY,CAAEP,MAAO,CAAC,EAAG;MACvC;IACD;IAEA,MAAMQ,KAAK,GAAGL,aAAa,CAACM,WAAW,CAAC,CAAC;IACzC;IACA;IACA,MAAMC,YAAY,GAAGV,MAAM,CAACE,iBAAiB,GAC1CF,MAAM,GACNA,MAAM,CAACW,OAAO,CAAE,mBAAoB,CAAC;IAExCH,KAAK,CAACI,UAAU,CAAEF,YAAa,CAAC;IAChCL,SAAS,CAACQ,eAAe,CAAC,CAAC;IAC3BR,SAAS,CAACS,QAAQ,CAAEN,KAAM,CAAC;IAE3BT,KAAK,CAACgB,cAAc,CAAC,CAAC;EACvB;EAEA,SAASC,SAASA,CAAEjB,KAAK,EAAG;IAC3B;IACA,IACCA,KAAK,CAACkB,aAAa,IACnB,CAAEpB,OAAO,CAACqB,QAAQ,CAAEnB,KAAK,CAACkB,aAAc,CAAC,IACzClB,KAAK,CAACkB,aAAa,CAACE,OAAO,KAAK,GAAG,EAClC;MACDrB,OAAO,CAAEC,KAAM,CAAC;IACjB;EACD;EAEAF,OAAO,CAACuB,gBAAgB,CAAE,OAAO,EAAEtB,OAAQ,CAAC;EAC5CD,OAAO,CAACuB,gBAAgB,CAAE,SAAS,EAAEJ,SAAU,CAAC;EAChD,OAAO,MAAM;IACZnB,OAAO,CAACwB,mBAAmB,CAAE,OAAO,EAAEvB,OAAQ,CAAC;IAC/CD,OAAO,CAACwB,mBAAmB,CAAE,SAAS,EAAEL,SAAU,CAAC;EACpD,CAAC;AACF,CAAC","ignoreList":[]}

View File

@@ -0,0 +1,50 @@
/**
* Internal dependencies
*/
import { isRangeEqual } from '../../is-range-equal';
/**
* Sometimes some browsers are not firing a `selectionchange` event when
* changing the selection by mouse or keyboard. This hook makes sure that, if we
* detect no `selectionchange` or `input` event between the up and down events,
* we fire a `selectionchange` event.
*/
export default (() => element => {
const {
ownerDocument
} = element;
const {
defaultView
} = ownerDocument;
const selection = defaultView?.getSelection();
let range;
function getRange() {
return selection.rangeCount ? selection.getRangeAt(0) : null;
}
function onDown(event) {
const type = event.type === 'keydown' ? 'keyup' : 'pointerup';
function onCancel() {
ownerDocument.removeEventListener(type, onUp);
ownerDocument.removeEventListener('selectionchange', onCancel);
ownerDocument.removeEventListener('input', onCancel);
}
function onUp() {
onCancel();
if (isRangeEqual(range, getRange())) {
return;
}
ownerDocument.dispatchEvent(new Event('selectionchange'));
}
ownerDocument.addEventListener(type, onUp);
ownerDocument.addEventListener('selectionchange', onCancel);
ownerDocument.addEventListener('input', onCancel);
range = getRange();
}
element.addEventListener('pointerdown', onDown);
element.addEventListener('keydown', onDown);
return () => {
element.removeEventListener('pointerdown', onDown);
element.removeEventListener('keydown', onDown);
};
});
//# sourceMappingURL=selection-change-compat.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["isRangeEqual","element","ownerDocument","defaultView","selection","getSelection","range","getRange","rangeCount","getRangeAt","onDown","event","type","onCancel","removeEventListener","onUp","dispatchEvent","Event","addEventListener"],"sources":["@wordpress/rich-text/src/component/event-listeners/selection-change-compat.js"],"sourcesContent":["/**\n * Internal dependencies\n */\nimport { isRangeEqual } from '../../is-range-equal';\n\n/**\n * Sometimes some browsers are not firing a `selectionchange` event when\n * changing the selection by mouse or keyboard. This hook makes sure that, if we\n * detect no `selectionchange` or `input` event between the up and down events,\n * we fire a `selectionchange` event.\n */\nexport default () => ( element ) => {\n\tconst { ownerDocument } = element;\n\tconst { defaultView } = ownerDocument;\n\tconst selection = defaultView?.getSelection();\n\n\tlet range;\n\n\tfunction getRange() {\n\t\treturn selection.rangeCount ? selection.getRangeAt( 0 ) : null;\n\t}\n\n\tfunction onDown( event ) {\n\t\tconst type = event.type === 'keydown' ? 'keyup' : 'pointerup';\n\n\t\tfunction onCancel() {\n\t\t\townerDocument.removeEventListener( type, onUp );\n\t\t\townerDocument.removeEventListener( 'selectionchange', onCancel );\n\t\t\townerDocument.removeEventListener( 'input', onCancel );\n\t\t}\n\n\t\tfunction onUp() {\n\t\t\tonCancel();\n\t\t\tif ( isRangeEqual( range, getRange() ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\townerDocument.dispatchEvent( new Event( 'selectionchange' ) );\n\t\t}\n\n\t\townerDocument.addEventListener( type, onUp );\n\t\townerDocument.addEventListener( 'selectionchange', onCancel );\n\t\townerDocument.addEventListener( 'input', onCancel );\n\n\t\trange = getRange();\n\t}\n\n\telement.addEventListener( 'pointerdown', onDown );\n\telement.addEventListener( 'keydown', onDown );\n\treturn () => {\n\t\telement.removeEventListener( 'pointerdown', onDown );\n\t\telement.removeEventListener( 'keydown', onDown );\n\t};\n};\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,YAAY,QAAQ,sBAAsB;;AAEnD;AACA;AACA;AACA;AACA;AACA;AACA,gBAAe,MAAQC,OAAO,IAAM;EACnC,MAAM;IAAEC;EAAc,CAAC,GAAGD,OAAO;EACjC,MAAM;IAAEE;EAAY,CAAC,GAAGD,aAAa;EACrC,MAAME,SAAS,GAAGD,WAAW,EAAEE,YAAY,CAAC,CAAC;EAE7C,IAAIC,KAAK;EAET,SAASC,QAAQA,CAAA,EAAG;IACnB,OAAOH,SAAS,CAACI,UAAU,GAAGJ,SAAS,CAACK,UAAU,CAAE,CAAE,CAAC,GAAG,IAAI;EAC/D;EAEA,SAASC,MAAMA,CAAEC,KAAK,EAAG;IACxB,MAAMC,IAAI,GAAGD,KAAK,CAACC,IAAI,KAAK,SAAS,GAAG,OAAO,GAAG,WAAW;IAE7D,SAASC,QAAQA,CAAA,EAAG;MACnBX,aAAa,CAACY,mBAAmB,CAAEF,IAAI,EAAEG,IAAK,CAAC;MAC/Cb,aAAa,CAACY,mBAAmB,CAAE,iBAAiB,EAAED,QAAS,CAAC;MAChEX,aAAa,CAACY,mBAAmB,CAAE,OAAO,EAAED,QAAS,CAAC;IACvD;IAEA,SAASE,IAAIA,CAAA,EAAG;MACfF,QAAQ,CAAC,CAAC;MACV,IAAKb,YAAY,CAAEM,KAAK,EAAEC,QAAQ,CAAC,CAAE,CAAC,EAAG;QACxC;MACD;MACAL,aAAa,CAACc,aAAa,CAAE,IAAIC,KAAK,CAAE,iBAAkB,CAAE,CAAC;IAC9D;IAEAf,aAAa,CAACgB,gBAAgB,CAAEN,IAAI,EAAEG,IAAK,CAAC;IAC5Cb,aAAa,CAACgB,gBAAgB,CAAE,iBAAiB,EAAEL,QAAS,CAAC;IAC7DX,aAAa,CAACgB,gBAAgB,CAAE,OAAO,EAAEL,QAAS,CAAC;IAEnDP,KAAK,GAAGC,QAAQ,CAAC,CAAC;EACnB;EAEAN,OAAO,CAACiB,gBAAgB,CAAE,aAAa,EAAER,MAAO,CAAC;EACjDT,OAAO,CAACiB,gBAAgB,CAAE,SAAS,EAAER,MAAO,CAAC;EAC7C,OAAO,MAAM;IACZT,OAAO,CAACa,mBAAmB,CAAE,aAAa,EAAEJ,MAAO,CAAC;IACpDT,OAAO,CAACa,mBAAmB,CAAE,SAAS,EAAEJ,MAAO,CAAC;EACjD,CAAC;AACF,CAAC","ignoreList":[]}

View File

@@ -0,0 +1,199 @@
/**
* WordPress dependencies
*/
import { useRef, useLayoutEffect, useReducer } from '@wordpress/element';
import { useMergeRefs, useRefEffect } from '@wordpress/compose';
import { useRegistry } from '@wordpress/data';
/**
* Internal dependencies
*/
import { create, RichTextData } from '../create';
import { apply } from '../to-dom';
import { toHTMLString } from '../to-html-string';
import { useDefaultStyle } from './use-default-style';
import { useBoundaryStyle } from './use-boundary-style';
import { useEventListeners } from './event-listeners';
export function useRichText({
value = '',
selectionStart,
selectionEnd,
placeholder,
onSelectionChange,
preserveWhiteSpace,
onChange,
__unstableDisableFormats: disableFormats,
__unstableIsSelected: isSelected,
__unstableDependencies = [],
__unstableAfterParse,
__unstableBeforeSerialize,
__unstableAddInvisibleFormats
}) {
const registry = useRegistry();
const [, forceRender] = useReducer(() => ({}));
const ref = useRef();
function createRecord() {
const {
ownerDocument: {
defaultView
}
} = ref.current;
const selection = defaultView.getSelection();
const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
return create({
element: ref.current,
range,
__unstableIsEditableTree: true
});
}
function applyRecord(newRecord, {
domOnly
} = {}) {
apply({
value: newRecord,
current: ref.current,
prepareEditableTree: __unstableAddInvisibleFormats,
__unstableDomOnly: domOnly,
placeholder
});
}
// Internal values are updated synchronously, unlike props and state.
const _value = useRef(value);
const record = useRef();
function setRecordFromProps() {
_value.current = value;
record.current = value;
if (!(value instanceof RichTextData)) {
record.current = value ? RichTextData.fromHTMLString(value, {
preserveWhiteSpace
}) : RichTextData.empty();
}
// To do: make rich text internally work with RichTextData.
record.current = {
text: record.current.text,
formats: record.current.formats,
replacements: record.current.replacements
};
if (disableFormats) {
record.current.formats = Array(value.length);
record.current.replacements = Array(value.length);
}
if (__unstableAfterParse) {
record.current.formats = __unstableAfterParse(record.current);
}
record.current.start = selectionStart;
record.current.end = selectionEnd;
}
const hadSelectionUpdate = useRef(false);
if (!record.current) {
hadSelectionUpdate.current = isSelected;
setRecordFromProps();
} else if (selectionStart !== record.current.start || selectionEnd !== record.current.end) {
hadSelectionUpdate.current = isSelected;
record.current = {
...record.current,
start: selectionStart,
end: selectionEnd,
activeFormats: undefined
};
}
/**
* Sync the value to global state. The node tree and selection will also be
* updated if differences are found.
*
* @param {Object} newRecord The record to sync and apply.
*/
function handleChange(newRecord) {
record.current = newRecord;
applyRecord(newRecord);
if (disableFormats) {
_value.current = newRecord.text;
} else {
const newFormats = __unstableBeforeSerialize ? __unstableBeforeSerialize(newRecord) : newRecord.formats;
newRecord = {
...newRecord,
formats: newFormats
};
if (typeof value === 'string') {
_value.current = toHTMLString({
value: newRecord,
preserveWhiteSpace
});
} else {
_value.current = new RichTextData(newRecord);
}
}
const {
start,
end,
formats,
text
} = record.current;
// Selection must be updated first, so it is recorded in history when
// the content change happens.
// We batch both calls to only attempt to rerender once.
registry.batch(() => {
onSelectionChange(start, end);
onChange(_value.current, {
__unstableFormats: formats,
__unstableText: text
});
});
forceRender();
}
function applyFromProps() {
setRecordFromProps();
applyRecord(record.current);
}
const didMount = useRef(false);
// Value updates must happen synchonously to avoid overwriting newer values.
useLayoutEffect(() => {
if (didMount.current && value !== _value.current) {
applyFromProps();
forceRender();
}
}, [value]);
// Value updates must happen synchonously to avoid overwriting newer values.
useLayoutEffect(() => {
if (!hadSelectionUpdate.current) {
return;
}
if (ref.current.ownerDocument.activeElement !== ref.current) {
ref.current.focus();
}
applyRecord(record.current);
hadSelectionUpdate.current = false;
}, [hadSelectionUpdate.current]);
const mergedRefs = useMergeRefs([ref, useDefaultStyle(), useBoundaryStyle({
record
}), useEventListeners({
record,
handleChange,
applyRecord,
createRecord,
isSelected,
onSelectionChange,
forceRender
}), useRefEffect(() => {
applyFromProps();
didMount.current = true;
}, [placeholder, ...__unstableDependencies])]);
return {
value: record.current,
// A function to get the most recent value so event handlers in
// useRichText implementations have access to it. For example when
// listening to input events, we internally update the state, but this
// state is not yet available to the input event handler because React
// may re-render asynchronously.
getValue: () => record.current,
onChange: handleChange,
ref: mergedRefs
};
}
export default function __experimentalRichText() {}
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,75 @@
/**
* WordPress dependencies
*/
import { useMemo } from '@wordpress/element';
import deprecated from '@wordpress/deprecated';
/**
* Internal dependencies
*/
import { getActiveFormat } from '../get-active-format';
/**
* @template T
* @typedef {import('@wordpress/element').RefObject<T>} RefObject<T>
*/
/** @typedef {import('../register-format-type').WPFormat} WPFormat */
/** @typedef {import('../types').RichTextValue} RichTextValue */
/**
* This hook, to be used in a format type's Edit component, returns the active
* element that is formatted, or the selection range if no format is active.
* The returned value is meant to be used for positioning UI, e.g. by passing it
* to the `Popover` component.
*
* @param {Object} $1 Named parameters.
* @param {RefObject<HTMLElement>} $1.ref React ref of the element
* containing the editable content.
* @param {RichTextValue} $1.value Value to check for selection.
* @param {WPFormat} $1.settings The format type's settings.
*
* @return {Element|Range} The active element or selection range.
*/
export function useAnchorRef({
ref,
value,
settings = {}
}) {
deprecated('`useAnchorRef` hook', {
since: '6.1',
alternative: '`useAnchor` hook'
});
const {
tagName,
className,
name
} = settings;
const activeFormat = name ? getActiveFormat(value, name) : undefined;
return useMemo(() => {
if (!ref.current) {
return;
}
const {
ownerDocument: {
defaultView
}
} = ref.current;
const selection = defaultView.getSelection();
if (!selection.rangeCount) {
return;
}
const range = selection.getRangeAt(0);
if (!activeFormat) {
return range;
}
let element = range.startContainer;
// If the caret is right before the element, select the next element.
element = element.nextElementSibling || element;
while (element.nodeType !== element.ELEMENT_NODE) {
element = element.parentNode;
}
return element.closest(tagName + (className ? '.' + className : ''));
}, [activeFormat, value.start, value.end, tagName, className]);
}
//# sourceMappingURL=use-anchor-ref.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["useMemo","deprecated","getActiveFormat","useAnchorRef","ref","value","settings","since","alternative","tagName","className","name","activeFormat","undefined","current","ownerDocument","defaultView","selection","getSelection","rangeCount","range","getRangeAt","element","startContainer","nextElementSibling","nodeType","ELEMENT_NODE","parentNode","closest","start","end"],"sources":["@wordpress/rich-text/src/component/use-anchor-ref.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { useMemo } from '@wordpress/element';\nimport deprecated from '@wordpress/deprecated';\n\n/**\n * Internal dependencies\n */\nimport { getActiveFormat } from '../get-active-format';\n\n/**\n * @template T\n * @typedef {import('@wordpress/element').RefObject<T>} RefObject<T>\n */\n/** @typedef {import('../register-format-type').WPFormat} WPFormat */\n/** @typedef {import('../types').RichTextValue} RichTextValue */\n\n/**\n * This hook, to be used in a format type's Edit component, returns the active\n * element that is formatted, or the selection range if no format is active.\n * The returned value is meant to be used for positioning UI, e.g. by passing it\n * to the `Popover` component.\n *\n * @param {Object} $1 Named parameters.\n * @param {RefObject<HTMLElement>} $1.ref React ref of the element\n * containing the editable content.\n * @param {RichTextValue} $1.value Value to check for selection.\n * @param {WPFormat} $1.settings The format type's settings.\n *\n * @return {Element|Range} The active element or selection range.\n */\nexport function useAnchorRef( { ref, value, settings = {} } ) {\n\tdeprecated( '`useAnchorRef` hook', {\n\t\tsince: '6.1',\n\t\talternative: '`useAnchor` hook',\n\t} );\n\n\tconst { tagName, className, name } = settings;\n\tconst activeFormat = name ? getActiveFormat( value, name ) : undefined;\n\n\treturn useMemo( () => {\n\t\tif ( ! ref.current ) {\n\t\t\treturn;\n\t\t}\n\t\tconst {\n\t\t\townerDocument: { defaultView },\n\t\t} = ref.current;\n\t\tconst selection = defaultView.getSelection();\n\n\t\tif ( ! selection.rangeCount ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst range = selection.getRangeAt( 0 );\n\n\t\tif ( ! activeFormat ) {\n\t\t\treturn range;\n\t\t}\n\n\t\tlet element = range.startContainer;\n\n\t\t// If the caret is right before the element, select the next element.\n\t\telement = element.nextElementSibling || element;\n\n\t\twhile ( element.nodeType !== element.ELEMENT_NODE ) {\n\t\t\telement = element.parentNode;\n\t\t}\n\n\t\treturn element.closest(\n\t\t\ttagName + ( className ? '.' + className : '' )\n\t\t);\n\t}, [ activeFormat, value.start, value.end, tagName, className ] );\n}\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,OAAO,QAAQ,oBAAoB;AAC5C,OAAOC,UAAU,MAAM,uBAAuB;;AAE9C;AACA;AACA;AACA,SAASC,eAAe,QAAQ,sBAAsB;;AAEtD;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,YAAYA,CAAE;EAAEC,GAAG;EAAEC,KAAK;EAAEC,QAAQ,GAAG,CAAC;AAAE,CAAC,EAAG;EAC7DL,UAAU,CAAE,qBAAqB,EAAE;IAClCM,KAAK,EAAE,KAAK;IACZC,WAAW,EAAE;EACd,CAAE,CAAC;EAEH,MAAM;IAAEC,OAAO;IAAEC,SAAS;IAAEC;EAAK,CAAC,GAAGL,QAAQ;EAC7C,MAAMM,YAAY,GAAGD,IAAI,GAAGT,eAAe,CAAEG,KAAK,EAAEM,IAAK,CAAC,GAAGE,SAAS;EAEtE,OAAOb,OAAO,CAAE,MAAM;IACrB,IAAK,CAAEI,GAAG,CAACU,OAAO,EAAG;MACpB;IACD;IACA,MAAM;MACLC,aAAa,EAAE;QAAEC;MAAY;IAC9B,CAAC,GAAGZ,GAAG,CAACU,OAAO;IACf,MAAMG,SAAS,GAAGD,WAAW,CAACE,YAAY,CAAC,CAAC;IAE5C,IAAK,CAAED,SAAS,CAACE,UAAU,EAAG;MAC7B;IACD;IAEA,MAAMC,KAAK,GAAGH,SAAS,CAACI,UAAU,CAAE,CAAE,CAAC;IAEvC,IAAK,CAAET,YAAY,EAAG;MACrB,OAAOQ,KAAK;IACb;IAEA,IAAIE,OAAO,GAAGF,KAAK,CAACG,cAAc;;IAElC;IACAD,OAAO,GAAGA,OAAO,CAACE,kBAAkB,IAAIF,OAAO;IAE/C,OAAQA,OAAO,CAACG,QAAQ,KAAKH,OAAO,CAACI,YAAY,EAAG;MACnDJ,OAAO,GAAGA,OAAO,CAACK,UAAU;IAC7B;IAEA,OAAOL,OAAO,CAACM,OAAO,CACrBnB,OAAO,IAAKC,SAAS,GAAG,GAAG,GAAGA,SAAS,GAAG,EAAE,CAC7C,CAAC;EACF,CAAC,EAAE,CAAEE,YAAY,EAAEP,KAAK,CAACwB,KAAK,EAAExB,KAAK,CAACyB,GAAG,EAAErB,OAAO,EAAEC,SAAS,CAAG,CAAC;AAClE","ignoreList":[]}

View File

@@ -0,0 +1,184 @@
/**
* WordPress dependencies
*/
import { usePrevious } from '@wordpress/compose';
import { useState, useLayoutEffect } from '@wordpress/element';
/** @typedef {import('../register-format-type').WPFormat} WPFormat */
/** @typedef {import('../types').RichTextValue} RichTextValue */
/**
* Given a range and a format tag name and class name, returns the closest
* format element.
*
* @param {Range} range The Range to check.
* @param {HTMLElement} editableContentElement The editable wrapper.
* @param {string} tagName The tag name of the format element.
* @param {string} className The class name of the format element.
*
* @return {HTMLElement|undefined} The format element, if found.
*/
function getFormatElement(range, editableContentElement, tagName, className) {
let element = range.startContainer;
// Even if the active format is defined, the actualy DOM range's start
// container may be outside of the format's DOM element:
// `a‸<strong>b</strong>` (DOM) while visually it's `a<strong>‸b</strong>`.
// So at a given selection index, start with the deepest format DOM element.
if (element.nodeType === element.TEXT_NODE && range.startOffset === element.length && element.nextSibling) {
element = element.nextSibling;
while (element.firstChild) {
element = element.firstChild;
}
}
if (element.nodeType !== element.ELEMENT_NODE) {
element = element.parentElement;
}
if (!element) {
return;
}
if (element === editableContentElement) {
return;
}
if (!editableContentElement.contains(element)) {
return;
}
const selector = tagName + (className ? '.' + className : '');
// .closest( selector ), but with a boundary. Check if the element matches
// the selector. If it doesn't match, try the parent element if it's not the
// editable wrapper. We don't want to try to match ancestors of the editable
// wrapper, which is what .closest( selector ) would do. When the element is
// the editable wrapper (which is most likely the case because most text is
// unformatted), this never runs.
while (element !== editableContentElement) {
if (element.matches(selector)) {
return element;
}
element = element.parentElement;
}
}
/**
* @typedef {Object} VirtualAnchorElement
* @property {() => DOMRect} getBoundingClientRect A function returning a DOMRect
* @property {HTMLElement} contextElement The actual DOM element
*/
/**
* Creates a virtual anchor element for a range.
*
* @param {Range} range The range to create a virtual anchor element for.
* @param {HTMLElement} editableContentElement The editable wrapper.
*
* @return {VirtualAnchorElement} The virtual anchor element.
*/
function createVirtualAnchorElement(range, editableContentElement) {
return {
contextElement: editableContentElement,
getBoundingClientRect() {
return editableContentElement.contains(range.startContainer) ? range.getBoundingClientRect() : editableContentElement.getBoundingClientRect();
}
};
}
/**
* Get the anchor: a format element if there is a matching one based on the
* tagName and className or a range otherwise.
*
* @param {HTMLElement} editableContentElement The editable wrapper.
* @param {string} tagName The tag name of the format
* element.
* @param {string} className The class name of the format
* element.
*
* @return {HTMLElement|VirtualAnchorElement|undefined} The anchor.
*/
function getAnchor(editableContentElement, tagName, className) {
if (!editableContentElement) {
return;
}
const {
ownerDocument
} = editableContentElement;
const {
defaultView
} = ownerDocument;
const selection = defaultView.getSelection();
if (!selection) {
return;
}
if (!selection.rangeCount) {
return;
}
const range = selection.getRangeAt(0);
if (!range || !range.startContainer) {
return;
}
const formatElement = getFormatElement(range, editableContentElement, tagName, className);
if (formatElement) {
return formatElement;
}
return createVirtualAnchorElement(range, editableContentElement);
}
/**
* This hook, to be used in a format type's Edit component, returns the active
* element that is formatted, or a virtual element for the selection range if
* no format is active. The returned value is meant to be used for positioning
* UI, e.g. by passing it to the `Popover` component via the `anchor` prop.
*
* @param {Object} $1 Named parameters.
* @param {HTMLElement|null} $1.editableContentElement The element containing
* the editable content.
* @param {WPFormat=} $1.settings The format type's settings.
* @return {Element|VirtualAnchorElement|undefined|null} The active element or selection range.
*/
export function useAnchor({
editableContentElement,
settings = {}
}) {
const {
tagName,
className,
isActive
} = settings;
const [anchor, setAnchor] = useState(() => getAnchor(editableContentElement, tagName, className));
const wasActive = usePrevious(isActive);
useLayoutEffect(() => {
if (!editableContentElement) {
return;
}
function callback() {
setAnchor(getAnchor(editableContentElement, tagName, className));
}
function attach() {
ownerDocument.addEventListener('selectionchange', callback);
}
function detach() {
ownerDocument.removeEventListener('selectionchange', callback);
}
const {
ownerDocument
} = editableContentElement;
if (editableContentElement === ownerDocument.activeElement ||
// When a link is created, we need to attach the popover to the newly created anchor.
!wasActive && isActive ||
// Sometimes we're _removing_ an active anchor, such as the inline color popover.
// When we add the color, it switches from a virtual anchor to a `<mark>` element.
// When we _remove_ the color, it switches from a `<mark>` element to a virtual anchor.
wasActive && !isActive) {
setAnchor(getAnchor(editableContentElement, tagName, className));
attach();
}
editableContentElement.addEventListener('focusin', attach);
editableContentElement.addEventListener('focusout', detach);
return () => {
detach();
editableContentElement.removeEventListener('focusin', attach);
editableContentElement.removeEventListener('focusout', detach);
};
}, [editableContentElement, tagName, className, isActive, wasActive]);
return anchor;
}
//# sourceMappingURL=use-anchor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,55 @@
/**
* WordPress dependencies
*/
import { useEffect, useRef } from '@wordpress/element';
/*
* Calculates and renders the format boundary style when the active formats
* change.
*/
export function useBoundaryStyle({
record
}) {
const ref = useRef();
const {
activeFormats = [],
replacements,
start
} = record.current;
const activeReplacement = replacements[start];
useEffect(() => {
// There's no need to recalculate the boundary styles if no formats are
// active, because no boundary styles will be visible.
if ((!activeFormats || !activeFormats.length) && !activeReplacement) {
return;
}
const boundarySelector = '*[data-rich-text-format-boundary]';
const element = ref.current.querySelector(boundarySelector);
if (!element) {
return;
}
const {
ownerDocument
} = element;
const {
defaultView
} = ownerDocument;
const computedStyle = defaultView.getComputedStyle(element);
const newColor = computedStyle.color.replace(')', ', 0.2)').replace('rgb', 'rgba');
const selector = `.rich-text:focus ${boundarySelector}`;
const rule = `background-color: ${newColor}`;
const style = `${selector} {${rule}}`;
const globalStyleId = 'rich-text-boundary-style';
let globalStyle = ownerDocument.getElementById(globalStyleId);
if (!globalStyle) {
globalStyle = ownerDocument.createElement('style');
globalStyle.id = globalStyleId;
ownerDocument.head.appendChild(globalStyle);
}
if (globalStyle.innerHTML !== style) {
globalStyle.innerHTML = style;
}
}, [activeFormats, activeReplacement]);
return ref;
}
//# sourceMappingURL=use-boundary-style.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["useEffect","useRef","useBoundaryStyle","record","ref","activeFormats","replacements","start","current","activeReplacement","length","boundarySelector","element","querySelector","ownerDocument","defaultView","computedStyle","getComputedStyle","newColor","color","replace","selector","rule","style","globalStyleId","globalStyle","getElementById","createElement","id","head","appendChild","innerHTML"],"sources":["@wordpress/rich-text/src/component/use-boundary-style.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { useEffect, useRef } from '@wordpress/element';\n\n/*\n * Calculates and renders the format boundary style when the active formats\n * change.\n */\nexport function useBoundaryStyle( { record } ) {\n\tconst ref = useRef();\n\tconst { activeFormats = [], replacements, start } = record.current;\n\tconst activeReplacement = replacements[ start ];\n\tuseEffect( () => {\n\t\t// There's no need to recalculate the boundary styles if no formats are\n\t\t// active, because no boundary styles will be visible.\n\t\tif (\n\t\t\t( ! activeFormats || ! activeFormats.length ) &&\n\t\t\t! activeReplacement\n\t\t) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst boundarySelector = '*[data-rich-text-format-boundary]';\n\t\tconst element = ref.current.querySelector( boundarySelector );\n\n\t\tif ( ! element ) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst { ownerDocument } = element;\n\t\tconst { defaultView } = ownerDocument;\n\t\tconst computedStyle = defaultView.getComputedStyle( element );\n\t\tconst newColor = computedStyle.color\n\t\t\t.replace( ')', ', 0.2)' )\n\t\t\t.replace( 'rgb', 'rgba' );\n\t\tconst selector = `.rich-text:focus ${ boundarySelector }`;\n\t\tconst rule = `background-color: ${ newColor }`;\n\t\tconst style = `${ selector } {${ rule }}`;\n\t\tconst globalStyleId = 'rich-text-boundary-style';\n\n\t\tlet globalStyle = ownerDocument.getElementById( globalStyleId );\n\n\t\tif ( ! globalStyle ) {\n\t\t\tglobalStyle = ownerDocument.createElement( 'style' );\n\t\t\tglobalStyle.id = globalStyleId;\n\t\t\townerDocument.head.appendChild( globalStyle );\n\t\t}\n\n\t\tif ( globalStyle.innerHTML !== style ) {\n\t\t\tglobalStyle.innerHTML = style;\n\t\t}\n\t}, [ activeFormats, activeReplacement ] );\n\treturn ref;\n}\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,SAAS,EAAEC,MAAM,QAAQ,oBAAoB;;AAEtD;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAAE;EAAEC;AAAO,CAAC,EAAG;EAC9C,MAAMC,GAAG,GAAGH,MAAM,CAAC,CAAC;EACpB,MAAM;IAAEI,aAAa,GAAG,EAAE;IAAEC,YAAY;IAAEC;EAAM,CAAC,GAAGJ,MAAM,CAACK,OAAO;EAClE,MAAMC,iBAAiB,GAAGH,YAAY,CAAEC,KAAK,CAAE;EAC/CP,SAAS,CAAE,MAAM;IAChB;IACA;IACA,IACC,CAAE,CAAEK,aAAa,IAAI,CAAEA,aAAa,CAACK,MAAM,KAC3C,CAAED,iBAAiB,EAClB;MACD;IACD;IAEA,MAAME,gBAAgB,GAAG,mCAAmC;IAC5D,MAAMC,OAAO,GAAGR,GAAG,CAACI,OAAO,CAACK,aAAa,CAAEF,gBAAiB,CAAC;IAE7D,IAAK,CAAEC,OAAO,EAAG;MAChB;IACD;IAEA,MAAM;MAAEE;IAAc,CAAC,GAAGF,OAAO;IACjC,MAAM;MAAEG;IAAY,CAAC,GAAGD,aAAa;IACrC,MAAME,aAAa,GAAGD,WAAW,CAACE,gBAAgB,CAAEL,OAAQ,CAAC;IAC7D,MAAMM,QAAQ,GAAGF,aAAa,CAACG,KAAK,CAClCC,OAAO,CAAE,GAAG,EAAE,QAAS,CAAC,CACxBA,OAAO,CAAE,KAAK,EAAE,MAAO,CAAC;IAC1B,MAAMC,QAAQ,GAAI,oBAAoBV,gBAAkB,EAAC;IACzD,MAAMW,IAAI,GAAI,qBAAqBJ,QAAU,EAAC;IAC9C,MAAMK,KAAK,GAAI,GAAGF,QAAU,KAAKC,IAAM,GAAE;IACzC,MAAME,aAAa,GAAG,0BAA0B;IAEhD,IAAIC,WAAW,GAAGX,aAAa,CAACY,cAAc,CAAEF,aAAc,CAAC;IAE/D,IAAK,CAAEC,WAAW,EAAG;MACpBA,WAAW,GAAGX,aAAa,CAACa,aAAa,CAAE,OAAQ,CAAC;MACpDF,WAAW,CAACG,EAAE,GAAGJ,aAAa;MAC9BV,aAAa,CAACe,IAAI,CAACC,WAAW,CAAEL,WAAY,CAAC;IAC9C;IAEA,IAAKA,WAAW,CAACM,SAAS,KAAKR,KAAK,EAAG;MACtCE,WAAW,CAACM,SAAS,GAAGR,KAAK;IAC9B;EACD,CAAC,EAAE,CAAElB,aAAa,EAAEI,iBAAiB,CAAG,CAAC;EACzC,OAAOL,GAAG;AACX","ignoreList":[]}

View File

@@ -0,0 +1,42 @@
/**
* WordPress dependencies
*/
import { useCallback } from '@wordpress/element';
/**
* In HTML, leading and trailing spaces are not visible, and multiple spaces
* elsewhere are visually reduced to one space. This rule prevents spaces from
* collapsing so all space is visible in the editor and can be removed. It also
* prevents some browsers from inserting non-breaking spaces at the end of a
* line to prevent the space from visually disappearing. Sometimes these non
* breaking spaces can linger in the editor causing unwanted non breaking spaces
* in between words. If also prevent Firefox from inserting a trailing `br` node
* to visualise any trailing space, causing the element to be saved.
*
* > Authors are encouraged to set the 'white-space' property on editing hosts
* > and on markup that was originally created through these editing mechanisms
* > to the value 'pre-wrap'. Default HTML whitespace handling is not well
* > suited to WYSIWYG editing, and line wrapping will not work correctly in
* > some corner cases if 'white-space' is left at its default value.
*
* https://html.spec.whatwg.org/multipage/interaction.html#best-practices-for-in-page-editors
*
* @type {string}
*/
const whiteSpace = 'pre-wrap';
/**
* A minimum width of 1px will prevent the rich text container from collapsing
* to 0 width and hiding the caret. This is useful for inline containers.
*/
const minWidth = '1px';
export function useDefaultStyle() {
return useCallback(element => {
if (!element) {
return;
}
element.style.whiteSpace = whiteSpace;
element.style.minWidth = minWidth;
}, []);
}
//# sourceMappingURL=use-default-style.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":["useCallback","whiteSpace","minWidth","useDefaultStyle","element","style"],"sources":["@wordpress/rich-text/src/component/use-default-style.js"],"sourcesContent":["/**\n * WordPress dependencies\n */\nimport { useCallback } from '@wordpress/element';\n\n/**\n * In HTML, leading and trailing spaces are not visible, and multiple spaces\n * elsewhere are visually reduced to one space. This rule prevents spaces from\n * collapsing so all space is visible in the editor and can be removed. It also\n * prevents some browsers from inserting non-breaking spaces at the end of a\n * line to prevent the space from visually disappearing. Sometimes these non\n * breaking spaces can linger in the editor causing unwanted non breaking spaces\n * in between words. If also prevent Firefox from inserting a trailing `br` node\n * to visualise any trailing space, causing the element to be saved.\n *\n * > Authors are encouraged to set the 'white-space' property on editing hosts\n * > and on markup that was originally created through these editing mechanisms\n * > to the value 'pre-wrap'. Default HTML whitespace handling is not well\n * > suited to WYSIWYG editing, and line wrapping will not work correctly in\n * > some corner cases if 'white-space' is left at its default value.\n *\n * https://html.spec.whatwg.org/multipage/interaction.html#best-practices-for-in-page-editors\n *\n * @type {string}\n */\nconst whiteSpace = 'pre-wrap';\n\n/**\n * A minimum width of 1px will prevent the rich text container from collapsing\n * to 0 width and hiding the caret. This is useful for inline containers.\n */\nconst minWidth = '1px';\n\nexport function useDefaultStyle() {\n\treturn useCallback( ( element ) => {\n\t\tif ( ! element ) {\n\t\t\treturn;\n\t\t}\n\t\telement.style.whiteSpace = whiteSpace;\n\t\telement.style.minWidth = minWidth;\n\t}, [] );\n}\n"],"mappings":"AAAA;AACA;AACA;AACA,SAASA,WAAW,QAAQ,oBAAoB;;AAEhD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,UAAU,GAAG,UAAU;;AAE7B;AACA;AACA;AACA;AACA,MAAMC,QAAQ,GAAG,KAAK;AAEtB,OAAO,SAASC,eAAeA,CAAA,EAAG;EACjC,OAAOH,WAAW,CAAII,OAAO,IAAM;IAClC,IAAK,CAAEA,OAAO,EAAG;MAChB;IACD;IACAA,OAAO,CAACC,KAAK,CAACJ,UAAU,GAAGA,UAAU;IACrCG,OAAO,CAACC,KAAK,CAACH,QAAQ,GAAGA,QAAQ;EAClC,CAAC,EAAE,EAAG,CAAC;AACR","ignoreList":[]}