@wordpress/rich-text
Advanced tools
Comparing version 3.18.0 to 3.18.1-rc.0
import _extends from "@babel/runtime/helpers/esm/extends"; | ||
import _defineProperty from "@babel/runtime/helpers/esm/defineProperty"; | ||
import _classCallCheck from "@babel/runtime/helpers/esm/classCallCheck"; | ||
import _createClass from "@babel/runtime/helpers/esm/createClass"; | ||
import _assertThisInitialized from "@babel/runtime/helpers/esm/assertThisInitialized"; | ||
import _possibleConstructorReturn from "@babel/runtime/helpers/esm/possibleConstructorReturn"; | ||
import _getPrototypeOf from "@babel/runtime/helpers/esm/getPrototypeOf"; | ||
import _inherits from "@babel/runtime/helpers/esm/inherits"; | ||
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray"; | ||
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties"; | ||
import { createElement, Fragment } from "@wordpress/element"; | ||
@@ -15,6 +11,2 @@ | ||
function _createSuper(Derived) { return function () { var Super = _getPrototypeOf(Derived), result; if (_isNativeReflectConstruct()) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } | ||
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } | ||
/** | ||
@@ -29,6 +21,4 @@ * External dependencies | ||
import { Component, forwardRef } from '@wordpress/element'; | ||
import { forwardRef, useEffect, useRef, useState, useMemo, useLayoutEffect } from '@wordpress/element'; | ||
import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, SPACE, ESCAPE } from '@wordpress/keycodes'; | ||
import { withSafeTimeout, compose } from '@wordpress/compose'; | ||
import isShallowEqual from '@wordpress/is-shallow-equal'; | ||
import deprecated from '@wordpress/deprecated'; | ||
@@ -53,4 +43,4 @@ /** | ||
import withFormatTypes from './with-format-types'; | ||
import { BoundaryStyle } from './boundary-style'; | ||
import { InlineWarning } from './inline-warning'; | ||
import { useBoundaryStyle } from './use-boundary-style'; | ||
import { useInlineWarning } from './use-inline-warning'; | ||
import { insert } from '../insert'; | ||
@@ -140,1082 +130,980 @@ /** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ | ||
} | ||
/** | ||
* See export statement below. | ||
*/ | ||
function RichText(_ref) { | ||
var _ref$tagName = _ref.tagName, | ||
TagName = _ref$tagName === void 0 ? 'div' : _ref$tagName, | ||
_ref$value = _ref.value, | ||
value = _ref$value === void 0 ? '' : _ref$value, | ||
selectionStart = _ref.selectionStart, | ||
selectionEnd = _ref.selectionEnd, | ||
children = _ref.children, | ||
allowedFormats = _ref.allowedFormats, | ||
withoutInteractiveFormatting = _ref.withoutInteractiveFormatting, | ||
formatTypes = _ref.formatTypes, | ||
style = _ref.style, | ||
className = _ref.className, | ||
placeholder = _ref.placeholder, | ||
disabled = _ref.disabled, | ||
preserveWhiteSpace = _ref.preserveWhiteSpace, | ||
onPaste = _ref.onPaste, | ||
_ref$format = _ref.format, | ||
format = _ref$format === void 0 ? 'string' : _ref$format, | ||
onDelete = _ref.onDelete, | ||
onEnter = _ref.onEnter, | ||
onSelectionChange = _ref.onSelectionChange, | ||
onChange = _ref.onChange, | ||
onFocus = _ref.unstableOnFocus, | ||
setFocusedElement = _ref.setFocusedElement, | ||
instanceId = _ref.instanceId, | ||
multilineTag = _ref.__unstableMultilineTag, | ||
multilineRootTag = _ref.__unstableMultilineRootTag, | ||
disableFormats = _ref.__unstableDisableFormats, | ||
didAutomaticChange = _ref.__unstableDidAutomaticChange, | ||
inputRule = _ref.__unstableInputRule, | ||
markAutomaticChange = _ref.__unstableMarkAutomaticChange, | ||
allowPrefixTransformations = _ref.__unstableAllowPrefixTransformations, | ||
undo = _ref.__unstableUndo, | ||
isCaretWithinFormattedText = _ref.__unstableIsCaretWithinFormattedText, | ||
onEnterFormattedText = _ref.__unstableOnEnterFormattedText, | ||
onExitFormattedText = _ref.__unstableOnExitFormattedText, | ||
onCreateUndoLevel = _ref.__unstableOnCreateUndoLevel, | ||
isSelected = _ref.__unstableIsSelected, | ||
ref = _ref.forwardedRef, | ||
remainingProps = _objectWithoutProperties(_ref, ["tagName", "value", "selectionStart", "selectionEnd", "children", "allowedFormats", "withoutInteractiveFormatting", "formatTypes", "style", "className", "placeholder", "disabled", "preserveWhiteSpace", "onPaste", "format", "onDelete", "onEnter", "onSelectionChange", "onChange", "unstableOnFocus", "setFocusedElement", "instanceId", "__unstableMultilineTag", "__unstableMultilineRootTag", "__unstableDisableFormats", "__unstableDidAutomaticChange", "__unstableInputRule", "__unstableMarkAutomaticChange", "__unstableAllowPrefixTransformations", "__unstableUndo", "__unstableIsCaretWithinFormattedText", "__unstableOnEnterFormattedText", "__unstableOnExitFormattedText", "__unstableOnCreateUndoLevel", "__unstableIsSelected", "forwardedRef"]); | ||
var RichText = /*#__PURE__*/function (_Component) { | ||
_inherits(RichText, _Component); | ||
var _useState = useState(), | ||
_useState2 = _slicedToArray(_useState, 2), | ||
_useState2$ = _useState2[0], | ||
activeFormats = _useState2$ === void 0 ? [] : _useState2$, | ||
setActiveFormats = _useState2[1]; | ||
var _super = _createSuper(RichText); | ||
function getDoc() { | ||
return ref.current.ownerDocument; | ||
} | ||
function RichText(_ref) { | ||
var _this; | ||
function getWin() { | ||
return getDoc().defaultView; | ||
} | ||
/** | ||
* Converts the outside data structure to our internal representation. | ||
* | ||
* @param {*} string The outside value, data type depends on props. | ||
* | ||
* @return {Object} An internal rich-text value. | ||
*/ | ||
var value = _ref.value, | ||
selectionStart = _ref.selectionStart, | ||
selectionEnd = _ref.selectionEnd; | ||
_classCallCheck(this, RichText); | ||
function formatToValue(string) { | ||
if (disableFormats) { | ||
return { | ||
text: string, | ||
formats: Array(string.length), | ||
replacements: Array(string.length) | ||
}; | ||
} | ||
_this = _super.apply(this, arguments); | ||
_this.getDocument = _this.getDocument.bind(_assertThisInitialized(_this)); | ||
_this.getWindow = _this.getWindow.bind(_assertThisInitialized(_this)); | ||
_this.onFocus = _this.onFocus.bind(_assertThisInitialized(_this)); | ||
_this.onBlur = _this.onBlur.bind(_assertThisInitialized(_this)); | ||
_this.onChange = _this.onChange.bind(_assertThisInitialized(_this)); | ||
_this.handleDelete = _this.handleDelete.bind(_assertThisInitialized(_this)); | ||
_this.handleEnter = _this.handleEnter.bind(_assertThisInitialized(_this)); | ||
_this.handleSpace = _this.handleSpace.bind(_assertThisInitialized(_this)); | ||
_this.handleHorizontalNavigation = _this.handleHorizontalNavigation.bind(_assertThisInitialized(_this)); | ||
_this.onPaste = _this.onPaste.bind(_assertThisInitialized(_this)); | ||
_this.onCreateUndoLevel = _this.onCreateUndoLevel.bind(_assertThisInitialized(_this)); | ||
_this.onInput = _this.onInput.bind(_assertThisInitialized(_this)); | ||
_this.onCompositionStart = _this.onCompositionStart.bind(_assertThisInitialized(_this)); | ||
_this.onCompositionEnd = _this.onCompositionEnd.bind(_assertThisInitialized(_this)); | ||
_this.onSelectionChange = _this.onSelectionChange.bind(_assertThisInitialized(_this)); | ||
_this.createRecord = _this.createRecord.bind(_assertThisInitialized(_this)); | ||
_this.applyRecord = _this.applyRecord.bind(_assertThisInitialized(_this)); | ||
_this.valueToFormat = _this.valueToFormat.bind(_assertThisInitialized(_this)); | ||
_this.onPointerDown = _this.onPointerDown.bind(_assertThisInitialized(_this)); | ||
_this.formatToValue = _this.formatToValue.bind(_assertThisInitialized(_this)); | ||
_this.Editable = _this.Editable.bind(_assertThisInitialized(_this)); | ||
if (format !== 'string') { | ||
return string; | ||
} | ||
_this.onKeyDown = function (event) { | ||
if (event.defaultPrevented) { | ||
return; | ||
} | ||
var prepare = createPrepareEditableTree(remainingProps, 'format_value_functions'); | ||
var result = create({ | ||
html: string, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
result.formats = prepare(result); | ||
return result; | ||
} | ||
/** | ||
* Removes editor only formats from the value. | ||
* | ||
* Editor only formats are applied using `prepareEditableTree`, so we need to | ||
* remove them before converting the internal state | ||
* | ||
* @param {Object} val The internal rich-text value. | ||
* | ||
* @return {Object} A new rich-text value. | ||
*/ | ||
_this.handleDelete(event); | ||
_this.handleEnter(event); | ||
function removeEditorOnlyFormats(val) { | ||
formatTypes.forEach(function (formatType) { | ||
// Remove formats created by prepareEditableTree, because they are editor only. | ||
if (formatType.__experimentalCreatePrepareEditableTree) { | ||
val = removeFormat(val, formatType.name, 0, val.text.length); | ||
} | ||
}); | ||
return val; | ||
} | ||
/** | ||
* Converts the internal value to the external data format. | ||
* | ||
* @param {Object} val The internal rich-text value. | ||
* | ||
* @return {*} The external data format, data type depends on props. | ||
*/ | ||
_this.handleSpace(event); | ||
_this.handleHorizontalNavigation(event); | ||
}; | ||
function valueToFormat(val) { | ||
if (disableFormats) { | ||
return val.text; | ||
} | ||
_this.state = {}; | ||
_this.lastHistoryValue = value; // Internal values are updated synchronously, unlike props and state. | ||
val = removeEditorOnlyFormats(val); | ||
_this.value = value; | ||
_this.record = _this.formatToValue(value); | ||
_this.record.start = selectionStart; | ||
_this.record.end = selectionEnd; | ||
return _this; | ||
} | ||
_createClass(RichText, [{ | ||
key: "componentWillUnmount", | ||
value: function componentWillUnmount() { | ||
this.getDocument().removeEventListener('selectionchange', this.onSelectionChange); | ||
this.getWindow().cancelAnimationFrame(this.rafId); | ||
if (format !== 'string') { | ||
return; | ||
} | ||
}, { | ||
key: "componentDidMount", | ||
value: function componentDidMount() { | ||
this.applyRecord(this.record, { | ||
domOnly: true | ||
}); | ||
} | ||
}, { | ||
key: "getDocument", | ||
value: function getDocument() { | ||
return this.props.forwardedRef.current.ownerDocument; | ||
} | ||
}, { | ||
key: "getWindow", | ||
value: function getWindow() { | ||
return this.getDocument().defaultView; | ||
} | ||
}, { | ||
key: "createRecord", | ||
value: function createRecord() { | ||
var _this$props = this.props, | ||
multilineTag = _this$props.__unstableMultilineTag, | ||
forwardedRef = _this$props.forwardedRef, | ||
preserveWhiteSpace = _this$props.preserveWhiteSpace; | ||
var selection = this.getWindow().getSelection(); | ||
var range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; | ||
return create({ | ||
element: forwardedRef.current, | ||
range: range, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
__unstableIsEditableTree: true, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
} | ||
}, { | ||
key: "applyRecord", | ||
value: function applyRecord(record) { | ||
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
domOnly = _ref2.domOnly; | ||
var _this$props2 = this.props, | ||
multilineTag = _this$props2.__unstableMultilineTag, | ||
forwardedRef = _this$props2.forwardedRef; | ||
apply({ | ||
value: record, | ||
current: forwardedRef.current, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
prepareEditableTree: createPrepareEditableTree(this.props, 'format_prepare_functions'), | ||
__unstableDomOnly: domOnly, | ||
placeholder: this.props.placeholder | ||
}); | ||
} | ||
/** | ||
* Handles a paste event. | ||
* | ||
* Saves the pasted data as plain text in `pastedPlainText`. | ||
* | ||
* @param {ClipboardEvent} event The paste event. | ||
*/ | ||
return toHTMLString({ | ||
value: val, | ||
multilineTag: multilineTag, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
} // Internal values are updated synchronously, unlike props and state. | ||
}, { | ||
key: "onPaste", | ||
value: function onPaste(event) { | ||
var _this$props3 = this.props, | ||
formatTypes = _this$props3.formatTypes, | ||
onPaste = _this$props3.onPaste, | ||
isSelected = _this$props3.__unstableIsSelected, | ||
__unstableDisableFormats = _this$props3.__unstableDisableFormats; | ||
var _this$state$activeFor = this.state.activeFormats, | ||
activeFormats = _this$state$activeFor === void 0 ? [] : _this$state$activeFor; | ||
if (!isSelected) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
var _value = useRef(value); | ||
var clipboardData = event.clipboardData; | ||
var items = clipboardData.items, | ||
files = clipboardData.files; // In Edge these properties can be null instead of undefined, so a more | ||
// rigorous test is required over using default values. | ||
var record = useRef(useMemo(function () { | ||
var initialRecord = formatToValue(value); | ||
initialRecord.start = selectionStart; | ||
initialRecord.end = selectionEnd; | ||
return initialRecord; | ||
}, [])); | ||
items = isNil(items) ? [] : items; | ||
files = isNil(files) ? [] : files; | ||
var plainText = ''; | ||
var html = ''; // IE11 only supports `Text` as an argument for `getData` and will | ||
// otherwise throw an invalid argument error, so we try the standard | ||
// arguments first, then fallback to `Text` if they fail. | ||
function createRecord() { | ||
var selection = getWin().getSelection(); | ||
var range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; | ||
return create({ | ||
element: ref.current, | ||
range: range, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
__unstableIsEditableTree: true, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
} | ||
try { | ||
plainText = clipboardData.getData('text/plain'); | ||
html = clipboardData.getData('text/html'); | ||
} catch (error1) { | ||
try { | ||
html = clipboardData.getData('Text'); | ||
} catch (error2) { | ||
// Some browsers like UC Browser paste plain text by default and | ||
// don't support clipboardData at all, so allow default | ||
// behaviour. | ||
return; | ||
} | ||
} | ||
function applyRecord(newRecord) { | ||
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
domOnly = _ref2.domOnly; | ||
event.preventDefault(); // Allows us to ask for this information when we get a report. | ||
apply({ | ||
value: newRecord, | ||
current: ref.current, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
prepareEditableTree: createPrepareEditableTree(remainingProps, 'format_prepare_functions'), | ||
__unstableDomOnly: domOnly, | ||
placeholder: placeholder | ||
}); | ||
} | ||
/** | ||
* Handles a paste event. | ||
* | ||
* Saves the pasted data as plain text in `pastedPlainText`. | ||
* | ||
* @param {ClipboardEvent} event The paste event. | ||
*/ | ||
window.console.log('Received HTML:\n\n', html); | ||
window.console.log('Received plain text:\n\n', plainText); | ||
if (__unstableDisableFormats) { | ||
this.onChange(insert(this.record, plainText)); | ||
return; | ||
} | ||
function handlePaste(event) { | ||
if (!isSelected) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
var record = this.record; | ||
var transformed = formatTypes.reduce(function (accumlator, _ref3) { | ||
var __unstablePasteRule = _ref3.__unstablePasteRule; | ||
var clipboardData = event.clipboardData; | ||
var items = clipboardData.items, | ||
files = clipboardData.files; // In Edge these properties can be null instead of undefined, so a more | ||
// rigorous test is required over using default values. | ||
// Only allow one transform. | ||
if (__unstablePasteRule && accumlator === record) { | ||
accumlator = __unstablePasteRule(record, { | ||
html: html, | ||
plainText: plainText | ||
}); | ||
} | ||
items = isNil(items) ? [] : items; | ||
files = isNil(files) ? [] : files; | ||
var plainText = ''; | ||
var html = ''; // IE11 only supports `Text` as an argument for `getData` and will | ||
// otherwise throw an invalid argument error, so we try the standard | ||
// arguments first, then fallback to `Text` if they fail. | ||
return accumlator; | ||
}, record); | ||
if (transformed !== record) { | ||
this.onChange(transformed); | ||
try { | ||
plainText = clipboardData.getData('text/plain'); | ||
html = clipboardData.getData('text/html'); | ||
} catch (error1) { | ||
try { | ||
html = clipboardData.getData('Text'); | ||
} catch (error2) { | ||
// Some browsers like UC Browser paste plain text by default and | ||
// don't support clipboardData at all, so allow default | ||
// behaviour. | ||
return; | ||
} | ||
} | ||
if (onPaste) { | ||
files = Array.from(files); | ||
Array.from(items).forEach(function (item) { | ||
if (!item.getAsFile) { | ||
return; | ||
} | ||
event.preventDefault(); // Allows us to ask for this information when we get a report. | ||
var file = item.getAsFile(); | ||
window.console.log('Received HTML:\n\n', html); | ||
window.console.log('Received plain text:\n\n', plainText); | ||
if (!file) { | ||
return; | ||
} | ||
if (disableFormats) { | ||
handleChange(insert(record.current, plainText)); | ||
return; | ||
} | ||
var name = file.name, | ||
type = file.type, | ||
size = file.size; | ||
var transformed = formatTypes.reduce(function (accumlator, _ref3) { | ||
var __unstablePasteRule = _ref3.__unstablePasteRule; | ||
if (!find(files, { | ||
name: name, | ||
type: type, | ||
size: size | ||
})) { | ||
files.push(file); | ||
} | ||
}); | ||
onPaste({ | ||
value: this.removeEditorOnlyFormats(record), | ||
onChange: this.onChange, | ||
// Only allow one transform. | ||
if (__unstablePasteRule && accumlator === record.current) { | ||
accumlator = __unstablePasteRule(record.current, { | ||
html: html, | ||
plainText: plainText, | ||
files: files, | ||
activeFormats: activeFormats | ||
plainText: plainText | ||
}); | ||
} | ||
return accumlator; | ||
}, record.current); | ||
if (transformed !== record.current) { | ||
handleChange(transformed); | ||
return; | ||
} | ||
/** | ||
* Handles a focus event on the contenteditable field, calling the | ||
* `unstableOnFocus` prop callback if one is defined. The callback does not | ||
* receive any arguments. | ||
* | ||
* This is marked as a private API and the `unstableOnFocus` prop is not | ||
* documented, as the current requirements where it is used are subject to | ||
* future refactoring following `isSelected` handling. | ||
* | ||
* In contrast with `setFocusedElement`, this is only triggered in response | ||
* to focus within the contenteditable field, whereas `setFocusedElement` | ||
* is triggered on focus within any `RichText` descendent element. | ||
* | ||
* @see setFocusedElement | ||
* | ||
* @private | ||
*/ | ||
}, { | ||
key: "onFocus", | ||
value: function onFocus() { | ||
var unstableOnFocus = this.props.unstableOnFocus; | ||
if (onPaste) { | ||
files = Array.from(files); | ||
Array.from(items).forEach(function (item) { | ||
if (!item.getAsFile) { | ||
return; | ||
} | ||
if (unstableOnFocus) { | ||
unstableOnFocus(); | ||
} | ||
var file = item.getAsFile(); | ||
if (!this.props.__unstableIsSelected) { | ||
// We know for certain that on focus, the old selection is invalid. It | ||
// will be recalculated on the next mouseup, keyup, or touchend event. | ||
var index = undefined; | ||
var activeFormats = EMPTY_ACTIVE_FORMATS; | ||
this.record = _objectSpread({}, this.record, { | ||
start: index, | ||
end: index, | ||
activeFormats: activeFormats | ||
}); | ||
this.props.onSelectionChange(index, index); | ||
this.setState({ | ||
activeFormats: activeFormats | ||
}); | ||
} else { | ||
this.props.onSelectionChange(this.record.start, this.record.end); | ||
this.setState({ | ||
activeFormats: getActiveFormats(_objectSpread({}, this.record, { | ||
activeFormats: undefined | ||
}), EMPTY_ACTIVE_FORMATS) | ||
}); | ||
} // Update selection as soon as possible, which is at the next animation | ||
// frame. The event listener for selection changes may be added too late | ||
// at this point, but this focus event is still too early to calculate | ||
// the selection. | ||
if (!file) { | ||
return; | ||
} | ||
var name = file.name, | ||
type = file.type, | ||
size = file.size; | ||
this.rafId = this.getWindow().requestAnimationFrame(this.onSelectionChange); | ||
this.getDocument().addEventListener('selectionchange', this.onSelectionChange); | ||
if (!find(files, { | ||
name: name, | ||
type: type, | ||
size: size | ||
})) { | ||
files.push(file); | ||
} | ||
}); | ||
onPaste({ | ||
value: removeEditorOnlyFormats(record.current), | ||
onChange: handleChange, | ||
html: html, | ||
plainText: plainText, | ||
files: files, | ||
activeFormats: activeFormats | ||
}); | ||
} | ||
} | ||
/** | ||
* Handles delete on keydown: | ||
* - outdent list items, | ||
* - delete content if everything is selected, | ||
* - trigger the onDelete prop when selection is uncollapsed and at an edge. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (this.props.setFocusedElement) { | ||
deprecated('wp.blockEditor.RichText setFocusedElement prop', { | ||
alternative: 'selection state from the block editor store.' | ||
}); | ||
this.props.setFocusedElement(this.props.instanceId); | ||
} | ||
function handleDelete(event) { | ||
var keyCode = event.keyCode; | ||
if (keyCode !== DELETE && keyCode !== BACKSPACE && keyCode !== ESCAPE) { | ||
return; | ||
} | ||
}, { | ||
key: "onBlur", | ||
value: function onBlur() { | ||
this.getDocument().removeEventListener('selectionchange', this.onSelectionChange); | ||
if (didAutomaticChange) { | ||
event.preventDefault(); | ||
undo(); | ||
return; | ||
} | ||
/** | ||
* Handle input on the next selection change event. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic input event. | ||
*/ | ||
}, { | ||
key: "onInput", | ||
value: 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 (this.isComposing) { | ||
return; | ||
} | ||
if (keyCode === ESCAPE) { | ||
return; | ||
} | ||
var inputType; | ||
var currentValue = createRecord(); | ||
var start = currentValue.start, | ||
end = currentValue.end, | ||
text = currentValue.text; | ||
var isReverse = keyCode === BACKSPACE; // Always handle full content deletion ourselves. | ||
if (event) { | ||
inputType = event.inputType; | ||
} | ||
if (start === 0 && end !== 0 && end === text.length) { | ||
handleChange(remove(currentValue)); | ||
event.preventDefault(); | ||
return; | ||
} | ||
if (!inputType && event && event.nativeEvent) { | ||
inputType = event.nativeEvent.inputType; | ||
} // The browser formatted something or tried to insert HTML. | ||
// Overwrite it. It will be handled later by the format library if | ||
// needed. | ||
if (multilineTag) { | ||
var newValue; // Check to see if we should remove the first item if empty. | ||
if (isReverse && currentValue.start === 0 && currentValue.end === 0 && isEmptyLine(currentValue)) { | ||
newValue = removeLineSeparator(currentValue, !isReverse); | ||
} else { | ||
newValue = removeLineSeparator(currentValue, isReverse); | ||
} | ||
if (inputType && (inputType.indexOf('format') === 0 || INSERTION_INPUT_TYPES_TO_IGNORE.has(inputType))) { | ||
this.applyRecord(this.record); | ||
if (newValue) { | ||
handleChange(newValue); | ||
event.preventDefault(); | ||
return; | ||
} | ||
} // Only process delete if the key press occurs at an uncollapsed edge. | ||
var value = this.createRecord(); | ||
var _this$record = this.record, | ||
start = _this$record.start, | ||
_this$record$activeFo = _this$record.activeFormats, | ||
activeFormats = _this$record$activeFo === void 0 ? [] : _this$record$activeFo; // Update the formats between the last and new caret position. | ||
var change = updateFormats({ | ||
value: value, | ||
start: start, | ||
end: value.start, | ||
formats: activeFormats | ||
}); | ||
this.onChange(change, { | ||
withoutHistory: true | ||
}); | ||
var _this$props4 = this.props, | ||
inputRule = _this$props4.__unstableInputRule, | ||
markAutomaticChange = _this$props4.__unstableMarkAutomaticChange, | ||
allowPrefixTransformations = _this$props4.__unstableAllowPrefixTransformations, | ||
formatTypes = _this$props4.formatTypes, | ||
setTimeout = _this$props4.setTimeout, | ||
clearTimeout = _this$props4.clearTimeout; // Create an undo level when input stops for over a second. | ||
if (!onDelete || !isCollapsed(currentValue) || activeFormats.length || isReverse && start !== 0 || !isReverse && end !== text.length) { | ||
return; | ||
} | ||
clearTimeout(this.onInput.timeout); | ||
this.onInput.timeout = setTimeout(this.onCreateUndoLevel, 1000); // Only run input rules when inserting text. | ||
onDelete({ | ||
isReverse: isReverse, | ||
value: currentValue | ||
}); | ||
event.preventDefault(); | ||
} | ||
/** | ||
* Triggers the `onEnter` prop on keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (inputType !== 'insertText') { | ||
return; | ||
} | ||
if (allowPrefixTransformations && inputRule) { | ||
inputRule(change, this.valueToFormat); | ||
} | ||
function handleEnter(event) { | ||
if (event.keyCode !== ENTER) { | ||
return; | ||
} | ||
var transformed = formatTypes.reduce(function (accumlator, _ref4) { | ||
var __unstableInputRule = _ref4.__unstableInputRule; | ||
event.preventDefault(); | ||
if (__unstableInputRule) { | ||
accumlator = __unstableInputRule(accumlator); | ||
} | ||
if (!onEnter) { | ||
return; | ||
} | ||
return accumlator; | ||
}, change); | ||
onEnter({ | ||
value: removeEditorOnlyFormats(createRecord()), | ||
onChange: handleChange, | ||
shiftKey: event.shiftKey | ||
}); | ||
} | ||
/** | ||
* Indents list items on space keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (transformed !== change) { | ||
this.onCreateUndoLevel(); | ||
this.onChange(_objectSpread({}, transformed, { | ||
activeFormats: activeFormats | ||
})); | ||
markAutomaticChange(); | ||
} | ||
function handleSpace(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
if ( // Only override when no modifiers are pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== SPACE || multilineTag !== 'li') { | ||
return; | ||
} | ||
}, { | ||
key: "onCompositionStart", | ||
value: function onCompositionStart() { | ||
this.isComposing = true; // Do not update the selection when characters are being composed as | ||
// this rerenders the component and might distroy internal browser | ||
// editing state. | ||
this.getDocument().removeEventListener('selectionchange', this.onSelectionChange); | ||
var currentValue = createRecord(); | ||
if (!isCollapsed(currentValue)) { | ||
return; | ||
} | ||
}, { | ||
key: "onCompositionEnd", | ||
value: function onCompositionEnd() { | ||
this.isComposing = false; // Ensure the value is up-to-date for browsers that don't emit a final | ||
// input event after composition. | ||
this.onInput({ | ||
inputType: 'insertText' | ||
}); // Tracking selection changes can be resumed. | ||
var text = currentValue.text, | ||
start = currentValue.start; | ||
var characterBefore = text[start - 1]; // The caret must be at the start of a line. | ||
this.getDocument().addEventListener('selectionchange', this.onSelectionChange); | ||
if (characterBefore && characterBefore !== LINE_SEPARATOR) { | ||
return; | ||
} | ||
/** | ||
* Syncs the selection to local state. A callback for the `selectionchange` | ||
* native events, `keyup`, `mouseup` and `touchend` synthetic events, and | ||
* animation frames after the `focus` event. | ||
* | ||
* @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event | ||
*/ | ||
}, { | ||
key: "onSelectionChange", | ||
value: function onSelectionChange(event) { | ||
if (event.type !== 'selectionchange' && !this.props.__unstableIsSelected) { | ||
return; | ||
} | ||
handleChange(indentListItems(currentValue, { | ||
type: multilineRootTag | ||
})); | ||
event.preventDefault(); | ||
} | ||
/** | ||
* Handles horizontal keyboard navigation when no modifiers are pressed. The | ||
* navigation is handled separately to move correctly around format | ||
* boundaries. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (this.props.disabled) { | ||
return; | ||
} // In case of a keyboard event, ignore selection changes during | ||
// composition. | ||
function handleHorizontalNavigation(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
if (this.isComposing) { | ||
return; | ||
} | ||
if ( // Only override left and right keys without modifiers pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== LEFT && keyCode !== RIGHT) { | ||
return; | ||
} | ||
var _this$createRecord = this.createRecord(), | ||
start = _this$createRecord.start, | ||
end = _this$createRecord.end, | ||
text = _this$createRecord.text; | ||
var _record$current = record.current, | ||
text = _record$current.text, | ||
formats = _record$current.formats, | ||
start = _record$current.start, | ||
end = _record$current.end, | ||
_record$current$activ = _record$current.activeFormats, | ||
currentActiveFormats = _record$current$activ === void 0 ? [] : _record$current$activ; | ||
var collapsed = isCollapsed(record.current); // To do: ideally, we should look at visual position instead. | ||
var value = this.record; // Fallback mechanism for IE11, which doesn't support the input event. | ||
// Any input results in a selection change. | ||
var _getWin$getComputedSt = getWin().getComputedStyle(ref.current), | ||
direction = _getWin$getComputedSt.direction; | ||
if (text !== value.text) { | ||
this.onInput(); | ||
var reverseKey = direction === 'rtl' ? RIGHT : LEFT; | ||
var 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 (start === value.start && end === value.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 (value.text.length === 0 && start === 0) { | ||
fixPlaceholderSelection(this.getWindow()); | ||
} | ||
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. | ||
var _this$props5 = this.props, | ||
isCaretWithinFormattedText = _this$props5.__unstableIsCaretWithinFormattedText, | ||
onEnterFormattedText = _this$props5.__unstableOnEnterFormattedText, | ||
onExitFormattedText = _this$props5.__unstableOnExitFormattedText; | ||
var newValue = _objectSpread({}, value, { | ||
start: start, | ||
end: end, | ||
// Allow `getActiveFormats` to get new `activeFormats`. | ||
activeFormats: undefined | ||
}); | ||
if (!collapsed) { | ||
return; | ||
} // In all other cases, prevent default behaviour. | ||
var activeFormats = getActiveFormats(newValue, EMPTY_ACTIVE_FORMATS); // Update the value with the new active formats. | ||
newValue.activeFormats = activeFormats; | ||
event.preventDefault(); | ||
var formatsBefore = formats[start - 1] || EMPTY_ACTIVE_FORMATS; | ||
var formatsAfter = formats[start] || EMPTY_ACTIVE_FORMATS; | ||
var newActiveFormatsLength = currentActiveFormats.length; | ||
var source = formatsAfter; | ||
if (!isCaretWithinFormattedText && activeFormats.length) { | ||
onEnterFormattedText(); | ||
} else if (isCaretWithinFormattedText && !activeFormats.length) { | ||
onExitFormattedText(); | ||
} // It is important that the internal value is updated first, | ||
// otherwise the value will be wrong on render! | ||
if (formatsBefore.length > formatsAfter.length) { | ||
source = formatsBefore; | ||
} // If the amount of formats before the caret and after the caret is | ||
// different, the caret is at a format boundary. | ||
this.record = newValue; | ||
this.applyRecord(newValue, { | ||
domOnly: true | ||
}); | ||
this.props.onSelectionChange(start, end); | ||
this.setState({ | ||
activeFormats: activeFormats | ||
}); | ||
} | ||
/** | ||
* Sync the value to global state. The node tree and selection will also be | ||
* updated if differences are found. | ||
* | ||
* @param {Object} record The record to sync and apply. | ||
* @param {Object} $2 Named options. | ||
* @param {boolean} $2.withoutHistory If true, no undo level will be | ||
* created. | ||
*/ | ||
if (formatsBefore.length < formatsAfter.length) { | ||
if (!isReverse && currentActiveFormats.length < formatsAfter.length) { | ||
newActiveFormatsLength++; | ||
} | ||
}, { | ||
key: "onChange", | ||
value: function onChange(record) { | ||
var _ref5 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
withoutHistory = _ref5.withoutHistory; | ||
if (isReverse && currentActiveFormats.length > formatsBefore.length) { | ||
newActiveFormatsLength--; | ||
} | ||
} else if (formatsBefore.length > formatsAfter.length) { | ||
if (!isReverse && currentActiveFormats.length > formatsAfter.length) { | ||
newActiveFormatsLength--; | ||
} | ||
if (this.props.__unstableDisableFormats) { | ||
record.formats = Array(record.text.length); | ||
record.replacements = Array(record.text.length); | ||
if (isReverse && currentActiveFormats.length < formatsBefore.length) { | ||
newActiveFormatsLength++; | ||
} | ||
} | ||
this.applyRecord(record); | ||
var start = record.start, | ||
end = record.end, | ||
_record$activeFormats = record.activeFormats, | ||
activeFormats = _record$activeFormats === void 0 ? [] : _record$activeFormats; | ||
var changeHandlers = pickBy(this.props, function (v, key) { | ||
return key.startsWith('format_on_change_functions_'); | ||
}); | ||
Object.values(changeHandlers).forEach(function (changeHandler) { | ||
changeHandler(record.formats, record.text); | ||
}); | ||
this.value = this.valueToFormat(record); | ||
this.record = record; // Selection must be updated first, so it is recorded in history when | ||
// the content change happens. | ||
if (newActiveFormatsLength !== currentActiveFormats.length) { | ||
var _newActiveFormats = source.slice(0, newActiveFormatsLength); | ||
this.props.onSelectionChange(start, end); | ||
this.props.onChange(this.value); | ||
this.setState({ | ||
activeFormats: activeFormats | ||
var _newValue = _objectSpread({}, record.current, { | ||
activeFormats: _newActiveFormats | ||
}); | ||
if (!withoutHistory) { | ||
this.onCreateUndoLevel(); | ||
} | ||
record.current = _newValue; | ||
applyRecord(_newValue); | ||
setActiveFormats(_newActiveFormats); | ||
return; | ||
} | ||
}, { | ||
key: "onCreateUndoLevel", | ||
value: function onCreateUndoLevel() { | ||
// If the content is the same, no level needs to be created. | ||
if (this.lastHistoryValue === this.value) { | ||
return; | ||
} | ||
this.props.__unstableOnCreateUndoLevel(); | ||
var newPos = start + (isReverse ? -1 : 1); | ||
var newActiveFormats = isReverse ? formatsBefore : formatsAfter; | ||
this.lastHistoryValue = this.value; | ||
} | ||
/** | ||
* Handles delete on keydown: | ||
* - outdent list items, | ||
* - delete content if everything is selected, | ||
* - trigger the onDelete prop when selection is uncollapsed and at an edge. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
var newValue = _objectSpread({}, record.current, { | ||
start: newPos, | ||
end: newPos, | ||
activeFormats: newActiveFormats | ||
}); | ||
}, { | ||
key: "handleDelete", | ||
value: function handleDelete(event) { | ||
var keyCode = event.keyCode; | ||
record.current = newValue; | ||
applyRecord(newValue); | ||
onSelectionChange(newPos, newPos); | ||
setActiveFormats(newActiveFormats); | ||
} | ||
if (keyCode !== DELETE && keyCode !== BACKSPACE && keyCode !== ESCAPE) { | ||
return; | ||
} | ||
function handleKeyDown(event) { | ||
if (event.defaultPrevented) { | ||
return; | ||
} | ||
if (this.props.__unstableDidAutomaticChange) { | ||
event.preventDefault(); | ||
handleDelete(event); | ||
handleEnter(event); | ||
handleSpace(event); | ||
handleHorizontalNavigation(event); | ||
} | ||
this.props.__unstableUndo(); | ||
var lastHistoryValue = useRef(value); | ||
return; | ||
} | ||
function createUndoLevel() { | ||
// If the content is the same, no level needs to be created. | ||
if (lastHistoryValue.current === _value.current) { | ||
return; | ||
} | ||
if (keyCode === ESCAPE) { | ||
return; | ||
} | ||
onCreateUndoLevel(); | ||
lastHistoryValue.current = _value.current; | ||
} | ||
var _this$props6 = this.props, | ||
onDelete = _this$props6.onDelete, | ||
multilineTag = _this$props6.__unstableMultilineTag; | ||
var _this$state$activeFor2 = this.state.activeFormats, | ||
activeFormats = _this$state$activeFor2 === void 0 ? [] : _this$state$activeFor2; | ||
var value = this.createRecord(); | ||
var start = value.start, | ||
end = value.end, | ||
text = value.text; | ||
var isReverse = keyCode === BACKSPACE; // Always handle full content deletion ourselves. | ||
var isComposing = useRef(false); | ||
var timeout = useRef(); | ||
/** | ||
* Handle input on the next selection change event. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic input event. | ||
*/ | ||
if (start === 0 && end !== 0 && end === text.length) { | ||
this.onChange(remove(value)); | ||
event.preventDefault(); | ||
return; | ||
} | ||
function handleInput(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.current) { | ||
return; | ||
} | ||
if (multilineTag) { | ||
var newValue; // Check to see if we should remove the first item if empty. | ||
var inputType; | ||
if (isReverse && value.start === 0 && value.end === 0 && isEmptyLine(value)) { | ||
newValue = removeLineSeparator(value, !isReverse); | ||
} else { | ||
newValue = removeLineSeparator(value, isReverse); | ||
} | ||
if (event) { | ||
inputType = event.inputType; | ||
} | ||
if (newValue) { | ||
this.onChange(newValue); | ||
event.preventDefault(); | ||
return; | ||
} | ||
} // Only process delete if the key press occurs at an uncollapsed edge. | ||
if (!inputType && event && event.nativeEvent) { | ||
inputType = event.nativeEvent.inputType; | ||
} // The browser formatted something or tried to insert HTML. | ||
// Overwrite it. It will be handled later by the format library if | ||
// needed. | ||
if (!onDelete || !isCollapsed(value) || activeFormats.length || isReverse && start !== 0 || !isReverse && end !== text.length) { | ||
return; | ||
} | ||
onDelete({ | ||
isReverse: isReverse, | ||
value: value | ||
}); | ||
event.preventDefault(); | ||
if (inputType && (inputType.indexOf('format') === 0 || INSERTION_INPUT_TYPES_TO_IGNORE.has(inputType))) { | ||
applyRecord(record.current); | ||
return; | ||
} | ||
/** | ||
* Triggers the `onEnter` prop on keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
}, { | ||
key: "handleEnter", | ||
value: function handleEnter(event) { | ||
if (event.keyCode !== ENTER) { | ||
return; | ||
} | ||
var currentValue = createRecord(); | ||
var _record$current2 = record.current, | ||
start = _record$current2.start, | ||
_record$current2$acti = _record$current2.activeFormats, | ||
oldActiveFormats = _record$current2$acti === void 0 ? [] : _record$current2$acti; // Update the formats between the last and new caret position. | ||
event.preventDefault(); | ||
var onEnter = this.props.onEnter; | ||
var change = updateFormats({ | ||
value: currentValue, | ||
start: start, | ||
end: currentValue.start, | ||
formats: oldActiveFormats | ||
}); | ||
handleChange(change, { | ||
withoutHistory: true | ||
}); // Create an undo level when input stops for over a second. | ||
if (!onEnter) { | ||
return; | ||
} | ||
getWin().clearTimeout(timeout.current); | ||
timeout.current = getWin().setTimeout(createUndoLevel, 1000); // Only run input rules when inserting text. | ||
onEnter({ | ||
value: this.removeEditorOnlyFormats(this.createRecord()), | ||
onChange: this.onChange, | ||
shiftKey: event.shiftKey | ||
}); | ||
if (inputType !== 'insertText') { | ||
return; | ||
} | ||
/** | ||
* Indents list items on space keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
}, { | ||
key: "handleSpace", | ||
value: function handleSpace(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
var _this$props7 = this.props, | ||
multilineRootTag = _this$props7.__unstableMultilineRootTag, | ||
multilineTag = _this$props7.__unstableMultilineTag; | ||
if (allowPrefixTransformations && inputRule) { | ||
inputRule(change, valueToFormat); | ||
} | ||
if ( // Only override when no modifiers are pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== SPACE || multilineTag !== 'li') { | ||
return; | ||
} | ||
var transformed = formatTypes.reduce(function (accumlator, _ref4) { | ||
var __unstableInputRule = _ref4.__unstableInputRule; | ||
var value = this.createRecord(); | ||
if (!isCollapsed(value)) { | ||
return; | ||
if (__unstableInputRule) { | ||
accumlator = __unstableInputRule(accumlator); | ||
} | ||
var text = value.text, | ||
start = value.start; | ||
var characterBefore = text[start - 1]; // The caret must be at the start of a line. | ||
return accumlator; | ||
}, change); | ||
if (characterBefore && characterBefore !== LINE_SEPARATOR) { | ||
return; | ||
} | ||
this.onChange(indentListItems(value, { | ||
type: multilineRootTag | ||
if (transformed !== change) { | ||
createUndoLevel(); | ||
handleChange(_objectSpread({}, transformed, { | ||
activeFormats: oldActiveFormats | ||
})); | ||
event.preventDefault(); | ||
markAutomaticChange(); | ||
} | ||
/** | ||
* Handles horizontal keyboard navigation when no modifiers are pressed. The | ||
* navigation is handled separately to move correctly around format | ||
* boundaries. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
} | ||
}, { | ||
key: "handleHorizontalNavigation", | ||
value: function handleHorizontalNavigation(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
function handleCompositionStart() { | ||
isComposing.current = true; // Do not update the selection when characters are being composed as | ||
// this rerenders the component and might distroy internal browser | ||
// editing state. | ||
if ( // Only override left and right keys without modifiers pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== LEFT && keyCode !== RIGHT) { | ||
return; | ||
} | ||
getDoc().removeEventListener('selectionchange', handleSelectionChange); | ||
} | ||
var value = this.record; | ||
var text = value.text, | ||
formats = value.formats, | ||
start = value.start, | ||
end = value.end, | ||
_value$activeFormats = value.activeFormats, | ||
activeFormats = _value$activeFormats === void 0 ? [] : _value$activeFormats; | ||
var collapsed = isCollapsed(value); // To do: ideally, we should look at visual position instead. | ||
function handleCompositionEnd() { | ||
isComposing.current = false; // Ensure the value is up-to-date for browsers that don't emit a final | ||
// input event after composition. | ||
var _this$getWindow$getCo = this.getWindow().getComputedStyle(this.props.forwardedRef.current), | ||
direction = _this$getWindow$getCo.direction; | ||
handleInput({ | ||
inputType: 'insertText' | ||
}); // Tracking selection changes can be resumed. | ||
var reverseKey = direction === 'rtl' ? RIGHT : LEFT; | ||
var 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. | ||
getDoc().addEventListener('selectionchange', handleSelectionChange); | ||
} | ||
if (collapsed && activeFormats.length === 0) { | ||
if (start === 0 && isReverse) { | ||
return; | ||
} | ||
var didMount = useRef(false); | ||
/** | ||
* Syncs the selection to local state. A callback for the `selectionchange` | ||
* native events, `keyup`, `mouseup` and `touchend` synthetic events, and | ||
* animation frames after the `focus` event. | ||
* | ||
* @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event | ||
*/ | ||
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. | ||
function handleSelectionChange(event) { | ||
if (!ref.current) { | ||
return; | ||
} | ||
if (document.activeElement !== ref.current) { | ||
return; | ||
} | ||
if (!collapsed) { | ||
return; | ||
} // In all other cases, prevent default behaviour. | ||
if (event.type !== 'selectionchange' && !isSelected) { | ||
return; | ||
} | ||
if (disabled) { | ||
return; | ||
} // In case of a keyboard event, ignore selection changes during | ||
// composition. | ||
event.preventDefault(); | ||
var formatsBefore = formats[start - 1] || EMPTY_ACTIVE_FORMATS; | ||
var formatsAfter = formats[start] || EMPTY_ACTIVE_FORMATS; | ||
var newActiveFormatsLength = activeFormats.length; | ||
var source = formatsAfter; | ||
if (formatsBefore.length > formatsAfter.length) { | ||
source = formatsBefore; | ||
} // If the amount of formats before the caret and after the caret is | ||
// different, the caret is at a format boundary. | ||
if (isComposing.current) { | ||
return; | ||
} | ||
var _createRecord = createRecord(), | ||
start = _createRecord.start, | ||
end = _createRecord.end, | ||
text = _createRecord.text; | ||
if (formatsBefore.length < formatsAfter.length) { | ||
if (!isReverse && activeFormats.length < formatsAfter.length) { | ||
newActiveFormatsLength++; | ||
} | ||
var oldRecord = record.current; // Fallback mechanism for IE11, which doesn't support the input event. | ||
// Any input results in a selection change. | ||
if (isReverse && activeFormats.length > formatsBefore.length) { | ||
newActiveFormatsLength--; | ||
} | ||
} else if (formatsBefore.length > formatsAfter.length) { | ||
if (!isReverse && activeFormats.length > formatsAfter.length) { | ||
newActiveFormatsLength--; | ||
} | ||
if (text !== oldRecord.text) { | ||
handleInput(); | ||
return; | ||
} | ||
if (isReverse && activeFormats.length < formatsBefore.length) { | ||
newActiveFormatsLength++; | ||
} | ||
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(getWin()); | ||
} | ||
if (newActiveFormatsLength !== activeFormats.length) { | ||
var _newActiveFormats = source.slice(0, newActiveFormatsLength); | ||
return; | ||
} | ||
var _newValue = _objectSpread({}, value, { | ||
activeFormats: _newActiveFormats | ||
}); | ||
var newValue = _objectSpread({}, oldRecord, { | ||
start: start, | ||
end: end, | ||
// Allow `getActiveFormats` to get new `activeFormats`. | ||
activeFormats: undefined | ||
}); | ||
this.record = _newValue; | ||
this.applyRecord(_newValue); | ||
this.setState({ | ||
activeFormats: _newActiveFormats | ||
}); | ||
return; | ||
} | ||
var newActiveFormats = getActiveFormats(newValue, EMPTY_ACTIVE_FORMATS); // Update the value with the new active formats. | ||
var newPos = start + (isReverse ? -1 : 1); | ||
var newActiveFormats = isReverse ? formatsBefore : formatsAfter; | ||
newValue.activeFormats = newActiveFormats; | ||
var newValue = _objectSpread({}, value, { | ||
start: newPos, | ||
end: newPos, | ||
activeFormats: newActiveFormats | ||
}); | ||
if (!isCaretWithinFormattedText && newActiveFormats.length) { | ||
onEnterFormattedText(); | ||
} else if (isCaretWithinFormattedText && !newActiveFormats.length) { | ||
onExitFormattedText(); | ||
} // It is important that the internal value is updated first, | ||
// otherwise the value will be wrong on render! | ||
this.record = newValue; | ||
this.applyRecord(newValue); | ||
this.props.onSelectionChange(newPos, newPos); | ||
this.setState({ | ||
activeFormats: newActiveFormats | ||
}); | ||
} | ||
/** | ||
* Select object when they are clicked. The browser will not set any | ||
* selection when clicking e.g. an image. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic mousedown or touchstart event. | ||
*/ | ||
}, { | ||
key: "onPointerDown", | ||
value: function onPointerDown(event) { | ||
var target = event.target; // If the child element has no text content, it must be an object. | ||
record.current = newValue; | ||
applyRecord(newValue, { | ||
domOnly: true | ||
}); | ||
onSelectionChange(start, end); | ||
setActiveFormats(newActiveFormats); | ||
} | ||
/** | ||
* 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. | ||
* @param {Object} $2 Named options. | ||
* @param {boolean} $2.withoutHistory If true, no undo level will be | ||
* created. | ||
*/ | ||
if (target === this.props.forwardedRef.current || target.textContent) { | ||
return; | ||
} | ||
var parentNode = target.parentNode; | ||
var index = Array.from(parentNode.childNodes).indexOf(target); | ||
var range = this.getDocument().createRange(); | ||
var selection = this.getWindow().getSelection(); | ||
range.setStart(target.parentNode, index); | ||
range.setEnd(target.parentNode, index + 1); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
function handleChange(newRecord) { | ||
var _ref5 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
withoutHistory = _ref5.withoutHistory; | ||
if (disableFormats) { | ||
newRecord.formats = Array(newRecord.text.length); | ||
newRecord.replacements = Array(newRecord.text.length); | ||
} | ||
}, { | ||
key: "componentDidUpdate", | ||
value: function componentDidUpdate(prevProps) { | ||
var _this$props8 = this.props, | ||
tagName = _this$props8.tagName, | ||
value = _this$props8.value, | ||
selectionStart = _this$props8.selectionStart, | ||
selectionEnd = _this$props8.selectionEnd, | ||
placeholder = _this$props8.placeholder, | ||
isSelected = _this$props8.__unstableIsSelected; // Check if tag name changed. | ||
var shouldReapply = tagName !== prevProps.tagName; // Check if the content changed. | ||
applyRecord(newRecord); | ||
var start = newRecord.start, | ||
end = newRecord.end, | ||
_newRecord$activeForm = newRecord.activeFormats, | ||
newActiveFormats = _newRecord$activeForm === void 0 ? [] : _newRecord$activeForm; | ||
var changeHandlers = pickBy(remainingProps, function (v, key) { | ||
return key.startsWith('format_on_change_functions_'); | ||
}); | ||
Object.values(changeHandlers).forEach(function (changeHandler) { | ||
changeHandler(newRecord.formats, newRecord.text); | ||
}); | ||
_value.current = valueToFormat(newRecord); | ||
record.current = newRecord; // Selection must be updated first, so it is recorded in history when | ||
// the content change happens. | ||
shouldReapply = shouldReapply || value !== prevProps.value && value !== this.value; | ||
var selectionChanged = selectionStart !== prevProps.selectionStart && selectionStart !== this.record.start || selectionEnd !== prevProps.selectionEnd && selectionEnd !== this.record.end; // Check if the selection changed. | ||
onSelectionChange(start, end); | ||
onChange(_value.current); | ||
setActiveFormats(newActiveFormats); | ||
shouldReapply = shouldReapply || isSelected && !prevProps.isSelected && selectionChanged; | ||
var prefix = 'format_prepare_props_'; | ||
if (!withoutHistory) { | ||
createUndoLevel(); | ||
} | ||
} | ||
/** | ||
* Select object when they are clicked. The browser will not set any | ||
* selection when clicking e.g. an image. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic mousedown or touchstart event. | ||
*/ | ||
var predicate = function predicate(v, key) { | ||
return key.startsWith(prefix); | ||
}; | ||
var prepareProps = pickBy(this.props, predicate); | ||
var prevPrepareProps = pickBy(prevProps, predicate); // Check if any format props changed. | ||
function handlePointerDown(event) { | ||
var target = event.target; // If the child element has no text content, it must be an object. | ||
shouldReapply = shouldReapply || !isShallowEqual(prepareProps, prevPrepareProps); // Rerender if the placeholder changed. | ||
if (target === ref.current || target.textContent) { | ||
return; | ||
} | ||
shouldReapply = shouldReapply || placeholder !== prevProps.placeholder; | ||
var parentNode = target.parentNode; | ||
var index = Array.from(parentNode.childNodes).indexOf(target); | ||
var range = getDoc().createRange(); | ||
var selection = getWin().getSelection(); | ||
range.setStart(target.parentNode, index); | ||
range.setEnd(target.parentNode, index + 1); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
} | ||
if (shouldReapply) { | ||
this.value = value; | ||
this.record = this.formatToValue(value); | ||
this.record.start = selectionStart; | ||
this.record.end = selectionEnd; | ||
this.applyRecord(this.record); | ||
} else if (selectionChanged) { | ||
this.record = _objectSpread({}, this.record, { | ||
start: selectionStart, | ||
end: selectionEnd | ||
}); | ||
} | ||
var rafId = useRef(); | ||
/** | ||
* Handles a focus event on the contenteditable field, calling the | ||
* `unstableOnFocus` prop callback if one is defined. The callback does not | ||
* receive any arguments. | ||
* | ||
* This is marked as a private API and the `unstableOnFocus` prop is not | ||
* documented, as the current requirements where it is used are subject to | ||
* future refactoring following `isSelected` handling. | ||
* | ||
* In contrast with `setFocusedElement`, this is only triggered in response | ||
* to focus within the contenteditable field, whereas `setFocusedElement` | ||
* is triggered on focus within any `RichText` descendent element. | ||
* | ||
* @see setFocusedElement | ||
* | ||
* @private | ||
*/ | ||
function handleFocus() { | ||
if (onFocus) { | ||
onFocus(); | ||
} | ||
/** | ||
* Converts the outside data structure to our internal representation. | ||
* | ||
* @param {*} value The outside value, data type depends on props. | ||
* @return {Object} An internal rich-text value. | ||
*/ | ||
}, { | ||
key: "formatToValue", | ||
value: function formatToValue(value) { | ||
var _this$props9 = this.props, | ||
format = _this$props9.format, | ||
multilineTag = _this$props9.__unstableMultilineTag, | ||
preserveWhiteSpace = _this$props9.preserveWhiteSpace, | ||
disableFormats = _this$props9.__unstableDisableFormats; | ||
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. | ||
var index = undefined; | ||
record.current = _objectSpread({}, record.current, { | ||
start: index, | ||
end: index, | ||
activeFormats: EMPTY_ACTIVE_FORMATS | ||
}); | ||
onSelectionChange(index, index); | ||
setActiveFormats(EMPTY_ACTIVE_FORMATS); | ||
} else { | ||
onSelectionChange(record.current.start, record.current.end); | ||
setActiveFormats(getActiveFormats(_objectSpread({}, record.current, { | ||
activeFormats: undefined | ||
}), EMPTY_ACTIVE_FORMATS)); | ||
} // Update selection as soon as possible, which is at the next animation | ||
// frame. The event listener for selection changes may be added too late | ||
// at this point, but this focus event is still too early to calculate | ||
// the selection. | ||
if (disableFormats) { | ||
return { | ||
text: value, | ||
formats: Array(value.length), | ||
replacements: Array(value.length) | ||
}; | ||
} | ||
if (format !== 'string') { | ||
return value; | ||
} | ||
rafId.current = getWin().requestAnimationFrame(handleSelectionChange); | ||
getDoc().addEventListener('selectionchange', handleSelectionChange); | ||
var prepare = createPrepareEditableTree(this.props, 'format_value_functions'); | ||
value = create({ | ||
html: value, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
if (setFocusedElement) { | ||
deprecated('wp.blockEditor.RichText setFocusedElement prop', { | ||
alternative: 'selection state from the block editor store.' | ||
}); | ||
value.formats = prepare(value); | ||
return value; | ||
setFocusedElement(instanceId); | ||
} | ||
/** | ||
* Removes editor only formats from the value. | ||
* | ||
* Editor only formats are applied using `prepareEditableTree`, so we need to | ||
* remove them before converting the internal state | ||
* | ||
* @param {Object} value The internal rich-text value. | ||
* @return {Object} A new rich-text value. | ||
*/ | ||
} | ||
}, { | ||
key: "removeEditorOnlyFormats", | ||
value: function removeEditorOnlyFormats(value) { | ||
this.props.formatTypes.forEach(function (formatType) { | ||
// Remove formats created by prepareEditableTree, because they are editor only. | ||
if (formatType.__experimentalCreatePrepareEditableTree) { | ||
value = removeFormat(value, formatType.name, 0, value.text.length); | ||
} | ||
}); | ||
return value; | ||
} | ||
/** | ||
* Converts the internal value to the external data format. | ||
* | ||
* @param {Object} value The internal rich-text value. | ||
* @return {*} The external data format, data type depends on props. | ||
*/ | ||
function handleBlur() { | ||
getDoc().removeEventListener('selectionchange', handleSelectionChange); | ||
} | ||
}, { | ||
key: "valueToFormat", | ||
value: function valueToFormat(value) { | ||
var _this$props10 = this.props, | ||
format = _this$props10.format, | ||
multilineTag = _this$props10.__unstableMultilineTag, | ||
preserveWhiteSpace = _this$props10.preserveWhiteSpace, | ||
disableFormats = _this$props10.__unstableDisableFormats; | ||
function applyFromProps() { | ||
_value.current = value; | ||
record.current = formatToValue(value); | ||
record.current.start = selectionStart; | ||
record.current.end = selectionEnd; | ||
applyRecord(record.current); | ||
} | ||
if (disableFormats) { | ||
return value.text; | ||
} | ||
useEffect(function () { | ||
if (didMount.current) { | ||
applyFromProps(); | ||
} | ||
}, [TagName, placeholder]); | ||
useEffect(function () { | ||
if (didMount.current && value !== _value.current) { | ||
applyFromProps(); | ||
} | ||
}, [value]); | ||
useEffect(function () { | ||
if (!didMount.current) { | ||
return; | ||
} | ||
value = this.removeEditorOnlyFormats(value); | ||
if (format !== 'string') { | ||
return; | ||
} | ||
return toHTMLString({ | ||
value: value, | ||
multilineTag: multilineTag, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
if (isSelected && (selectionStart !== record.current.start || selectionEnd !== record.current.end)) { | ||
applyFromProps(); | ||
} else { | ||
record.current = _objectSpread({}, record.current, { | ||
start: selectionStart, | ||
end: selectionEnd | ||
}); | ||
} | ||
}, { | ||
key: "Editable", | ||
value: function Editable(props) { | ||
var _this2 = this; | ||
}, [selectionStart, selectionEnd, isSelected]); | ||
var prefix = 'format_prepare_props_'; | ||
var _this$props11 = this.props, | ||
_this$props11$tagName = _this$props11.tagName, | ||
TagName = _this$props11$tagName === void 0 ? 'div' : _this$props11$tagName, | ||
style = _this$props11.style, | ||
className = _this$props11.className, | ||
placeholder = _this$props11.placeholder, | ||
forwardedRef = _this$props11.forwardedRef, | ||
disabled = _this$props11.disabled; | ||
var ariaProps = pickBy(this.props, function (value, key) { | ||
return startsWith(key, 'aria-'); | ||
}); | ||
return createElement(TagName // Overridable props. | ||
, _extends({ | ||
role: "textbox", | ||
"aria-multiline": true, | ||
"aria-label": placeholder | ||
}, props, ariaProps, { | ||
ref: forwardedRef, | ||
style: style ? _objectSpread({}, style, { | ||
whiteSpace: whiteSpace | ||
}) : defaultStyle, | ||
className: classnames('rich-text', className), | ||
onPaste: this.onPaste, | ||
onInput: this.onInput, | ||
onCompositionStart: this.onCompositionStart, | ||
onCompositionEnd: this.onCompositionEnd, | ||
onKeyDown: props.onKeyDown ? function (event) { | ||
props.onKeyDown(event); | ||
var predicate = function predicate(v, key) { | ||
return key.startsWith(prefix); | ||
}; | ||
_this2.onKeyDown(event); | ||
} : this.onKeyDown, | ||
onFocus: this.onFocus, | ||
onBlur: this.onBlur, | ||
onMouseDown: this.onPointerDown, | ||
onTouchStart: this.onPointerDown // Selection updates must be done at these events as they | ||
// happen before the `selectionchange` event. In some cases, | ||
// the `selectionchange` event may not even fire, for | ||
// example when the window receives focus again on click. | ||
, | ||
onKeyUp: this.onSelectionChange, | ||
onMouseUp: this.onSelectionChange, | ||
onTouchEnd: this.onSelectionChange // Do not set the attribute if disabled. | ||
, | ||
contentEditable: disabled ? undefined : true, | ||
suppressContentEditableWarning: !disabled | ||
})); | ||
var prepareProps = pickBy(remainingProps, predicate); | ||
useEffect(function () { | ||
if (didMount.current) { | ||
applyFromProps(); | ||
} | ||
}, { | ||
key: "render", | ||
value: function render() { | ||
var _this3 = this; | ||
}, Object.values(prepareProps)); | ||
useLayoutEffect(function () { | ||
applyRecord(record.current, { | ||
domOnly: true | ||
}); | ||
didMount.current = true; | ||
return function () { | ||
getDoc().removeEventListener('selectionchange', handleSelectionChange); | ||
getWin().cancelAnimationFrame(rafId.current); | ||
getWin().clearTimeout(timeout.current); | ||
}; | ||
}, []); | ||
var _this$props12 = this.props, | ||
isSelected = _this$props12.__unstableIsSelected, | ||
children = _this$props12.children, | ||
allowedFormats = _this$props12.allowedFormats, | ||
withoutInteractiveFormatting = _this$props12.withoutInteractiveFormatting, | ||
formatTypes = _this$props12.formatTypes, | ||
forwardedRef = _this$props12.forwardedRef; | ||
var activeFormats = this.state.activeFormats; | ||
function focus() { | ||
ref.current.focus(); | ||
applyRecord(record.current); | ||
} | ||
var onFocus = function onFocus() { | ||
forwardedRef.current.focus(); | ||
var ariaProps = pickBy(remainingProps, function (v, key) { | ||
return startsWith(key, 'aria-'); | ||
}); | ||
_this3.applyRecord(_this3.record); | ||
}; | ||
var editableProps = _objectSpread({ | ||
// Overridable props. | ||
role: 'textbox', | ||
'aria-multiline': '', | ||
'aria-label': placeholder | ||
}, ariaProps, { | ||
ref: ref, | ||
style: style ? _objectSpread({}, style, { | ||
whiteSpace: whiteSpace | ||
}) : defaultStyle, | ||
className: classnames('rich-text', className), | ||
onPaste: handlePaste, | ||
onInput: handleInput, | ||
onCompositionStart: handleCompositionStart, | ||
onCompositionEnd: handleCompositionEnd, | ||
onKeyDown: handleKeyDown, | ||
onFocus: handleFocus, | ||
onBlur: handleBlur, | ||
onMouseDown: handlePointerDown, | ||
onTouchStart: handlePointerDown, | ||
// Selection updates must be done at these events as they | ||
// happen before the `selectionchange` event. In some cases, | ||
// the `selectionchange` event may not even fire, for | ||
// example when the window receives focus again on click. | ||
onKeyUp: handleSelectionChange, | ||
onMouseUp: handleSelectionChange, | ||
onTouchEnd: handleSelectionChange, | ||
// Do not set the attribute if disabled. | ||
contentEditable: disabled ? undefined : true, | ||
suppressContentEditableWarning: !disabled | ||
}); | ||
return createElement(Fragment, null, createElement(BoundaryStyle, { | ||
activeFormats: activeFormats, | ||
forwardedRef: forwardedRef | ||
}), createElement(InlineWarning, { | ||
forwardedRef: forwardedRef | ||
}), isSelected && createElement(FormatEdit, { | ||
allowedFormats: allowedFormats, | ||
withoutInteractiveFormatting: withoutInteractiveFormatting, | ||
value: this.record, | ||
onChange: this.onChange, | ||
onFocus: onFocus, | ||
formatTypes: formatTypes | ||
}), children && children({ | ||
isSelected: isSelected, | ||
value: this.record, | ||
onChange: this.onChange, | ||
onFocus: onFocus, | ||
Editable: this.Editable | ||
}), !children && createElement(this.Editable, null)); | ||
} | ||
}]); | ||
useBoundaryStyle({ | ||
ref: ref, | ||
activeFormats: activeFormats | ||
}); | ||
useInlineWarning({ | ||
ref: ref | ||
}); | ||
return createElement(Fragment, null, isSelected && createElement(FormatEdit, { | ||
allowedFormats: allowedFormats, | ||
withoutInteractiveFormatting: withoutInteractiveFormatting, | ||
value: record.current, | ||
onChange: handleChange, | ||
onFocus: focus, | ||
formatTypes: formatTypes | ||
}), children && children({ | ||
isSelected: isSelected, | ||
value: record.current, | ||
onChange: handleChange, | ||
onFocus: focus, | ||
editableProps: editableProps, | ||
editableTagName: TagName | ||
}), !children && createElement(TagName, editableProps)); | ||
} | ||
return RichText; | ||
}(Component); | ||
RichText.defaultProps = { | ||
format: 'string', | ||
value: '' | ||
}; | ||
var RichTextWrapper = compose([withSafeTimeout, withFormatTypes])(RichText); | ||
var RichTextWrapper = withFormatTypes(RichText); | ||
/** | ||
@@ -1222,0 +1110,0 @@ * Renders a rich content input, providing users with the option to format the |
/** | ||
* Gets the all format objects at the start of the selection. | ||
* | ||
* @param {Object} value Value to inspect. | ||
* @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no active | ||
* formats. | ||
* @param {Object} value Value to inspect. | ||
* @param {Array<Array>} value.formats Formats object data values. | ||
* @param {number} value.start Index to start from. | ||
* @param {number} value.end Index to end. | ||
* @param {Array} value.activeFormats Array to return if there are active formats. | ||
* @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no active | ||
* formats. | ||
* | ||
@@ -8,0 +12,0 @@ * @return {?Object} Active format objects. |
@@ -16,14 +16,6 @@ "use strict"; | ||
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); | ||
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); | ||
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); | ||
var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties")); | ||
var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized")); | ||
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn")); | ||
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf")); | ||
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits")); | ||
var _classnames = _interopRequireDefault(require("classnames")); | ||
@@ -35,6 +27,2 @@ | ||
var _compose = require("@wordpress/compose"); | ||
var _isShallowEqual = _interopRequireDefault(require("@wordpress/is-shallow-equal")); | ||
var _deprecated = _interopRequireDefault(require("@wordpress/deprecated")); | ||
@@ -70,5 +58,5 @@ | ||
var _boundaryStyle = require("./boundary-style"); | ||
var _useBoundaryStyle = require("./use-boundary-style"); | ||
var _inlineWarning = require("./inline-warning"); | ||
var _useInlineWarning = require("./use-inline-warning"); | ||
@@ -81,6 +69,2 @@ var _insert = require("../insert"); | ||
function _createSuper(Derived) { return function () { var Super = (0, _getPrototypeOf2.default)(Derived), result; if (_isNativeReflectConstruct()) { var NewTarget = (0, _getPrototypeOf2.default)(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2.default)(this, result); }; } | ||
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } } | ||
/** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ | ||
@@ -168,1079 +152,980 @@ | ||
} | ||
/** | ||
* See export statement below. | ||
*/ | ||
function RichText(_ref) { | ||
var _ref$tagName = _ref.tagName, | ||
TagName = _ref$tagName === void 0 ? 'div' : _ref$tagName, | ||
_ref$value = _ref.value, | ||
value = _ref$value === void 0 ? '' : _ref$value, | ||
selectionStart = _ref.selectionStart, | ||
selectionEnd = _ref.selectionEnd, | ||
children = _ref.children, | ||
allowedFormats = _ref.allowedFormats, | ||
withoutInteractiveFormatting = _ref.withoutInteractiveFormatting, | ||
formatTypes = _ref.formatTypes, | ||
style = _ref.style, | ||
className = _ref.className, | ||
placeholder = _ref.placeholder, | ||
disabled = _ref.disabled, | ||
preserveWhiteSpace = _ref.preserveWhiteSpace, | ||
onPaste = _ref.onPaste, | ||
_ref$format = _ref.format, | ||
format = _ref$format === void 0 ? 'string' : _ref$format, | ||
onDelete = _ref.onDelete, | ||
onEnter = _ref.onEnter, | ||
onSelectionChange = _ref.onSelectionChange, | ||
onChange = _ref.onChange, | ||
onFocus = _ref.unstableOnFocus, | ||
setFocusedElement = _ref.setFocusedElement, | ||
instanceId = _ref.instanceId, | ||
multilineTag = _ref.__unstableMultilineTag, | ||
multilineRootTag = _ref.__unstableMultilineRootTag, | ||
disableFormats = _ref.__unstableDisableFormats, | ||
didAutomaticChange = _ref.__unstableDidAutomaticChange, | ||
inputRule = _ref.__unstableInputRule, | ||
markAutomaticChange = _ref.__unstableMarkAutomaticChange, | ||
allowPrefixTransformations = _ref.__unstableAllowPrefixTransformations, | ||
undo = _ref.__unstableUndo, | ||
isCaretWithinFormattedText = _ref.__unstableIsCaretWithinFormattedText, | ||
onEnterFormattedText = _ref.__unstableOnEnterFormattedText, | ||
onExitFormattedText = _ref.__unstableOnExitFormattedText, | ||
onCreateUndoLevel = _ref.__unstableOnCreateUndoLevel, | ||
isSelected = _ref.__unstableIsSelected, | ||
ref = _ref.forwardedRef, | ||
remainingProps = (0, _objectWithoutProperties2.default)(_ref, ["tagName", "value", "selectionStart", "selectionEnd", "children", "allowedFormats", "withoutInteractiveFormatting", "formatTypes", "style", "className", "placeholder", "disabled", "preserveWhiteSpace", "onPaste", "format", "onDelete", "onEnter", "onSelectionChange", "onChange", "unstableOnFocus", "setFocusedElement", "instanceId", "__unstableMultilineTag", "__unstableMultilineRootTag", "__unstableDisableFormats", "__unstableDidAutomaticChange", "__unstableInputRule", "__unstableMarkAutomaticChange", "__unstableAllowPrefixTransformations", "__unstableUndo", "__unstableIsCaretWithinFormattedText", "__unstableOnEnterFormattedText", "__unstableOnExitFormattedText", "__unstableOnCreateUndoLevel", "__unstableIsSelected", "forwardedRef"]); | ||
var RichText = /*#__PURE__*/function (_Component) { | ||
(0, _inherits2.default)(RichText, _Component); | ||
var _useState = (0, _element.useState)(), | ||
_useState2 = (0, _slicedToArray2.default)(_useState, 2), | ||
_useState2$ = _useState2[0], | ||
activeFormats = _useState2$ === void 0 ? [] : _useState2$, | ||
setActiveFormats = _useState2[1]; | ||
var _super = _createSuper(RichText); | ||
function getDoc() { | ||
return ref.current.ownerDocument; | ||
} | ||
function RichText(_ref) { | ||
var _this; | ||
function getWin() { | ||
return getDoc().defaultView; | ||
} | ||
/** | ||
* Converts the outside data structure to our internal representation. | ||
* | ||
* @param {*} string The outside value, data type depends on props. | ||
* | ||
* @return {Object} An internal rich-text value. | ||
*/ | ||
var value = _ref.value, | ||
selectionStart = _ref.selectionStart, | ||
selectionEnd = _ref.selectionEnd; | ||
(0, _classCallCheck2.default)(this, RichText); | ||
_this = _super.apply(this, arguments); | ||
_this.getDocument = _this.getDocument.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.getWindow = _this.getWindow.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onFocus = _this.onFocus.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onBlur = _this.onBlur.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onChange = _this.onChange.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.handleDelete = _this.handleDelete.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.handleEnter = _this.handleEnter.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.handleSpace = _this.handleSpace.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.handleHorizontalNavigation = _this.handleHorizontalNavigation.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onPaste = _this.onPaste.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onCreateUndoLevel = _this.onCreateUndoLevel.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onInput = _this.onInput.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onCompositionStart = _this.onCompositionStart.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onCompositionEnd = _this.onCompositionEnd.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onSelectionChange = _this.onSelectionChange.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.createRecord = _this.createRecord.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.applyRecord = _this.applyRecord.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.valueToFormat = _this.valueToFormat.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onPointerDown = _this.onPointerDown.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.formatToValue = _this.formatToValue.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.Editable = _this.Editable.bind((0, _assertThisInitialized2.default)(_this)); | ||
_this.onKeyDown = function (event) { | ||
if (event.defaultPrevented) { | ||
return; | ||
} | ||
function formatToValue(string) { | ||
if (disableFormats) { | ||
return { | ||
text: string, | ||
formats: Array(string.length), | ||
replacements: Array(string.length) | ||
}; | ||
} | ||
_this.handleDelete(event); | ||
if (format !== 'string') { | ||
return string; | ||
} | ||
_this.handleEnter(event); | ||
var prepare = createPrepareEditableTree(remainingProps, 'format_value_functions'); | ||
var result = (0, _create.create)({ | ||
html: string, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
result.formats = prepare(result); | ||
return result; | ||
} | ||
/** | ||
* Removes editor only formats from the value. | ||
* | ||
* Editor only formats are applied using `prepareEditableTree`, so we need to | ||
* remove them before converting the internal state | ||
* | ||
* @param {Object} val The internal rich-text value. | ||
* | ||
* @return {Object} A new rich-text value. | ||
*/ | ||
_this.handleSpace(event); | ||
_this.handleHorizontalNavigation(event); | ||
}; | ||
function removeEditorOnlyFormats(val) { | ||
formatTypes.forEach(function (formatType) { | ||
// Remove formats created by prepareEditableTree, because they are editor only. | ||
if (formatType.__experimentalCreatePrepareEditableTree) { | ||
val = (0, _removeFormat.removeFormat)(val, formatType.name, 0, val.text.length); | ||
} | ||
}); | ||
return val; | ||
} | ||
/** | ||
* Converts the internal value to the external data format. | ||
* | ||
* @param {Object} val The internal rich-text value. | ||
* | ||
* @return {*} The external data format, data type depends on props. | ||
*/ | ||
_this.state = {}; | ||
_this.lastHistoryValue = value; // Internal values are updated synchronously, unlike props and state. | ||
_this.value = value; | ||
_this.record = _this.formatToValue(value); | ||
_this.record.start = selectionStart; | ||
_this.record.end = selectionEnd; | ||
return _this; | ||
} | ||
(0, _createClass2.default)(RichText, [{ | ||
key: "componentWillUnmount", | ||
value: function componentWillUnmount() { | ||
this.getDocument().removeEventListener('selectionchange', this.onSelectionChange); | ||
this.getWindow().cancelAnimationFrame(this.rafId); | ||
function valueToFormat(val) { | ||
if (disableFormats) { | ||
return val.text; | ||
} | ||
}, { | ||
key: "componentDidMount", | ||
value: function componentDidMount() { | ||
this.applyRecord(this.record, { | ||
domOnly: true | ||
}); | ||
} | ||
}, { | ||
key: "getDocument", | ||
value: function getDocument() { | ||
return this.props.forwardedRef.current.ownerDocument; | ||
} | ||
}, { | ||
key: "getWindow", | ||
value: function getWindow() { | ||
return this.getDocument().defaultView; | ||
} | ||
}, { | ||
key: "createRecord", | ||
value: function createRecord() { | ||
var _this$props = this.props, | ||
multilineTag = _this$props.__unstableMultilineTag, | ||
forwardedRef = _this$props.forwardedRef, | ||
preserveWhiteSpace = _this$props.preserveWhiteSpace; | ||
var selection = this.getWindow().getSelection(); | ||
var range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; | ||
return (0, _create.create)({ | ||
element: forwardedRef.current, | ||
range: range, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
__unstableIsEditableTree: true, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
} | ||
}, { | ||
key: "applyRecord", | ||
value: function applyRecord(record) { | ||
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
domOnly = _ref2.domOnly; | ||
var _this$props2 = this.props, | ||
multilineTag = _this$props2.__unstableMultilineTag, | ||
forwardedRef = _this$props2.forwardedRef; | ||
(0, _toDom.apply)({ | ||
value: record, | ||
current: forwardedRef.current, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
prepareEditableTree: createPrepareEditableTree(this.props, 'format_prepare_functions'), | ||
__unstableDomOnly: domOnly, | ||
placeholder: this.props.placeholder | ||
}); | ||
val = removeEditorOnlyFormats(val); | ||
if (format !== 'string') { | ||
return; | ||
} | ||
/** | ||
* Handles a paste event. | ||
* | ||
* Saves the pasted data as plain text in `pastedPlainText`. | ||
* | ||
* @param {ClipboardEvent} event The paste event. | ||
*/ | ||
}, { | ||
key: "onPaste", | ||
value: function onPaste(event) { | ||
var _this$props3 = this.props, | ||
formatTypes = _this$props3.formatTypes, | ||
onPaste = _this$props3.onPaste, | ||
isSelected = _this$props3.__unstableIsSelected, | ||
__unstableDisableFormats = _this$props3.__unstableDisableFormats; | ||
var _this$state$activeFor = this.state.activeFormats, | ||
activeFormats = _this$state$activeFor === void 0 ? [] : _this$state$activeFor; | ||
return (0, _toHtmlString.toHTMLString)({ | ||
value: val, | ||
multilineTag: multilineTag, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
} // Internal values are updated synchronously, unlike props and state. | ||
if (!isSelected) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
var clipboardData = event.clipboardData; | ||
var items = clipboardData.items, | ||
files = clipboardData.files; // In Edge these properties can be null instead of undefined, so a more | ||
// rigorous test is required over using default values. | ||
var _value = (0, _element.useRef)(value); | ||
items = (0, _lodash.isNil)(items) ? [] : items; | ||
files = (0, _lodash.isNil)(files) ? [] : files; | ||
var plainText = ''; | ||
var html = ''; // IE11 only supports `Text` as an argument for `getData` and will | ||
// otherwise throw an invalid argument error, so we try the standard | ||
// arguments first, then fallback to `Text` if they fail. | ||
var record = (0, _element.useRef)((0, _element.useMemo)(function () { | ||
var initialRecord = formatToValue(value); | ||
initialRecord.start = selectionStart; | ||
initialRecord.end = selectionEnd; | ||
return initialRecord; | ||
}, [])); | ||
try { | ||
plainText = clipboardData.getData('text/plain'); | ||
html = clipboardData.getData('text/html'); | ||
} catch (error1) { | ||
try { | ||
html = clipboardData.getData('Text'); | ||
} catch (error2) { | ||
// Some browsers like UC Browser paste plain text by default and | ||
// don't support clipboardData at all, so allow default | ||
// behaviour. | ||
return; | ||
} | ||
} | ||
function createRecord() { | ||
var selection = getWin().getSelection(); | ||
var range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; | ||
return (0, _create.create)({ | ||
element: ref.current, | ||
range: range, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
__unstableIsEditableTree: true, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
}); | ||
} | ||
event.preventDefault(); // Allows us to ask for this information when we get a report. | ||
function applyRecord(newRecord) { | ||
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
domOnly = _ref2.domOnly; | ||
window.console.log('Received HTML:\n\n', html); | ||
window.console.log('Received plain text:\n\n', plainText); | ||
(0, _toDom.apply)({ | ||
value: newRecord, | ||
current: ref.current, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
prepareEditableTree: createPrepareEditableTree(remainingProps, 'format_prepare_functions'), | ||
__unstableDomOnly: domOnly, | ||
placeholder: placeholder | ||
}); | ||
} | ||
/** | ||
* Handles a paste event. | ||
* | ||
* Saves the pasted data as plain text in `pastedPlainText`. | ||
* | ||
* @param {ClipboardEvent} event The paste event. | ||
*/ | ||
if (__unstableDisableFormats) { | ||
this.onChange((0, _insert.insert)(this.record, plainText)); | ||
return; | ||
} | ||
var record = this.record; | ||
var transformed = formatTypes.reduce(function (accumlator, _ref3) { | ||
var __unstablePasteRule = _ref3.__unstablePasteRule; | ||
function handlePaste(event) { | ||
if (!isSelected) { | ||
event.preventDefault(); | ||
return; | ||
} | ||
// Only allow one transform. | ||
if (__unstablePasteRule && accumlator === record) { | ||
accumlator = __unstablePasteRule(record, { | ||
html: html, | ||
plainText: plainText | ||
}); | ||
} | ||
var clipboardData = event.clipboardData; | ||
var items = clipboardData.items, | ||
files = clipboardData.files; // In Edge these properties can be null instead of undefined, so a more | ||
// rigorous test is required over using default values. | ||
return accumlator; | ||
}, record); | ||
items = (0, _lodash.isNil)(items) ? [] : items; | ||
files = (0, _lodash.isNil)(files) ? [] : files; | ||
var plainText = ''; | ||
var html = ''; // IE11 only supports `Text` as an argument for `getData` and will | ||
// otherwise throw an invalid argument error, so we try the standard | ||
// arguments first, then fallback to `Text` if they fail. | ||
if (transformed !== record) { | ||
this.onChange(transformed); | ||
try { | ||
plainText = clipboardData.getData('text/plain'); | ||
html = clipboardData.getData('text/html'); | ||
} catch (error1) { | ||
try { | ||
html = clipboardData.getData('Text'); | ||
} catch (error2) { | ||
// Some browsers like UC Browser paste plain text by default and | ||
// don't support clipboardData at all, so allow default | ||
// behaviour. | ||
return; | ||
} | ||
} | ||
if (onPaste) { | ||
files = Array.from(files); | ||
Array.from(items).forEach(function (item) { | ||
if (!item.getAsFile) { | ||
return; | ||
} | ||
event.preventDefault(); // Allows us to ask for this information when we get a report. | ||
var file = item.getAsFile(); | ||
window.console.log('Received HTML:\n\n', html); | ||
window.console.log('Received plain text:\n\n', plainText); | ||
if (!file) { | ||
return; | ||
} | ||
if (disableFormats) { | ||
handleChange((0, _insert.insert)(record.current, plainText)); | ||
return; | ||
} | ||
var name = file.name, | ||
type = file.type, | ||
size = file.size; | ||
var transformed = formatTypes.reduce(function (accumlator, _ref3) { | ||
var __unstablePasteRule = _ref3.__unstablePasteRule; | ||
if (!(0, _lodash.find)(files, { | ||
name: name, | ||
type: type, | ||
size: size | ||
})) { | ||
files.push(file); | ||
} | ||
}); | ||
onPaste({ | ||
value: this.removeEditorOnlyFormats(record), | ||
onChange: this.onChange, | ||
// Only allow one transform. | ||
if (__unstablePasteRule && accumlator === record.current) { | ||
accumlator = __unstablePasteRule(record.current, { | ||
html: html, | ||
plainText: plainText, | ||
files: files, | ||
activeFormats: activeFormats | ||
plainText: plainText | ||
}); | ||
} | ||
return accumlator; | ||
}, record.current); | ||
if (transformed !== record.current) { | ||
handleChange(transformed); | ||
return; | ||
} | ||
/** | ||
* Handles a focus event on the contenteditable field, calling the | ||
* `unstableOnFocus` prop callback if one is defined. The callback does not | ||
* receive any arguments. | ||
* | ||
* This is marked as a private API and the `unstableOnFocus` prop is not | ||
* documented, as the current requirements where it is used are subject to | ||
* future refactoring following `isSelected` handling. | ||
* | ||
* In contrast with `setFocusedElement`, this is only triggered in response | ||
* to focus within the contenteditable field, whereas `setFocusedElement` | ||
* is triggered on focus within any `RichText` descendent element. | ||
* | ||
* @see setFocusedElement | ||
* | ||
* @private | ||
*/ | ||
}, { | ||
key: "onFocus", | ||
value: function onFocus() { | ||
var unstableOnFocus = this.props.unstableOnFocus; | ||
if (onPaste) { | ||
files = Array.from(files); | ||
Array.from(items).forEach(function (item) { | ||
if (!item.getAsFile) { | ||
return; | ||
} | ||
if (unstableOnFocus) { | ||
unstableOnFocus(); | ||
} | ||
var file = item.getAsFile(); | ||
if (!this.props.__unstableIsSelected) { | ||
// We know for certain that on focus, the old selection is invalid. It | ||
// will be recalculated on the next mouseup, keyup, or touchend event. | ||
var index = undefined; | ||
var activeFormats = EMPTY_ACTIVE_FORMATS; | ||
this.record = _objectSpread({}, this.record, { | ||
start: index, | ||
end: index, | ||
activeFormats: activeFormats | ||
}); | ||
this.props.onSelectionChange(index, index); | ||
this.setState({ | ||
activeFormats: activeFormats | ||
}); | ||
} else { | ||
this.props.onSelectionChange(this.record.start, this.record.end); | ||
this.setState({ | ||
activeFormats: (0, _getActiveFormats.getActiveFormats)(_objectSpread({}, this.record, { | ||
activeFormats: undefined | ||
}), EMPTY_ACTIVE_FORMATS) | ||
}); | ||
} // Update selection as soon as possible, which is at the next animation | ||
// frame. The event listener for selection changes may be added too late | ||
// at this point, but this focus event is still too early to calculate | ||
// the selection. | ||
if (!file) { | ||
return; | ||
} | ||
var name = file.name, | ||
type = file.type, | ||
size = file.size; | ||
this.rafId = this.getWindow().requestAnimationFrame(this.onSelectionChange); | ||
this.getDocument().addEventListener('selectionchange', this.onSelectionChange); | ||
if (!(0, _lodash.find)(files, { | ||
name: name, | ||
type: type, | ||
size: size | ||
})) { | ||
files.push(file); | ||
} | ||
}); | ||
onPaste({ | ||
value: removeEditorOnlyFormats(record.current), | ||
onChange: handleChange, | ||
html: html, | ||
plainText: plainText, | ||
files: files, | ||
activeFormats: activeFormats | ||
}); | ||
} | ||
} | ||
/** | ||
* Handles delete on keydown: | ||
* - outdent list items, | ||
* - delete content if everything is selected, | ||
* - trigger the onDelete prop when selection is uncollapsed and at an edge. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (this.props.setFocusedElement) { | ||
(0, _deprecated.default)('wp.blockEditor.RichText setFocusedElement prop', { | ||
alternative: 'selection state from the block editor store.' | ||
}); | ||
this.props.setFocusedElement(this.props.instanceId); | ||
} | ||
function handleDelete(event) { | ||
var keyCode = event.keyCode; | ||
if (keyCode !== _keycodes.DELETE && keyCode !== _keycodes.BACKSPACE && keyCode !== _keycodes.ESCAPE) { | ||
return; | ||
} | ||
}, { | ||
key: "onBlur", | ||
value: function onBlur() { | ||
this.getDocument().removeEventListener('selectionchange', this.onSelectionChange); | ||
if (didAutomaticChange) { | ||
event.preventDefault(); | ||
undo(); | ||
return; | ||
} | ||
/** | ||
* Handle input on the next selection change event. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic input event. | ||
*/ | ||
}, { | ||
key: "onInput", | ||
value: 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 (this.isComposing) { | ||
return; | ||
} | ||
if (keyCode === _keycodes.ESCAPE) { | ||
return; | ||
} | ||
var inputType; | ||
var currentValue = createRecord(); | ||
var start = currentValue.start, | ||
end = currentValue.end, | ||
text = currentValue.text; | ||
var isReverse = keyCode === _keycodes.BACKSPACE; // Always handle full content deletion ourselves. | ||
if (event) { | ||
inputType = event.inputType; | ||
} | ||
if (start === 0 && end !== 0 && end === text.length) { | ||
handleChange((0, _remove.remove)(currentValue)); | ||
event.preventDefault(); | ||
return; | ||
} | ||
if (!inputType && event && event.nativeEvent) { | ||
inputType = event.nativeEvent.inputType; | ||
} // The browser formatted something or tried to insert HTML. | ||
// Overwrite it. It will be handled later by the format library if | ||
// needed. | ||
if (multilineTag) { | ||
var newValue; // Check to see if we should remove the first item if empty. | ||
if (isReverse && currentValue.start === 0 && currentValue.end === 0 && (0, _isEmpty.isEmptyLine)(currentValue)) { | ||
newValue = (0, _removeLineSeparator.removeLineSeparator)(currentValue, !isReverse); | ||
} else { | ||
newValue = (0, _removeLineSeparator.removeLineSeparator)(currentValue, isReverse); | ||
} | ||
if (inputType && (inputType.indexOf('format') === 0 || INSERTION_INPUT_TYPES_TO_IGNORE.has(inputType))) { | ||
this.applyRecord(this.record); | ||
if (newValue) { | ||
handleChange(newValue); | ||
event.preventDefault(); | ||
return; | ||
} | ||
} // Only process delete if the key press occurs at an uncollapsed edge. | ||
var value = this.createRecord(); | ||
var _this$record = this.record, | ||
start = _this$record.start, | ||
_this$record$activeFo = _this$record.activeFormats, | ||
activeFormats = _this$record$activeFo === void 0 ? [] : _this$record$activeFo; // Update the formats between the last and new caret position. | ||
var change = (0, _updateFormats.updateFormats)({ | ||
value: value, | ||
start: start, | ||
end: value.start, | ||
formats: activeFormats | ||
}); | ||
this.onChange(change, { | ||
withoutHistory: true | ||
}); | ||
var _this$props4 = this.props, | ||
inputRule = _this$props4.__unstableInputRule, | ||
markAutomaticChange = _this$props4.__unstableMarkAutomaticChange, | ||
allowPrefixTransformations = _this$props4.__unstableAllowPrefixTransformations, | ||
formatTypes = _this$props4.formatTypes, | ||
setTimeout = _this$props4.setTimeout, | ||
clearTimeout = _this$props4.clearTimeout; // Create an undo level when input stops for over a second. | ||
if (!onDelete || !(0, _isCollapsed.isCollapsed)(currentValue) || activeFormats.length || isReverse && start !== 0 || !isReverse && end !== text.length) { | ||
return; | ||
} | ||
clearTimeout(this.onInput.timeout); | ||
this.onInput.timeout = setTimeout(this.onCreateUndoLevel, 1000); // Only run input rules when inserting text. | ||
onDelete({ | ||
isReverse: isReverse, | ||
value: currentValue | ||
}); | ||
event.preventDefault(); | ||
} | ||
/** | ||
* Triggers the `onEnter` prop on keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (inputType !== 'insertText') { | ||
return; | ||
} | ||
if (allowPrefixTransformations && inputRule) { | ||
inputRule(change, this.valueToFormat); | ||
} | ||
function handleEnter(event) { | ||
if (event.keyCode !== _keycodes.ENTER) { | ||
return; | ||
} | ||
var transformed = formatTypes.reduce(function (accumlator, _ref4) { | ||
var __unstableInputRule = _ref4.__unstableInputRule; | ||
event.preventDefault(); | ||
if (__unstableInputRule) { | ||
accumlator = __unstableInputRule(accumlator); | ||
} | ||
if (!onEnter) { | ||
return; | ||
} | ||
return accumlator; | ||
}, change); | ||
onEnter({ | ||
value: removeEditorOnlyFormats(createRecord()), | ||
onChange: handleChange, | ||
shiftKey: event.shiftKey | ||
}); | ||
} | ||
/** | ||
* Indents list items on space keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (transformed !== change) { | ||
this.onCreateUndoLevel(); | ||
this.onChange(_objectSpread({}, transformed, { | ||
activeFormats: activeFormats | ||
})); | ||
markAutomaticChange(); | ||
} | ||
function handleSpace(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
if ( // Only override when no modifiers are pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== _keycodes.SPACE || multilineTag !== 'li') { | ||
return; | ||
} | ||
}, { | ||
key: "onCompositionStart", | ||
value: function onCompositionStart() { | ||
this.isComposing = true; // Do not update the selection when characters are being composed as | ||
// this rerenders the component and might distroy internal browser | ||
// editing state. | ||
this.getDocument().removeEventListener('selectionchange', this.onSelectionChange); | ||
var currentValue = createRecord(); | ||
if (!(0, _isCollapsed.isCollapsed)(currentValue)) { | ||
return; | ||
} | ||
}, { | ||
key: "onCompositionEnd", | ||
value: function onCompositionEnd() { | ||
this.isComposing = false; // Ensure the value is up-to-date for browsers that don't emit a final | ||
// input event after composition. | ||
this.onInput({ | ||
inputType: 'insertText' | ||
}); // Tracking selection changes can be resumed. | ||
var text = currentValue.text, | ||
start = currentValue.start; | ||
var characterBefore = text[start - 1]; // The caret must be at the start of a line. | ||
this.getDocument().addEventListener('selectionchange', this.onSelectionChange); | ||
if (characterBefore && characterBefore !== _specialCharacters.LINE_SEPARATOR) { | ||
return; | ||
} | ||
/** | ||
* Syncs the selection to local state. A callback for the `selectionchange` | ||
* native events, `keyup`, `mouseup` and `touchend` synthetic events, and | ||
* animation frames after the `focus` event. | ||
* | ||
* @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event | ||
*/ | ||
}, { | ||
key: "onSelectionChange", | ||
value: function onSelectionChange(event) { | ||
if (event.type !== 'selectionchange' && !this.props.__unstableIsSelected) { | ||
return; | ||
} | ||
handleChange((0, _indentListItems.indentListItems)(currentValue, { | ||
type: multilineRootTag | ||
})); | ||
event.preventDefault(); | ||
} | ||
/** | ||
* Handles horizontal keyboard navigation when no modifiers are pressed. The | ||
* navigation is handled separately to move correctly around format | ||
* boundaries. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
if (this.props.disabled) { | ||
return; | ||
} // In case of a keyboard event, ignore selection changes during | ||
// composition. | ||
function handleHorizontalNavigation(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
if (this.isComposing) { | ||
return; | ||
} | ||
if ( // Only override left and right keys without modifiers pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== _keycodes.LEFT && keyCode !== _keycodes.RIGHT) { | ||
return; | ||
} | ||
var _this$createRecord = this.createRecord(), | ||
start = _this$createRecord.start, | ||
end = _this$createRecord.end, | ||
text = _this$createRecord.text; | ||
var _record$current = record.current, | ||
text = _record$current.text, | ||
formats = _record$current.formats, | ||
start = _record$current.start, | ||
end = _record$current.end, | ||
_record$current$activ = _record$current.activeFormats, | ||
currentActiveFormats = _record$current$activ === void 0 ? [] : _record$current$activ; | ||
var collapsed = (0, _isCollapsed.isCollapsed)(record.current); // To do: ideally, we should look at visual position instead. | ||
var value = this.record; // Fallback mechanism for IE11, which doesn't support the input event. | ||
// Any input results in a selection change. | ||
var _getWin$getComputedSt = getWin().getComputedStyle(ref.current), | ||
direction = _getWin$getComputedSt.direction; | ||
if (text !== value.text) { | ||
this.onInput(); | ||
var reverseKey = direction === 'rtl' ? _keycodes.RIGHT : _keycodes.LEFT; | ||
var 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 (start === value.start && end === value.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 (value.text.length === 0 && start === 0) { | ||
fixPlaceholderSelection(this.getWindow()); | ||
} | ||
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. | ||
var _this$props5 = this.props, | ||
isCaretWithinFormattedText = _this$props5.__unstableIsCaretWithinFormattedText, | ||
onEnterFormattedText = _this$props5.__unstableOnEnterFormattedText, | ||
onExitFormattedText = _this$props5.__unstableOnExitFormattedText; | ||
var newValue = _objectSpread({}, value, { | ||
start: start, | ||
end: end, | ||
// Allow `getActiveFormats` to get new `activeFormats`. | ||
activeFormats: undefined | ||
}); | ||
if (!collapsed) { | ||
return; | ||
} // In all other cases, prevent default behaviour. | ||
var activeFormats = (0, _getActiveFormats.getActiveFormats)(newValue, EMPTY_ACTIVE_FORMATS); // Update the value with the new active formats. | ||
newValue.activeFormats = activeFormats; | ||
event.preventDefault(); | ||
var formatsBefore = formats[start - 1] || EMPTY_ACTIVE_FORMATS; | ||
var formatsAfter = formats[start] || EMPTY_ACTIVE_FORMATS; | ||
var newActiveFormatsLength = currentActiveFormats.length; | ||
var source = formatsAfter; | ||
if (!isCaretWithinFormattedText && activeFormats.length) { | ||
onEnterFormattedText(); | ||
} else if (isCaretWithinFormattedText && !activeFormats.length) { | ||
onExitFormattedText(); | ||
} // It is important that the internal value is updated first, | ||
// otherwise the value will be wrong on render! | ||
if (formatsBefore.length > formatsAfter.length) { | ||
source = formatsBefore; | ||
} // If the amount of formats before the caret and after the caret is | ||
// different, the caret is at a format boundary. | ||
this.record = newValue; | ||
this.applyRecord(newValue, { | ||
domOnly: true | ||
}); | ||
this.props.onSelectionChange(start, end); | ||
this.setState({ | ||
activeFormats: activeFormats | ||
}); | ||
} | ||
/** | ||
* Sync the value to global state. The node tree and selection will also be | ||
* updated if differences are found. | ||
* | ||
* @param {Object} record The record to sync and apply. | ||
* @param {Object} $2 Named options. | ||
* @param {boolean} $2.withoutHistory If true, no undo level will be | ||
* created. | ||
*/ | ||
if (formatsBefore.length < formatsAfter.length) { | ||
if (!isReverse && currentActiveFormats.length < formatsAfter.length) { | ||
newActiveFormatsLength++; | ||
} | ||
}, { | ||
key: "onChange", | ||
value: function onChange(record) { | ||
var _ref5 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
withoutHistory = _ref5.withoutHistory; | ||
if (isReverse && currentActiveFormats.length > formatsBefore.length) { | ||
newActiveFormatsLength--; | ||
} | ||
} else if (formatsBefore.length > formatsAfter.length) { | ||
if (!isReverse && currentActiveFormats.length > formatsAfter.length) { | ||
newActiveFormatsLength--; | ||
} | ||
if (this.props.__unstableDisableFormats) { | ||
record.formats = Array(record.text.length); | ||
record.replacements = Array(record.text.length); | ||
if (isReverse && currentActiveFormats.length < formatsBefore.length) { | ||
newActiveFormatsLength++; | ||
} | ||
} | ||
this.applyRecord(record); | ||
var start = record.start, | ||
end = record.end, | ||
_record$activeFormats = record.activeFormats, | ||
activeFormats = _record$activeFormats === void 0 ? [] : _record$activeFormats; | ||
var changeHandlers = (0, _lodash.pickBy)(this.props, function (v, key) { | ||
return key.startsWith('format_on_change_functions_'); | ||
}); | ||
Object.values(changeHandlers).forEach(function (changeHandler) { | ||
changeHandler(record.formats, record.text); | ||
}); | ||
this.value = this.valueToFormat(record); | ||
this.record = record; // Selection must be updated first, so it is recorded in history when | ||
// the content change happens. | ||
if (newActiveFormatsLength !== currentActiveFormats.length) { | ||
var _newActiveFormats = source.slice(0, newActiveFormatsLength); | ||
this.props.onSelectionChange(start, end); | ||
this.props.onChange(this.value); | ||
this.setState({ | ||
activeFormats: activeFormats | ||
var _newValue = _objectSpread({}, record.current, { | ||
activeFormats: _newActiveFormats | ||
}); | ||
if (!withoutHistory) { | ||
this.onCreateUndoLevel(); | ||
} | ||
record.current = _newValue; | ||
applyRecord(_newValue); | ||
setActiveFormats(_newActiveFormats); | ||
return; | ||
} | ||
}, { | ||
key: "onCreateUndoLevel", | ||
value: function onCreateUndoLevel() { | ||
// If the content is the same, no level needs to be created. | ||
if (this.lastHistoryValue === this.value) { | ||
return; | ||
} | ||
this.props.__unstableOnCreateUndoLevel(); | ||
var newPos = start + (isReverse ? -1 : 1); | ||
var newActiveFormats = isReverse ? formatsBefore : formatsAfter; | ||
this.lastHistoryValue = this.value; | ||
} | ||
/** | ||
* Handles delete on keydown: | ||
* - outdent list items, | ||
* - delete content if everything is selected, | ||
* - trigger the onDelete prop when selection is uncollapsed and at an edge. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
var newValue = _objectSpread({}, record.current, { | ||
start: newPos, | ||
end: newPos, | ||
activeFormats: newActiveFormats | ||
}); | ||
}, { | ||
key: "handleDelete", | ||
value: function handleDelete(event) { | ||
var keyCode = event.keyCode; | ||
record.current = newValue; | ||
applyRecord(newValue); | ||
onSelectionChange(newPos, newPos); | ||
setActiveFormats(newActiveFormats); | ||
} | ||
if (keyCode !== _keycodes.DELETE && keyCode !== _keycodes.BACKSPACE && keyCode !== _keycodes.ESCAPE) { | ||
return; | ||
} | ||
function handleKeyDown(event) { | ||
if (event.defaultPrevented) { | ||
return; | ||
} | ||
if (this.props.__unstableDidAutomaticChange) { | ||
event.preventDefault(); | ||
handleDelete(event); | ||
handleEnter(event); | ||
handleSpace(event); | ||
handleHorizontalNavigation(event); | ||
} | ||
this.props.__unstableUndo(); | ||
var lastHistoryValue = (0, _element.useRef)(value); | ||
return; | ||
} | ||
function createUndoLevel() { | ||
// If the content is the same, no level needs to be created. | ||
if (lastHistoryValue.current === _value.current) { | ||
return; | ||
} | ||
if (keyCode === _keycodes.ESCAPE) { | ||
return; | ||
} | ||
onCreateUndoLevel(); | ||
lastHistoryValue.current = _value.current; | ||
} | ||
var _this$props6 = this.props, | ||
onDelete = _this$props6.onDelete, | ||
multilineTag = _this$props6.__unstableMultilineTag; | ||
var _this$state$activeFor2 = this.state.activeFormats, | ||
activeFormats = _this$state$activeFor2 === void 0 ? [] : _this$state$activeFor2; | ||
var value = this.createRecord(); | ||
var start = value.start, | ||
end = value.end, | ||
text = value.text; | ||
var isReverse = keyCode === _keycodes.BACKSPACE; // Always handle full content deletion ourselves. | ||
var isComposing = (0, _element.useRef)(false); | ||
var timeout = (0, _element.useRef)(); | ||
/** | ||
* Handle input on the next selection change event. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic input event. | ||
*/ | ||
if (start === 0 && end !== 0 && end === text.length) { | ||
this.onChange((0, _remove.remove)(value)); | ||
event.preventDefault(); | ||
return; | ||
} | ||
function handleInput(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.current) { | ||
return; | ||
} | ||
if (multilineTag) { | ||
var newValue; // Check to see if we should remove the first item if empty. | ||
var inputType; | ||
if (isReverse && value.start === 0 && value.end === 0 && (0, _isEmpty.isEmptyLine)(value)) { | ||
newValue = (0, _removeLineSeparator.removeLineSeparator)(value, !isReverse); | ||
} else { | ||
newValue = (0, _removeLineSeparator.removeLineSeparator)(value, isReverse); | ||
} | ||
if (event) { | ||
inputType = event.inputType; | ||
} | ||
if (newValue) { | ||
this.onChange(newValue); | ||
event.preventDefault(); | ||
return; | ||
} | ||
} // Only process delete if the key press occurs at an uncollapsed edge. | ||
if (!inputType && event && event.nativeEvent) { | ||
inputType = event.nativeEvent.inputType; | ||
} // The browser formatted something or tried to insert HTML. | ||
// Overwrite it. It will be handled later by the format library if | ||
// needed. | ||
if (!onDelete || !(0, _isCollapsed.isCollapsed)(value) || activeFormats.length || isReverse && start !== 0 || !isReverse && end !== text.length) { | ||
return; | ||
} | ||
onDelete({ | ||
isReverse: isReverse, | ||
value: value | ||
}); | ||
event.preventDefault(); | ||
if (inputType && (inputType.indexOf('format') === 0 || INSERTION_INPUT_TYPES_TO_IGNORE.has(inputType))) { | ||
applyRecord(record.current); | ||
return; | ||
} | ||
/** | ||
* Triggers the `onEnter` prop on keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
}, { | ||
key: "handleEnter", | ||
value: function handleEnter(event) { | ||
if (event.keyCode !== _keycodes.ENTER) { | ||
return; | ||
} | ||
var currentValue = createRecord(); | ||
var _record$current2 = record.current, | ||
start = _record$current2.start, | ||
_record$current2$acti = _record$current2.activeFormats, | ||
oldActiveFormats = _record$current2$acti === void 0 ? [] : _record$current2$acti; // Update the formats between the last and new caret position. | ||
event.preventDefault(); | ||
var onEnter = this.props.onEnter; | ||
var change = (0, _updateFormats.updateFormats)({ | ||
value: currentValue, | ||
start: start, | ||
end: currentValue.start, | ||
formats: oldActiveFormats | ||
}); | ||
handleChange(change, { | ||
withoutHistory: true | ||
}); // Create an undo level when input stops for over a second. | ||
if (!onEnter) { | ||
return; | ||
} | ||
getWin().clearTimeout(timeout.current); | ||
timeout.current = getWin().setTimeout(createUndoLevel, 1000); // Only run input rules when inserting text. | ||
onEnter({ | ||
value: this.removeEditorOnlyFormats(this.createRecord()), | ||
onChange: this.onChange, | ||
shiftKey: event.shiftKey | ||
}); | ||
if (inputType !== 'insertText') { | ||
return; | ||
} | ||
/** | ||
* Indents list items on space keydown. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
}, { | ||
key: "handleSpace", | ||
value: function handleSpace(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
var _this$props7 = this.props, | ||
multilineRootTag = _this$props7.__unstableMultilineRootTag, | ||
multilineTag = _this$props7.__unstableMultilineTag; | ||
if (allowPrefixTransformations && inputRule) { | ||
inputRule(change, valueToFormat); | ||
} | ||
if ( // Only override when no modifiers are pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== _keycodes.SPACE || multilineTag !== 'li') { | ||
return; | ||
} | ||
var transformed = formatTypes.reduce(function (accumlator, _ref4) { | ||
var __unstableInputRule = _ref4.__unstableInputRule; | ||
var value = this.createRecord(); | ||
if (!(0, _isCollapsed.isCollapsed)(value)) { | ||
return; | ||
if (__unstableInputRule) { | ||
accumlator = __unstableInputRule(accumlator); | ||
} | ||
var text = value.text, | ||
start = value.start; | ||
var characterBefore = text[start - 1]; // The caret must be at the start of a line. | ||
return accumlator; | ||
}, change); | ||
if (characterBefore && characterBefore !== _specialCharacters.LINE_SEPARATOR) { | ||
return; | ||
} | ||
this.onChange((0, _indentListItems.indentListItems)(value, { | ||
type: multilineRootTag | ||
if (transformed !== change) { | ||
createUndoLevel(); | ||
handleChange(_objectSpread({}, transformed, { | ||
activeFormats: oldActiveFormats | ||
})); | ||
event.preventDefault(); | ||
markAutomaticChange(); | ||
} | ||
/** | ||
* Handles horizontal keyboard navigation when no modifiers are pressed. The | ||
* navigation is handled separately to move correctly around format | ||
* boundaries. | ||
* | ||
* @param {WPSyntheticEvent} event A synthetic keyboard event. | ||
*/ | ||
} | ||
}, { | ||
key: "handleHorizontalNavigation", | ||
value: function handleHorizontalNavigation(event) { | ||
var keyCode = event.keyCode, | ||
shiftKey = event.shiftKey, | ||
altKey = event.altKey, | ||
metaKey = event.metaKey, | ||
ctrlKey = event.ctrlKey; | ||
function handleCompositionStart() { | ||
isComposing.current = true; // Do not update the selection when characters are being composed as | ||
// this rerenders the component and might distroy internal browser | ||
// editing state. | ||
if ( // Only override left and right keys without modifiers pressed. | ||
shiftKey || altKey || metaKey || ctrlKey || keyCode !== _keycodes.LEFT && keyCode !== _keycodes.RIGHT) { | ||
return; | ||
} | ||
getDoc().removeEventListener('selectionchange', handleSelectionChange); | ||
} | ||
var value = this.record; | ||
var text = value.text, | ||
formats = value.formats, | ||
start = value.start, | ||
end = value.end, | ||
_value$activeFormats = value.activeFormats, | ||
activeFormats = _value$activeFormats === void 0 ? [] : _value$activeFormats; | ||
var collapsed = (0, _isCollapsed.isCollapsed)(value); // To do: ideally, we should look at visual position instead. | ||
function handleCompositionEnd() { | ||
isComposing.current = false; // Ensure the value is up-to-date for browsers that don't emit a final | ||
// input event after composition. | ||
var _this$getWindow$getCo = this.getWindow().getComputedStyle(this.props.forwardedRef.current), | ||
direction = _this$getWindow$getCo.direction; | ||
handleInput({ | ||
inputType: 'insertText' | ||
}); // Tracking selection changes can be resumed. | ||
var reverseKey = direction === 'rtl' ? _keycodes.RIGHT : _keycodes.LEFT; | ||
var 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. | ||
getDoc().addEventListener('selectionchange', handleSelectionChange); | ||
} | ||
if (collapsed && activeFormats.length === 0) { | ||
if (start === 0 && isReverse) { | ||
return; | ||
} | ||
var didMount = (0, _element.useRef)(false); | ||
/** | ||
* Syncs the selection to local state. A callback for the `selectionchange` | ||
* native events, `keyup`, `mouseup` and `touchend` synthetic events, and | ||
* animation frames after the `focus` event. | ||
* | ||
* @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event | ||
*/ | ||
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. | ||
function handleSelectionChange(event) { | ||
if (!ref.current) { | ||
return; | ||
} | ||
if (document.activeElement !== ref.current) { | ||
return; | ||
} | ||
if (!collapsed) { | ||
return; | ||
} // In all other cases, prevent default behaviour. | ||
if (event.type !== 'selectionchange' && !isSelected) { | ||
return; | ||
} | ||
if (disabled) { | ||
return; | ||
} // In case of a keyboard event, ignore selection changes during | ||
// composition. | ||
event.preventDefault(); | ||
var formatsBefore = formats[start - 1] || EMPTY_ACTIVE_FORMATS; | ||
var formatsAfter = formats[start] || EMPTY_ACTIVE_FORMATS; | ||
var newActiveFormatsLength = activeFormats.length; | ||
var source = formatsAfter; | ||
if (formatsBefore.length > formatsAfter.length) { | ||
source = formatsBefore; | ||
} // If the amount of formats before the caret and after the caret is | ||
// different, the caret is at a format boundary. | ||
if (isComposing.current) { | ||
return; | ||
} | ||
var _createRecord = createRecord(), | ||
start = _createRecord.start, | ||
end = _createRecord.end, | ||
text = _createRecord.text; | ||
if (formatsBefore.length < formatsAfter.length) { | ||
if (!isReverse && activeFormats.length < formatsAfter.length) { | ||
newActiveFormatsLength++; | ||
} | ||
var oldRecord = record.current; // Fallback mechanism for IE11, which doesn't support the input event. | ||
// Any input results in a selection change. | ||
if (isReverse && activeFormats.length > formatsBefore.length) { | ||
newActiveFormatsLength--; | ||
} | ||
} else if (formatsBefore.length > formatsAfter.length) { | ||
if (!isReverse && activeFormats.length > formatsAfter.length) { | ||
newActiveFormatsLength--; | ||
} | ||
if (text !== oldRecord.text) { | ||
handleInput(); | ||
return; | ||
} | ||
if (isReverse && activeFormats.length < formatsBefore.length) { | ||
newActiveFormatsLength++; | ||
} | ||
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(getWin()); | ||
} | ||
if (newActiveFormatsLength !== activeFormats.length) { | ||
var _newActiveFormats = source.slice(0, newActiveFormatsLength); | ||
return; | ||
} | ||
var _newValue = _objectSpread({}, value, { | ||
activeFormats: _newActiveFormats | ||
}); | ||
var newValue = _objectSpread({}, oldRecord, { | ||
start: start, | ||
end: end, | ||
// Allow `getActiveFormats` to get new `activeFormats`. | ||
activeFormats: undefined | ||
}); | ||
this.record = _newValue; | ||
this.applyRecord(_newValue); | ||
this.setState({ | ||
activeFormats: _newActiveFormats | ||
}); | ||
return; | ||
} | ||
var newActiveFormats = (0, _getActiveFormats.getActiveFormats)(newValue, EMPTY_ACTIVE_FORMATS); // Update the value with the new active formats. | ||
var newPos = start + (isReverse ? -1 : 1); | ||
var newActiveFormats = isReverse ? formatsBefore : formatsAfter; | ||
newValue.activeFormats = newActiveFormats; | ||
var newValue = _objectSpread({}, value, { | ||
start: newPos, | ||
end: newPos, | ||
activeFormats: newActiveFormats | ||
}); | ||
if (!isCaretWithinFormattedText && newActiveFormats.length) { | ||
onEnterFormattedText(); | ||
} else if (isCaretWithinFormattedText && !newActiveFormats.length) { | ||
onExitFormattedText(); | ||
} // It is important that the internal value is updated first, | ||
// otherwise the value will be wrong on render! | ||
this.record = newValue; | ||
this.applyRecord(newValue); | ||
this.props.onSelectionChange(newPos, newPos); | ||
this.setState({ | ||
activeFormats: newActiveFormats | ||
}); | ||
} | ||
/** | ||
* Select object when they are clicked. The browser will not set any | ||
* selection when clicking e.g. an image. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic mousedown or touchstart event. | ||
*/ | ||
}, { | ||
key: "onPointerDown", | ||
value: function onPointerDown(event) { | ||
var target = event.target; // If the child element has no text content, it must be an object. | ||
record.current = newValue; | ||
applyRecord(newValue, { | ||
domOnly: true | ||
}); | ||
onSelectionChange(start, end); | ||
setActiveFormats(newActiveFormats); | ||
} | ||
/** | ||
* 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. | ||
* @param {Object} $2 Named options. | ||
* @param {boolean} $2.withoutHistory If true, no undo level will be | ||
* created. | ||
*/ | ||
if (target === this.props.forwardedRef.current || target.textContent) { | ||
return; | ||
} | ||
var parentNode = target.parentNode; | ||
var index = Array.from(parentNode.childNodes).indexOf(target); | ||
var range = this.getDocument().createRange(); | ||
var selection = this.getWindow().getSelection(); | ||
range.setStart(target.parentNode, index); | ||
range.setEnd(target.parentNode, index + 1); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
function handleChange(newRecord) { | ||
var _ref5 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, | ||
withoutHistory = _ref5.withoutHistory; | ||
if (disableFormats) { | ||
newRecord.formats = Array(newRecord.text.length); | ||
newRecord.replacements = Array(newRecord.text.length); | ||
} | ||
}, { | ||
key: "componentDidUpdate", | ||
value: function componentDidUpdate(prevProps) { | ||
var _this$props8 = this.props, | ||
tagName = _this$props8.tagName, | ||
value = _this$props8.value, | ||
selectionStart = _this$props8.selectionStart, | ||
selectionEnd = _this$props8.selectionEnd, | ||
placeholder = _this$props8.placeholder, | ||
isSelected = _this$props8.__unstableIsSelected; // Check if tag name changed. | ||
var shouldReapply = tagName !== prevProps.tagName; // Check if the content changed. | ||
applyRecord(newRecord); | ||
var start = newRecord.start, | ||
end = newRecord.end, | ||
_newRecord$activeForm = newRecord.activeFormats, | ||
newActiveFormats = _newRecord$activeForm === void 0 ? [] : _newRecord$activeForm; | ||
var changeHandlers = (0, _lodash.pickBy)(remainingProps, function (v, key) { | ||
return key.startsWith('format_on_change_functions_'); | ||
}); | ||
Object.values(changeHandlers).forEach(function (changeHandler) { | ||
changeHandler(newRecord.formats, newRecord.text); | ||
}); | ||
_value.current = valueToFormat(newRecord); | ||
record.current = newRecord; // Selection must be updated first, so it is recorded in history when | ||
// the content change happens. | ||
shouldReapply = shouldReapply || value !== prevProps.value && value !== this.value; | ||
var selectionChanged = selectionStart !== prevProps.selectionStart && selectionStart !== this.record.start || selectionEnd !== prevProps.selectionEnd && selectionEnd !== this.record.end; // Check if the selection changed. | ||
onSelectionChange(start, end); | ||
onChange(_value.current); | ||
setActiveFormats(newActiveFormats); | ||
shouldReapply = shouldReapply || isSelected && !prevProps.isSelected && selectionChanged; | ||
var prefix = 'format_prepare_props_'; | ||
if (!withoutHistory) { | ||
createUndoLevel(); | ||
} | ||
} | ||
/** | ||
* Select object when they are clicked. The browser will not set any | ||
* selection when clicking e.g. an image. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic mousedown or touchstart event. | ||
*/ | ||
var predicate = function predicate(v, key) { | ||
return key.startsWith(prefix); | ||
}; | ||
var prepareProps = (0, _lodash.pickBy)(this.props, predicate); | ||
var prevPrepareProps = (0, _lodash.pickBy)(prevProps, predicate); // Check if any format props changed. | ||
function handlePointerDown(event) { | ||
var target = event.target; // If the child element has no text content, it must be an object. | ||
shouldReapply = shouldReapply || !(0, _isShallowEqual.default)(prepareProps, prevPrepareProps); // Rerender if the placeholder changed. | ||
if (target === ref.current || target.textContent) { | ||
return; | ||
} | ||
shouldReapply = shouldReapply || placeholder !== prevProps.placeholder; | ||
var parentNode = target.parentNode; | ||
var index = Array.from(parentNode.childNodes).indexOf(target); | ||
var range = getDoc().createRange(); | ||
var selection = getWin().getSelection(); | ||
range.setStart(target.parentNode, index); | ||
range.setEnd(target.parentNode, index + 1); | ||
selection.removeAllRanges(); | ||
selection.addRange(range); | ||
} | ||
if (shouldReapply) { | ||
this.value = value; | ||
this.record = this.formatToValue(value); | ||
this.record.start = selectionStart; | ||
this.record.end = selectionEnd; | ||
this.applyRecord(this.record); | ||
} else if (selectionChanged) { | ||
this.record = _objectSpread({}, this.record, { | ||
start: selectionStart, | ||
end: selectionEnd | ||
}); | ||
} | ||
var rafId = (0, _element.useRef)(); | ||
/** | ||
* Handles a focus event on the contenteditable field, calling the | ||
* `unstableOnFocus` prop callback if one is defined. The callback does not | ||
* receive any arguments. | ||
* | ||
* This is marked as a private API and the `unstableOnFocus` prop is not | ||
* documented, as the current requirements where it is used are subject to | ||
* future refactoring following `isSelected` handling. | ||
* | ||
* In contrast with `setFocusedElement`, this is only triggered in response | ||
* to focus within the contenteditable field, whereas `setFocusedElement` | ||
* is triggered on focus within any `RichText` descendent element. | ||
* | ||
* @see setFocusedElement | ||
* | ||
* @private | ||
*/ | ||
function handleFocus() { | ||
if (onFocus) { | ||
onFocus(); | ||
} | ||
/** | ||
* Converts the outside data structure to our internal representation. | ||
* | ||
* @param {*} value The outside value, data type depends on props. | ||
* @return {Object} An internal rich-text value. | ||
*/ | ||
}, { | ||
key: "formatToValue", | ||
value: function formatToValue(value) { | ||
var _this$props9 = this.props, | ||
format = _this$props9.format, | ||
multilineTag = _this$props9.__unstableMultilineTag, | ||
preserveWhiteSpace = _this$props9.preserveWhiteSpace, | ||
disableFormats = _this$props9.__unstableDisableFormats; | ||
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. | ||
var index = undefined; | ||
record.current = _objectSpread({}, record.current, { | ||
start: index, | ||
end: index, | ||
activeFormats: EMPTY_ACTIVE_FORMATS | ||
}); | ||
onSelectionChange(index, index); | ||
setActiveFormats(EMPTY_ACTIVE_FORMATS); | ||
} else { | ||
onSelectionChange(record.current.start, record.current.end); | ||
setActiveFormats((0, _getActiveFormats.getActiveFormats)(_objectSpread({}, record.current, { | ||
activeFormats: undefined | ||
}), EMPTY_ACTIVE_FORMATS)); | ||
} // Update selection as soon as possible, which is at the next animation | ||
// frame. The event listener for selection changes may be added too late | ||
// at this point, but this focus event is still too early to calculate | ||
// the selection. | ||
if (disableFormats) { | ||
return { | ||
text: value, | ||
formats: Array(value.length), | ||
replacements: Array(value.length) | ||
}; | ||
} | ||
if (format !== 'string') { | ||
return value; | ||
} | ||
rafId.current = getWin().requestAnimationFrame(handleSelectionChange); | ||
getDoc().addEventListener('selectionchange', handleSelectionChange); | ||
var prepare = createPrepareEditableTree(this.props, 'format_value_functions'); | ||
value = (0, _create.create)({ | ||
html: value, | ||
multilineTag: multilineTag, | ||
multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
if (setFocusedElement) { | ||
(0, _deprecated.default)('wp.blockEditor.RichText setFocusedElement prop', { | ||
alternative: 'selection state from the block editor store.' | ||
}); | ||
value.formats = prepare(value); | ||
return value; | ||
setFocusedElement(instanceId); | ||
} | ||
/** | ||
* Removes editor only formats from the value. | ||
* | ||
* Editor only formats are applied using `prepareEditableTree`, so we need to | ||
* remove them before converting the internal state | ||
* | ||
* @param {Object} value The internal rich-text value. | ||
* @return {Object} A new rich-text value. | ||
*/ | ||
} | ||
}, { | ||
key: "removeEditorOnlyFormats", | ||
value: function removeEditorOnlyFormats(value) { | ||
this.props.formatTypes.forEach(function (formatType) { | ||
// Remove formats created by prepareEditableTree, because they are editor only. | ||
if (formatType.__experimentalCreatePrepareEditableTree) { | ||
value = (0, _removeFormat.removeFormat)(value, formatType.name, 0, value.text.length); | ||
} | ||
}); | ||
return value; | ||
} | ||
/** | ||
* Converts the internal value to the external data format. | ||
* | ||
* @param {Object} value The internal rich-text value. | ||
* @return {*} The external data format, data type depends on props. | ||
*/ | ||
function handleBlur() { | ||
getDoc().removeEventListener('selectionchange', handleSelectionChange); | ||
} | ||
}, { | ||
key: "valueToFormat", | ||
value: function valueToFormat(value) { | ||
var _this$props10 = this.props, | ||
format = _this$props10.format, | ||
multilineTag = _this$props10.__unstableMultilineTag, | ||
preserveWhiteSpace = _this$props10.preserveWhiteSpace, | ||
disableFormats = _this$props10.__unstableDisableFormats; | ||
function applyFromProps() { | ||
_value.current = value; | ||
record.current = formatToValue(value); | ||
record.current.start = selectionStart; | ||
record.current.end = selectionEnd; | ||
applyRecord(record.current); | ||
} | ||
if (disableFormats) { | ||
return value.text; | ||
} | ||
(0, _element.useEffect)(function () { | ||
if (didMount.current) { | ||
applyFromProps(); | ||
} | ||
}, [TagName, placeholder]); | ||
(0, _element.useEffect)(function () { | ||
if (didMount.current && value !== _value.current) { | ||
applyFromProps(); | ||
} | ||
}, [value]); | ||
(0, _element.useEffect)(function () { | ||
if (!didMount.current) { | ||
return; | ||
} | ||
value = this.removeEditorOnlyFormats(value); | ||
if (format !== 'string') { | ||
return; | ||
} | ||
return (0, _toHtmlString.toHTMLString)({ | ||
value: value, | ||
multilineTag: multilineTag, | ||
preserveWhiteSpace: preserveWhiteSpace | ||
if (isSelected && (selectionStart !== record.current.start || selectionEnd !== record.current.end)) { | ||
applyFromProps(); | ||
} else { | ||
record.current = _objectSpread({}, record.current, { | ||
start: selectionStart, | ||
end: selectionEnd | ||
}); | ||
} | ||
}, { | ||
key: "Editable", | ||
value: function Editable(props) { | ||
var _this2 = this; | ||
}, [selectionStart, selectionEnd, isSelected]); | ||
var prefix = 'format_prepare_props_'; | ||
var _this$props11 = this.props, | ||
_this$props11$tagName = _this$props11.tagName, | ||
TagName = _this$props11$tagName === void 0 ? 'div' : _this$props11$tagName, | ||
style = _this$props11.style, | ||
className = _this$props11.className, | ||
placeholder = _this$props11.placeholder, | ||
forwardedRef = _this$props11.forwardedRef, | ||
disabled = _this$props11.disabled; | ||
var ariaProps = (0, _lodash.pickBy)(this.props, function (value, key) { | ||
return (0, _lodash.startsWith)(key, 'aria-'); | ||
}); | ||
return (0, _element.createElement)(TagName // Overridable props. | ||
, (0, _extends2.default)({ | ||
role: "textbox", | ||
"aria-multiline": true, | ||
"aria-label": placeholder | ||
}, props, ariaProps, { | ||
ref: forwardedRef, | ||
style: style ? _objectSpread({}, style, { | ||
whiteSpace: whiteSpace | ||
}) : defaultStyle, | ||
className: (0, _classnames.default)('rich-text', className), | ||
onPaste: this.onPaste, | ||
onInput: this.onInput, | ||
onCompositionStart: this.onCompositionStart, | ||
onCompositionEnd: this.onCompositionEnd, | ||
onKeyDown: props.onKeyDown ? function (event) { | ||
props.onKeyDown(event); | ||
var predicate = function predicate(v, key) { | ||
return key.startsWith(prefix); | ||
}; | ||
_this2.onKeyDown(event); | ||
} : this.onKeyDown, | ||
onFocus: this.onFocus, | ||
onBlur: this.onBlur, | ||
onMouseDown: this.onPointerDown, | ||
onTouchStart: this.onPointerDown // Selection updates must be done at these events as they | ||
// happen before the `selectionchange` event. In some cases, | ||
// the `selectionchange` event may not even fire, for | ||
// example when the window receives focus again on click. | ||
, | ||
onKeyUp: this.onSelectionChange, | ||
onMouseUp: this.onSelectionChange, | ||
onTouchEnd: this.onSelectionChange // Do not set the attribute if disabled. | ||
, | ||
contentEditable: disabled ? undefined : true, | ||
suppressContentEditableWarning: !disabled | ||
})); | ||
var prepareProps = (0, _lodash.pickBy)(remainingProps, predicate); | ||
(0, _element.useEffect)(function () { | ||
if (didMount.current) { | ||
applyFromProps(); | ||
} | ||
}, { | ||
key: "render", | ||
value: function render() { | ||
var _this3 = this; | ||
}, Object.values(prepareProps)); | ||
(0, _element.useLayoutEffect)(function () { | ||
applyRecord(record.current, { | ||
domOnly: true | ||
}); | ||
didMount.current = true; | ||
return function () { | ||
getDoc().removeEventListener('selectionchange', handleSelectionChange); | ||
getWin().cancelAnimationFrame(rafId.current); | ||
getWin().clearTimeout(timeout.current); | ||
}; | ||
}, []); | ||
var _this$props12 = this.props, | ||
isSelected = _this$props12.__unstableIsSelected, | ||
children = _this$props12.children, | ||
allowedFormats = _this$props12.allowedFormats, | ||
withoutInteractiveFormatting = _this$props12.withoutInteractiveFormatting, | ||
formatTypes = _this$props12.formatTypes, | ||
forwardedRef = _this$props12.forwardedRef; | ||
var activeFormats = this.state.activeFormats; | ||
function focus() { | ||
ref.current.focus(); | ||
applyRecord(record.current); | ||
} | ||
var onFocus = function onFocus() { | ||
forwardedRef.current.focus(); | ||
var ariaProps = (0, _lodash.pickBy)(remainingProps, function (v, key) { | ||
return (0, _lodash.startsWith)(key, 'aria-'); | ||
}); | ||
_this3.applyRecord(_this3.record); | ||
}; | ||
var editableProps = _objectSpread({ | ||
// Overridable props. | ||
role: 'textbox', | ||
'aria-multiline': '', | ||
'aria-label': placeholder | ||
}, ariaProps, { | ||
ref: ref, | ||
style: style ? _objectSpread({}, style, { | ||
whiteSpace: whiteSpace | ||
}) : defaultStyle, | ||
className: (0, _classnames.default)('rich-text', className), | ||
onPaste: handlePaste, | ||
onInput: handleInput, | ||
onCompositionStart: handleCompositionStart, | ||
onCompositionEnd: handleCompositionEnd, | ||
onKeyDown: handleKeyDown, | ||
onFocus: handleFocus, | ||
onBlur: handleBlur, | ||
onMouseDown: handlePointerDown, | ||
onTouchStart: handlePointerDown, | ||
// Selection updates must be done at these events as they | ||
// happen before the `selectionchange` event. In some cases, | ||
// the `selectionchange` event may not even fire, for | ||
// example when the window receives focus again on click. | ||
onKeyUp: handleSelectionChange, | ||
onMouseUp: handleSelectionChange, | ||
onTouchEnd: handleSelectionChange, | ||
// Do not set the attribute if disabled. | ||
contentEditable: disabled ? undefined : true, | ||
suppressContentEditableWarning: !disabled | ||
}); | ||
return (0, _element.createElement)(_element.Fragment, null, (0, _element.createElement)(_boundaryStyle.BoundaryStyle, { | ||
activeFormats: activeFormats, | ||
forwardedRef: forwardedRef | ||
}), (0, _element.createElement)(_inlineWarning.InlineWarning, { | ||
forwardedRef: forwardedRef | ||
}), isSelected && (0, _element.createElement)(_formatEdit.default, { | ||
allowedFormats: allowedFormats, | ||
withoutInteractiveFormatting: withoutInteractiveFormatting, | ||
value: this.record, | ||
onChange: this.onChange, | ||
onFocus: onFocus, | ||
formatTypes: formatTypes | ||
}), children && children({ | ||
isSelected: isSelected, | ||
value: this.record, | ||
onChange: this.onChange, | ||
onFocus: onFocus, | ||
Editable: this.Editable | ||
}), !children && (0, _element.createElement)(this.Editable, null)); | ||
} | ||
}]); | ||
return RichText; | ||
}(_element.Component); | ||
(0, _useBoundaryStyle.useBoundaryStyle)({ | ||
ref: ref, | ||
activeFormats: activeFormats | ||
}); | ||
(0, _useInlineWarning.useInlineWarning)({ | ||
ref: ref | ||
}); | ||
return (0, _element.createElement)(_element.Fragment, null, isSelected && (0, _element.createElement)(_formatEdit.default, { | ||
allowedFormats: allowedFormats, | ||
withoutInteractiveFormatting: withoutInteractiveFormatting, | ||
value: record.current, | ||
onChange: handleChange, | ||
onFocus: focus, | ||
formatTypes: formatTypes | ||
}), children && children({ | ||
isSelected: isSelected, | ||
value: record.current, | ||
onChange: handleChange, | ||
onFocus: focus, | ||
editableProps: editableProps, | ||
editableTagName: TagName | ||
}), !children && (0, _element.createElement)(TagName, editableProps)); | ||
} | ||
RichText.defaultProps = { | ||
format: 'string', | ||
value: '' | ||
}; | ||
var RichTextWrapper = (0, _compose.compose)([_compose.withSafeTimeout, _withFormatTypes.default])(RichText); | ||
var RichTextWrapper = (0, _withFormatTypes.default)(RichText); | ||
/** | ||
@@ -1247,0 +1132,0 @@ * Renders a rich content input, providing users with the option to format the |
@@ -11,5 +11,9 @@ "use strict"; | ||
* | ||
* @param {Object} value Value to inspect. | ||
* @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no active | ||
* formats. | ||
* @param {Object} value Value to inspect. | ||
* @param {Array<Array>} value.formats Formats object data values. | ||
* @param {number} value.start Index to start from. | ||
* @param {number} value.end Index to end. | ||
* @param {Array} value.activeFormats Array to return if there are active formats. | ||
* @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no active | ||
* formats. | ||
* | ||
@@ -16,0 +20,0 @@ * @return {?Object} Active format objects. |
{ | ||
"name": "@wordpress/rich-text", | ||
"version": "3.18.0", | ||
"version": "3.18.1-rc.0", | ||
"description": "Rich text value and manipulation API.", | ||
@@ -9,2 +9,3 @@ "author": "The WordPress Contributors", | ||
"wordpress", | ||
"gutenberg", | ||
"rich-text" | ||
@@ -29,9 +30,9 @@ ], | ||
"@babel/runtime": "^7.9.2", | ||
"@wordpress/compose": "^3.17.0", | ||
"@wordpress/data": "^4.20.0", | ||
"@wordpress/deprecated": "^2.8.0", | ||
"@wordpress/element": "^2.14.0", | ||
"@wordpress/escape-html": "^1.8.0", | ||
"@wordpress/is-shallow-equal": "^2.0.0", | ||
"@wordpress/keycodes": "^2.13.0", | ||
"@wordpress/compose": "^3.17.1-rc.0", | ||
"@wordpress/data": "^4.20.1-rc.0", | ||
"@wordpress/deprecated": "^2.8.1-rc.0", | ||
"@wordpress/element": "^2.14.1-rc.0", | ||
"@wordpress/escape-html": "^1.8.1-rc.0", | ||
"@wordpress/is-shallow-equal": "^2.0.1-rc.0", | ||
"@wordpress/keycodes": "^2.13.1-rc.0", | ||
"classnames": "^2.2.5", | ||
@@ -45,3 +46,3 @@ "lodash": "^4.17.15", | ||
}, | ||
"gitHead": "ab0988b3b10ea7603ae701908e780ec40c7ef1d6" | ||
"gitHead": "b68941a77fc6a19657b1c94a481849ec11984865" | ||
} |
@@ -10,4 +10,11 @@ /** | ||
*/ | ||
import { Component, forwardRef } from '@wordpress/element'; | ||
import { | ||
forwardRef, | ||
useEffect, | ||
useRef, | ||
useState, | ||
useMemo, | ||
useLayoutEffect, | ||
} from '@wordpress/element'; | ||
import { | ||
BACKSPACE, | ||
@@ -21,4 +28,2 @@ DELETE, | ||
} from '@wordpress/keycodes'; | ||
import { withSafeTimeout, compose } from '@wordpress/compose'; | ||
import isShallowEqual from '@wordpress/is-shallow-equal'; | ||
import deprecated from '@wordpress/deprecated'; | ||
@@ -43,4 +48,4 @@ | ||
import withFormatTypes from './with-format-types'; | ||
import { BoundaryStyle } from './boundary-style'; | ||
import { InlineWarning } from './inline-warning'; | ||
import { useBoundaryStyle } from './use-boundary-style'; | ||
import { useInlineWarning } from './use-inline-warning'; | ||
import { insert } from '../insert'; | ||
@@ -138,81 +143,144 @@ | ||
/** | ||
* See export statement below. | ||
*/ | ||
class RichText extends Component { | ||
constructor( { value, selectionStart, selectionEnd } ) { | ||
super( ...arguments ); | ||
function RichText( { | ||
tagName: TagName = 'div', | ||
value = '', | ||
selectionStart, | ||
selectionEnd, | ||
children, | ||
allowedFormats, | ||
withoutInteractiveFormatting, | ||
formatTypes, | ||
style, | ||
className, | ||
placeholder, | ||
disabled, | ||
preserveWhiteSpace, | ||
onPaste, | ||
format = 'string', | ||
onDelete, | ||
onEnter, | ||
onSelectionChange, | ||
onChange, | ||
unstableOnFocus: onFocus, | ||
setFocusedElement, | ||
instanceId, | ||
__unstableMultilineTag: multilineTag, | ||
__unstableMultilineRootTag: multilineRootTag, | ||
__unstableDisableFormats: disableFormats, | ||
__unstableDidAutomaticChange: didAutomaticChange, | ||
__unstableInputRule: inputRule, | ||
__unstableMarkAutomaticChange: markAutomaticChange, | ||
__unstableAllowPrefixTransformations: allowPrefixTransformations, | ||
__unstableUndo: undo, | ||
__unstableIsCaretWithinFormattedText: isCaretWithinFormattedText, | ||
__unstableOnEnterFormattedText: onEnterFormattedText, | ||
__unstableOnExitFormattedText: onExitFormattedText, | ||
__unstableOnCreateUndoLevel: onCreateUndoLevel, | ||
__unstableIsSelected: isSelected, | ||
forwardedRef: ref, | ||
...remainingProps | ||
} ) { | ||
const [ activeFormats = [], setActiveFormats ] = useState(); | ||
this.getDocument = this.getDocument.bind( this ); | ||
this.getWindow = this.getWindow.bind( this ); | ||
this.onFocus = this.onFocus.bind( this ); | ||
this.onBlur = this.onBlur.bind( this ); | ||
this.onChange = this.onChange.bind( this ); | ||
this.handleDelete = this.handleDelete.bind( this ); | ||
this.handleEnter = this.handleEnter.bind( this ); | ||
this.handleSpace = this.handleSpace.bind( this ); | ||
this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( | ||
this | ||
); | ||
this.onPaste = this.onPaste.bind( this ); | ||
this.onCreateUndoLevel = this.onCreateUndoLevel.bind( this ); | ||
this.onInput = this.onInput.bind( this ); | ||
this.onCompositionStart = this.onCompositionStart.bind( this ); | ||
this.onCompositionEnd = this.onCompositionEnd.bind( this ); | ||
this.onSelectionChange = this.onSelectionChange.bind( this ); | ||
this.createRecord = this.createRecord.bind( this ); | ||
this.applyRecord = this.applyRecord.bind( this ); | ||
this.valueToFormat = this.valueToFormat.bind( this ); | ||
this.onPointerDown = this.onPointerDown.bind( this ); | ||
this.formatToValue = this.formatToValue.bind( this ); | ||
this.Editable = this.Editable.bind( this ); | ||
function getDoc() { | ||
return ref.current.ownerDocument; | ||
} | ||
this.onKeyDown = ( event ) => { | ||
if ( event.defaultPrevented ) { | ||
return; | ||
} | ||
function getWin() { | ||
return getDoc().defaultView; | ||
} | ||
this.handleDelete( event ); | ||
this.handleEnter( event ); | ||
this.handleSpace( event ); | ||
this.handleHorizontalNavigation( event ); | ||
}; | ||
/** | ||
* Converts the outside data structure to our internal representation. | ||
* | ||
* @param {*} string The outside value, data type depends on props. | ||
* | ||
* @return {Object} An internal rich-text value. | ||
*/ | ||
function formatToValue( string ) { | ||
if ( disableFormats ) { | ||
return { | ||
text: string, | ||
formats: Array( string.length ), | ||
replacements: Array( string.length ), | ||
}; | ||
} | ||
this.state = {}; | ||
this.lastHistoryValue = value; | ||
if ( format !== 'string' ) { | ||
return string; | ||
} | ||
// Internal values are updated synchronously, unlike props and state. | ||
this.value = value; | ||
this.record = this.formatToValue( value ); | ||
this.record.start = selectionStart; | ||
this.record.end = selectionEnd; | ||
} | ||
componentWillUnmount() { | ||
this.getDocument().removeEventListener( | ||
'selectionchange', | ||
this.onSelectionChange | ||
const prepare = createPrepareEditableTree( | ||
remainingProps, | ||
'format_value_functions' | ||
); | ||
this.getWindow().cancelAnimationFrame( this.rafId ); | ||
} | ||
componentDidMount() { | ||
this.applyRecord( this.record, { domOnly: true } ); | ||
const result = create( { | ||
html: string, | ||
multilineTag, | ||
multilineWrapperTags: | ||
multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, | ||
preserveWhiteSpace, | ||
} ); | ||
result.formats = prepare( result ); | ||
return result; | ||
} | ||
getDocument() { | ||
return this.props.forwardedRef.current.ownerDocument; | ||
/** | ||
* Removes editor only formats from the value. | ||
* | ||
* Editor only formats are applied using `prepareEditableTree`, so we need to | ||
* remove them before converting the internal state | ||
* | ||
* @param {Object} val The internal rich-text value. | ||
* | ||
* @return {Object} A new rich-text value. | ||
*/ | ||
function removeEditorOnlyFormats( val ) { | ||
formatTypes.forEach( ( formatType ) => { | ||
// Remove formats created by prepareEditableTree, because they are editor only. | ||
if ( formatType.__experimentalCreatePrepareEditableTree ) { | ||
val = removeFormat( val, formatType.name, 0, val.text.length ); | ||
} | ||
} ); | ||
return val; | ||
} | ||
getWindow() { | ||
return this.getDocument().defaultView; | ||
/** | ||
* Converts the internal value to the external data format. | ||
* | ||
* @param {Object} val The internal rich-text value. | ||
* | ||
* @return {*} The external data format, data type depends on props. | ||
*/ | ||
function valueToFormat( val ) { | ||
if ( disableFormats ) { | ||
return val.text; | ||
} | ||
val = removeEditorOnlyFormats( val ); | ||
if ( format !== 'string' ) { | ||
return; | ||
} | ||
return toHTMLString( { value: val, multilineTag, preserveWhiteSpace } ); | ||
} | ||
createRecord() { | ||
const { | ||
__unstableMultilineTag: multilineTag, | ||
forwardedRef, | ||
preserveWhiteSpace, | ||
} = this.props; | ||
const selection = this.getWindow().getSelection(); | ||
// Internal values are updated synchronously, unlike props and state. | ||
const _value = useRef( value ); | ||
const record = useRef( | ||
useMemo( () => { | ||
const initialRecord = formatToValue( value ); | ||
initialRecord.start = selectionStart; | ||
initialRecord.end = selectionEnd; | ||
return initialRecord; | ||
}, [] ) | ||
); | ||
function createRecord() { | ||
const selection = getWin().getSelection(); | ||
const range = | ||
@@ -222,3 +290,3 @@ selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null; | ||
return create( { | ||
element: forwardedRef.current, | ||
element: ref.current, | ||
range, | ||
@@ -233,11 +301,6 @@ multilineTag, | ||
applyRecord( record, { domOnly } = {} ) { | ||
const { | ||
__unstableMultilineTag: multilineTag, | ||
forwardedRef, | ||
} = this.props; | ||
function applyRecord( newRecord, { domOnly } = {} ) { | ||
apply( { | ||
value: record, | ||
current: forwardedRef.current, | ||
value: newRecord, | ||
current: ref.current, | ||
multilineTag, | ||
@@ -247,7 +310,7 @@ multilineWrapperTags: | ||
prepareEditableTree: createPrepareEditableTree( | ||
this.props, | ||
remainingProps, | ||
'format_prepare_functions' | ||
), | ||
__unstableDomOnly: domOnly, | ||
placeholder: this.props.placeholder, | ||
placeholder, | ||
} ); | ||
@@ -263,11 +326,3 @@ } | ||
*/ | ||
onPaste( event ) { | ||
const { | ||
formatTypes, | ||
onPaste, | ||
__unstableIsSelected: isSelected, | ||
__unstableDisableFormats, | ||
} = this.props; | ||
const { activeFormats = [] } = this.state; | ||
function handlePaste( event ) { | ||
if ( ! isSelected ) { | ||
@@ -312,13 +367,12 @@ event.preventDefault(); | ||
if ( __unstableDisableFormats ) { | ||
this.onChange( insert( this.record, plainText ) ); | ||
if ( disableFormats ) { | ||
handleChange( insert( record.current, plainText ) ); | ||
return; | ||
} | ||
const record = this.record; | ||
const transformed = formatTypes.reduce( | ||
( accumlator, { __unstablePasteRule } ) => { | ||
// Only allow one transform. | ||
if ( __unstablePasteRule && accumlator === record ) { | ||
accumlator = __unstablePasteRule( record, { | ||
if ( __unstablePasteRule && accumlator === record.current ) { | ||
accumlator = __unstablePasteRule( record.current, { | ||
html, | ||
@@ -331,7 +385,7 @@ plainText, | ||
}, | ||
record | ||
record.current | ||
); | ||
if ( transformed !== record ) { | ||
this.onChange( transformed ); | ||
if ( transformed !== record.current ) { | ||
handleChange( transformed ); | ||
return; | ||
@@ -362,4 +416,4 @@ } | ||
onPaste( { | ||
value: this.removeEditorOnlyFormats( record ), | ||
onChange: this.onChange, | ||
value: removeEditorOnlyFormats( record.current ), | ||
onChange: handleChange, | ||
html, | ||
@@ -374,323 +428,2 @@ plainText, | ||
/** | ||
* Handles a focus event on the contenteditable field, calling the | ||
* `unstableOnFocus` prop callback if one is defined. The callback does not | ||
* receive any arguments. | ||
* | ||
* This is marked as a private API and the `unstableOnFocus` prop is not | ||
* documented, as the current requirements where it is used are subject to | ||
* future refactoring following `isSelected` handling. | ||
* | ||
* In contrast with `setFocusedElement`, this is only triggered in response | ||
* to focus within the contenteditable field, whereas `setFocusedElement` | ||
* is triggered on focus within any `RichText` descendent element. | ||
* | ||
* @see setFocusedElement | ||
* | ||
* @private | ||
*/ | ||
onFocus() { | ||
const { unstableOnFocus } = this.props; | ||
if ( unstableOnFocus ) { | ||
unstableOnFocus(); | ||
} | ||
if ( ! this.props.__unstableIsSelected ) { | ||
// 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; | ||
const activeFormats = EMPTY_ACTIVE_FORMATS; | ||
this.record = { | ||
...this.record, | ||
start: index, | ||
end: index, | ||
activeFormats, | ||
}; | ||
this.props.onSelectionChange( index, index ); | ||
this.setState( { activeFormats } ); | ||
} else { | ||
this.props.onSelectionChange( this.record.start, this.record.end ); | ||
this.setState( { | ||
activeFormats: getActiveFormats( | ||
{ | ||
...this.record, | ||
activeFormats: undefined, | ||
}, | ||
EMPTY_ACTIVE_FORMATS | ||
), | ||
} ); | ||
} | ||
// Update selection as soon as possible, which is at the next animation | ||
// frame. The event listener for selection changes may be added too late | ||
// at this point, but this focus event is still too early to calculate | ||
// the selection. | ||
this.rafId = this.getWindow().requestAnimationFrame( | ||
this.onSelectionChange | ||
); | ||
this.getDocument().addEventListener( | ||
'selectionchange', | ||
this.onSelectionChange | ||
); | ||
if ( this.props.setFocusedElement ) { | ||
deprecated( 'wp.blockEditor.RichText setFocusedElement prop', { | ||
alternative: 'selection state from the block editor store.', | ||
} ); | ||
this.props.setFocusedElement( this.props.instanceId ); | ||
} | ||
} | ||
onBlur() { | ||
this.getDocument().removeEventListener( | ||
'selectionchange', | ||
this.onSelectionChange | ||
); | ||
} | ||
/** | ||
* Handle input on the next selection change event. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic input event. | ||
*/ | ||
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 ( this.isComposing ) { | ||
return; | ||
} | ||
let inputType; | ||
if ( event ) { | ||
inputType = event.inputType; | ||
} | ||
if ( ! inputType && event && event.nativeEvent ) { | ||
inputType = event.nativeEvent.inputType; | ||
} | ||
// 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 ) ) | ||
) { | ||
this.applyRecord( this.record ); | ||
return; | ||
} | ||
const value = this.createRecord(); | ||
const { start, activeFormats = [] } = this.record; | ||
// Update the formats between the last and new caret position. | ||
const change = updateFormats( { | ||
value, | ||
start, | ||
end: value.start, | ||
formats: activeFormats, | ||
} ); | ||
this.onChange( change, { withoutHistory: true } ); | ||
const { | ||
__unstableInputRule: inputRule, | ||
__unstableMarkAutomaticChange: markAutomaticChange, | ||
__unstableAllowPrefixTransformations: allowPrefixTransformations, | ||
formatTypes, | ||
setTimeout, | ||
clearTimeout, | ||
} = this.props; | ||
// Create an undo level when input stops for over a second. | ||
clearTimeout( this.onInput.timeout ); | ||
this.onInput.timeout = setTimeout( this.onCreateUndoLevel, 1000 ); | ||
// Only run input rules when inserting text. | ||
if ( inputType !== 'insertText' ) { | ||
return; | ||
} | ||
if ( allowPrefixTransformations && inputRule ) { | ||
inputRule( change, this.valueToFormat ); | ||
} | ||
const transformed = formatTypes.reduce( | ||
( accumlator, { __unstableInputRule } ) => { | ||
if ( __unstableInputRule ) { | ||
accumlator = __unstableInputRule( accumlator ); | ||
} | ||
return accumlator; | ||
}, | ||
change | ||
); | ||
if ( transformed !== change ) { | ||
this.onCreateUndoLevel(); | ||
this.onChange( { ...transformed, activeFormats } ); | ||
markAutomaticChange(); | ||
} | ||
} | ||
onCompositionStart() { | ||
this.isComposing = true; | ||
// Do not update the selection when characters are being composed as | ||
// this rerenders the component and might distroy internal browser | ||
// editing state. | ||
this.getDocument().removeEventListener( | ||
'selectionchange', | ||
this.onSelectionChange | ||
); | ||
} | ||
onCompositionEnd() { | ||
this.isComposing = false; | ||
// Ensure the value is up-to-date for browsers that don't emit a final | ||
// input event after composition. | ||
this.onInput( { inputType: 'insertText' } ); | ||
// Tracking selection changes can be resumed. | ||
this.getDocument().addEventListener( | ||
'selectionchange', | ||
this.onSelectionChange | ||
); | ||
} | ||
/** | ||
* Syncs the selection to local state. A callback for the `selectionchange` | ||
* native events, `keyup`, `mouseup` and `touchend` synthetic events, and | ||
* animation frames after the `focus` event. | ||
* | ||
* @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event | ||
*/ | ||
onSelectionChange( event ) { | ||
if ( | ||
event.type !== 'selectionchange' && | ||
! this.props.__unstableIsSelected | ||
) { | ||
return; | ||
} | ||
if ( this.props.disabled ) { | ||
return; | ||
} | ||
// In case of a keyboard event, ignore selection changes during | ||
// composition. | ||
if ( this.isComposing ) { | ||
return; | ||
} | ||
const { start, end, text } = this.createRecord(); | ||
const value = this.record; | ||
// Fallback mechanism for IE11, which doesn't support the input event. | ||
// Any input results in a selection change. | ||
if ( text !== value.text ) { | ||
this.onInput(); | ||
return; | ||
} | ||
if ( start === value.start && end === value.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 ( value.text.length === 0 && start === 0 ) { | ||
fixPlaceholderSelection( this.getWindow() ); | ||
} | ||
return; | ||
} | ||
const { | ||
__unstableIsCaretWithinFormattedText: isCaretWithinFormattedText, | ||
__unstableOnEnterFormattedText: onEnterFormattedText, | ||
__unstableOnExitFormattedText: onExitFormattedText, | ||
} = this.props; | ||
const newValue = { | ||
...value, | ||
start, | ||
end, | ||
// Allow `getActiveFormats` to get new `activeFormats`. | ||
activeFormats: undefined, | ||
}; | ||
const activeFormats = getActiveFormats( | ||
newValue, | ||
EMPTY_ACTIVE_FORMATS | ||
); | ||
// Update the value with the new active formats. | ||
newValue.activeFormats = activeFormats; | ||
if ( ! isCaretWithinFormattedText && activeFormats.length ) { | ||
onEnterFormattedText(); | ||
} else if ( isCaretWithinFormattedText && ! activeFormats.length ) { | ||
onExitFormattedText(); | ||
} | ||
// It is important that the internal value is updated first, | ||
// otherwise the value will be wrong on render! | ||
this.record = newValue; | ||
this.applyRecord( newValue, { domOnly: true } ); | ||
this.props.onSelectionChange( start, end ); | ||
this.setState( { activeFormats } ); | ||
} | ||
/** | ||
* Sync the value to global state. The node tree and selection will also be | ||
* updated if differences are found. | ||
* | ||
* @param {Object} record The record to sync and apply. | ||
* @param {Object} $2 Named options. | ||
* @param {boolean} $2.withoutHistory If true, no undo level will be | ||
* created. | ||
*/ | ||
onChange( record, { withoutHistory } = {} ) { | ||
if ( this.props.__unstableDisableFormats ) { | ||
record.formats = Array( record.text.length ); | ||
record.replacements = Array( record.text.length ); | ||
} | ||
this.applyRecord( record ); | ||
const { start, end, activeFormats = [] } = record; | ||
const changeHandlers = pickBy( this.props, ( v, key ) => | ||
key.startsWith( 'format_on_change_functions_' ) | ||
); | ||
Object.values( changeHandlers ).forEach( ( changeHandler ) => { | ||
changeHandler( record.formats, record.text ); | ||
} ); | ||
this.value = this.valueToFormat( record ); | ||
this.record = record; | ||
// Selection must be updated first, so it is recorded in history when | ||
// the content change happens. | ||
this.props.onSelectionChange( start, end ); | ||
this.props.onChange( this.value ); | ||
this.setState( { activeFormats } ); | ||
if ( ! withoutHistory ) { | ||
this.onCreateUndoLevel(); | ||
} | ||
} | ||
onCreateUndoLevel() { | ||
// If the content is the same, no level needs to be created. | ||
if ( this.lastHistoryValue === this.value ) { | ||
return; | ||
} | ||
this.props.__unstableOnCreateUndoLevel(); | ||
this.lastHistoryValue = this.value; | ||
} | ||
/** | ||
* Handles delete on keydown: | ||
@@ -703,3 +436,3 @@ * - outdent list items, | ||
*/ | ||
handleDelete( event ) { | ||
function handleDelete( event ) { | ||
const { keyCode } = event; | ||
@@ -715,5 +448,5 @@ | ||
if ( this.props.__unstableDidAutomaticChange ) { | ||
if ( didAutomaticChange ) { | ||
event.preventDefault(); | ||
this.props.__unstableUndo(); | ||
undo(); | ||
return; | ||
@@ -726,6 +459,4 @@ } | ||
const { onDelete, __unstableMultilineTag: multilineTag } = this.props; | ||
const { activeFormats = [] } = this.state; | ||
const value = this.createRecord(); | ||
const { start, end, text } = value; | ||
const currentValue = createRecord(); | ||
const { start, end, text } = currentValue; | ||
const isReverse = keyCode === BACKSPACE; | ||
@@ -735,3 +466,3 @@ | ||
if ( start === 0 && end !== 0 && end === text.length ) { | ||
this.onChange( remove( value ) ); | ||
handleChange( remove( currentValue ) ); | ||
event.preventDefault(); | ||
@@ -747,13 +478,13 @@ return; | ||
isReverse && | ||
value.start === 0 && | ||
value.end === 0 && | ||
isEmptyLine( value ) | ||
currentValue.start === 0 && | ||
currentValue.end === 0 && | ||
isEmptyLine( currentValue ) | ||
) { | ||
newValue = removeLineSeparator( value, ! isReverse ); | ||
newValue = removeLineSeparator( currentValue, ! isReverse ); | ||
} else { | ||
newValue = removeLineSeparator( value, isReverse ); | ||
newValue = removeLineSeparator( currentValue, isReverse ); | ||
} | ||
if ( newValue ) { | ||
this.onChange( newValue ); | ||
handleChange( newValue ); | ||
event.preventDefault(); | ||
@@ -767,3 +498,3 @@ return; | ||
! onDelete || | ||
! isCollapsed( value ) || | ||
! isCollapsed( currentValue ) || | ||
activeFormats.length || | ||
@@ -776,3 +507,3 @@ ( isReverse && start !== 0 ) || | ||
onDelete( { isReverse, value } ); | ||
onDelete( { isReverse, value: currentValue } ); | ||
event.preventDefault(); | ||
@@ -786,3 +517,3 @@ } | ||
*/ | ||
handleEnter( event ) { | ||
function handleEnter( event ) { | ||
if ( event.keyCode !== ENTER ) { | ||
@@ -794,4 +525,2 @@ return; | ||
const { onEnter } = this.props; | ||
if ( ! onEnter ) { | ||
@@ -802,4 +531,4 @@ return; | ||
onEnter( { | ||
value: this.removeEditorOnlyFormats( this.createRecord() ), | ||
onChange: this.onChange, | ||
value: removeEditorOnlyFormats( createRecord() ), | ||
onChange: handleChange, | ||
shiftKey: event.shiftKey, | ||
@@ -814,8 +543,4 @@ } ); | ||
*/ | ||
handleSpace( event ) { | ||
function handleSpace( event ) { | ||
const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; | ||
const { | ||
__unstableMultilineRootTag: multilineRootTag, | ||
__unstableMultilineTag: multilineTag, | ||
} = this.props; | ||
@@ -834,9 +559,9 @@ if ( | ||
const value = this.createRecord(); | ||
const currentValue = createRecord(); | ||
if ( ! isCollapsed( value ) ) { | ||
if ( ! isCollapsed( currentValue ) ) { | ||
return; | ||
} | ||
const { text, start } = value; | ||
const { text, start } = currentValue; | ||
const characterBefore = text[ start - 1 ]; | ||
@@ -849,3 +574,5 @@ | ||
this.onChange( indentListItems( value, { type: multilineRootTag } ) ); | ||
handleChange( | ||
indentListItems( currentValue, { type: multilineRootTag } ) | ||
); | ||
event.preventDefault(); | ||
@@ -861,3 +588,3 @@ } | ||
*/ | ||
handleHorizontalNavigation( event ) { | ||
function handleHorizontalNavigation( event ) { | ||
const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; | ||
@@ -876,9 +603,12 @@ | ||
const value = this.record; | ||
const { text, formats, start, end, activeFormats = [] } = value; | ||
const collapsed = isCollapsed( value ); | ||
const { | ||
text, | ||
formats, | ||
start, | ||
end, | ||
activeFormats: currentActiveFormats = [], | ||
} = record.current; | ||
const collapsed = isCollapsed( record.current ); | ||
// To do: ideally, we should look at visual position instead. | ||
const { direction } = this.getWindow().getComputedStyle( | ||
this.props.forwardedRef.current | ||
); | ||
const { direction } = getWin().getComputedStyle( ref.current ); | ||
const reverseKey = direction === 'rtl' ? RIGHT : LEFT; | ||
@@ -891,3 +621,3 @@ const isReverse = event.keyCode === reverseKey; | ||
// navigating forward. | ||
if ( collapsed && activeFormats.length === 0 ) { | ||
if ( collapsed && currentActiveFormats.length === 0 ) { | ||
if ( start === 0 && isReverse ) { | ||
@@ -915,3 +645,3 @@ return; | ||
let newActiveFormatsLength = activeFormats.length; | ||
let newActiveFormatsLength = currentActiveFormats.length; | ||
let source = formatsAfter; | ||
@@ -926,15 +656,27 @@ | ||
if ( formatsBefore.length < formatsAfter.length ) { | ||
if ( ! isReverse && activeFormats.length < formatsAfter.length ) { | ||
if ( | ||
! isReverse && | ||
currentActiveFormats.length < formatsAfter.length | ||
) { | ||
newActiveFormatsLength++; | ||
} | ||
if ( isReverse && activeFormats.length > formatsBefore.length ) { | ||
if ( | ||
isReverse && | ||
currentActiveFormats.length > formatsBefore.length | ||
) { | ||
newActiveFormatsLength--; | ||
} | ||
} else if ( formatsBefore.length > formatsAfter.length ) { | ||
if ( ! isReverse && activeFormats.length > formatsAfter.length ) { | ||
if ( | ||
! isReverse && | ||
currentActiveFormats.length > formatsAfter.length | ||
) { | ||
newActiveFormatsLength--; | ||
} | ||
if ( isReverse && activeFormats.length < formatsBefore.length ) { | ||
if ( | ||
isReverse && | ||
currentActiveFormats.length < formatsBefore.length | ||
) { | ||
newActiveFormatsLength++; | ||
@@ -944,8 +686,11 @@ } | ||
if ( newActiveFormatsLength !== activeFormats.length ) { | ||
if ( newActiveFormatsLength !== currentActiveFormats.length ) { | ||
const newActiveFormats = source.slice( 0, newActiveFormatsLength ); | ||
const newValue = { ...value, activeFormats: newActiveFormats }; | ||
this.record = newValue; | ||
this.applyRecord( newValue ); | ||
this.setState( { activeFormats: newActiveFormats } ); | ||
const newValue = { | ||
...record.current, | ||
activeFormats: newActiveFormats, | ||
}; | ||
record.current = newValue; | ||
applyRecord( newValue ); | ||
setActiveFormats( newActiveFormats ); | ||
return; | ||
@@ -957,3 +702,3 @@ } | ||
const newValue = { | ||
...value, | ||
...record.current, | ||
start: newPos, | ||
@@ -964,300 +709,494 @@ end: newPos, | ||
this.record = newValue; | ||
this.applyRecord( newValue ); | ||
this.props.onSelectionChange( newPos, newPos ); | ||
this.setState( { activeFormats: newActiveFormats } ); | ||
record.current = newValue; | ||
applyRecord( newValue ); | ||
onSelectionChange( newPos, newPos ); | ||
setActiveFormats( newActiveFormats ); | ||
} | ||
function handleKeyDown( event ) { | ||
if ( event.defaultPrevented ) { | ||
return; | ||
} | ||
handleDelete( event ); | ||
handleEnter( event ); | ||
handleSpace( event ); | ||
handleHorizontalNavigation( event ); | ||
} | ||
const lastHistoryValue = useRef( value ); | ||
function createUndoLevel() { | ||
// If the content is the same, no level needs to be created. | ||
if ( lastHistoryValue.current === _value.current ) { | ||
return; | ||
} | ||
onCreateUndoLevel(); | ||
lastHistoryValue.current = _value.current; | ||
} | ||
const isComposing = useRef( false ); | ||
const timeout = useRef(); | ||
/** | ||
* Select object when they are clicked. The browser will not set any | ||
* selection when clicking e.g. an image. | ||
* Handle input on the next selection change event. | ||
* | ||
* @param {WPSyntheticEvent} event Synthetic mousedown or touchstart event. | ||
* @param {WPSyntheticEvent} event Synthetic input event. | ||
*/ | ||
onPointerDown( event ) { | ||
const { target } = event; | ||
function handleInput( 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.current ) { | ||
return; | ||
} | ||
// If the child element has no text content, it must be an object. | ||
let inputType; | ||
if ( event ) { | ||
inputType = event.inputType; | ||
} | ||
if ( ! inputType && event && event.nativeEvent ) { | ||
inputType = event.nativeEvent.inputType; | ||
} | ||
// The browser formatted something or tried to insert HTML. | ||
// Overwrite it. It will be handled later by the format library if | ||
// needed. | ||
if ( | ||
target === this.props.forwardedRef.current || | ||
target.textContent | ||
inputType && | ||
( inputType.indexOf( 'format' ) === 0 || | ||
INSERTION_INPUT_TYPES_TO_IGNORE.has( inputType ) ) | ||
) { | ||
applyRecord( record.current ); | ||
return; | ||
} | ||
const { parentNode } = target; | ||
const index = Array.from( parentNode.childNodes ).indexOf( target ); | ||
const range = this.getDocument().createRange(); | ||
const selection = this.getWindow().getSelection(); | ||
const currentValue = createRecord(); | ||
const { start, activeFormats: oldActiveFormats = [] } = record.current; | ||
range.setStart( target.parentNode, index ); | ||
range.setEnd( target.parentNode, index + 1 ); | ||
// Update the formats between the last and new caret position. | ||
const change = updateFormats( { | ||
value: currentValue, | ||
start, | ||
end: currentValue.start, | ||
formats: oldActiveFormats, | ||
} ); | ||
selection.removeAllRanges(); | ||
selection.addRange( range ); | ||
handleChange( change, { withoutHistory: true } ); | ||
// Create an undo level when input stops for over a second. | ||
getWin().clearTimeout( timeout.current ); | ||
timeout.current = getWin().setTimeout( createUndoLevel, 1000 ); | ||
// Only run input rules when inserting text. | ||
if ( inputType !== 'insertText' ) { | ||
return; | ||
} | ||
if ( allowPrefixTransformations && inputRule ) { | ||
inputRule( change, valueToFormat ); | ||
} | ||
const transformed = formatTypes.reduce( | ||
( accumlator, { __unstableInputRule } ) => { | ||
if ( __unstableInputRule ) { | ||
accumlator = __unstableInputRule( accumlator ); | ||
} | ||
return accumlator; | ||
}, | ||
change | ||
); | ||
if ( transformed !== change ) { | ||
createUndoLevel(); | ||
handleChange( { ...transformed, activeFormats: oldActiveFormats } ); | ||
markAutomaticChange(); | ||
} | ||
} | ||
componentDidUpdate( prevProps ) { | ||
const { | ||
tagName, | ||
value, | ||
selectionStart, | ||
selectionEnd, | ||
placeholder, | ||
__unstableIsSelected: isSelected, | ||
} = this.props; | ||
function handleCompositionStart() { | ||
isComposing.current = true; | ||
// Do not update the selection when characters are being composed as | ||
// this rerenders the component and might distroy internal browser | ||
// editing state. | ||
getDoc().removeEventListener( | ||
'selectionchange', | ||
handleSelectionChange | ||
); | ||
} | ||
// Check if tag name changed. | ||
let shouldReapply = tagName !== prevProps.tagName; | ||
function handleCompositionEnd() { | ||
isComposing.current = false; | ||
// Ensure the value is up-to-date for browsers that don't emit a final | ||
// input event after composition. | ||
handleInput( { inputType: 'insertText' } ); | ||
// Tracking selection changes can be resumed. | ||
getDoc().addEventListener( 'selectionchange', handleSelectionChange ); | ||
} | ||
// Check if the content changed. | ||
shouldReapply = | ||
shouldReapply || | ||
( value !== prevProps.value && value !== this.value ); | ||
const didMount = useRef( false ); | ||
const selectionChanged = | ||
( selectionStart !== prevProps.selectionStart && | ||
selectionStart !== this.record.start ) || | ||
( selectionEnd !== prevProps.selectionEnd && | ||
selectionEnd !== this.record.end ); | ||
/** | ||
* Syncs the selection to local state. A callback for the `selectionchange` | ||
* native events, `keyup`, `mouseup` and `touchend` synthetic events, and | ||
* animation frames after the `focus` event. | ||
* | ||
* @param {Event|WPSyntheticEvent|DOMHighResTimeStamp} event | ||
*/ | ||
function handleSelectionChange( event ) { | ||
if ( ! ref.current ) { | ||
return; | ||
} | ||
// Check if the selection changed. | ||
shouldReapply = | ||
shouldReapply || | ||
( isSelected && ! prevProps.isSelected && selectionChanged ); | ||
if ( document.activeElement !== ref.current ) { | ||
return; | ||
} | ||
const prefix = 'format_prepare_props_'; | ||
const predicate = ( v, key ) => key.startsWith( prefix ); | ||
const prepareProps = pickBy( this.props, predicate ); | ||
const prevPrepareProps = pickBy( prevProps, predicate ); | ||
if ( event.type !== 'selectionchange' && ! isSelected ) { | ||
return; | ||
} | ||
// Check if any format props changed. | ||
shouldReapply = | ||
shouldReapply || ! isShallowEqual( prepareProps, prevPrepareProps ); | ||
if ( disabled ) { | ||
return; | ||
} | ||
// Rerender if the placeholder changed. | ||
shouldReapply = shouldReapply || placeholder !== prevProps.placeholder; | ||
// In case of a keyboard event, ignore selection changes during | ||
// composition. | ||
if ( isComposing.current ) { | ||
return; | ||
} | ||
if ( shouldReapply ) { | ||
this.value = value; | ||
this.record = this.formatToValue( value ); | ||
this.record.start = selectionStart; | ||
this.record.end = selectionEnd; | ||
this.applyRecord( this.record ); | ||
} else if ( selectionChanged ) { | ||
this.record = { | ||
...this.record, | ||
start: selectionStart, | ||
end: selectionEnd, | ||
}; | ||
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 ) { | ||
handleInput(); | ||
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( getWin() ); | ||
} | ||
return; | ||
} | ||
const newValue = { | ||
...oldRecord, | ||
start, | ||
end, | ||
// Allow `getActiveFormats` to get new `activeFormats`. | ||
activeFormats: undefined, | ||
}; | ||
const newActiveFormats = getActiveFormats( | ||
newValue, | ||
EMPTY_ACTIVE_FORMATS | ||
); | ||
// Update the value with the new active formats. | ||
newValue.activeFormats = newActiveFormats; | ||
if ( ! isCaretWithinFormattedText && newActiveFormats.length ) { | ||
onEnterFormattedText(); | ||
} else if ( isCaretWithinFormattedText && ! newActiveFormats.length ) { | ||
onExitFormattedText(); | ||
} | ||
// 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 ); | ||
setActiveFormats( newActiveFormats ); | ||
} | ||
/** | ||
* Converts the outside data structure to our internal representation. | ||
* Sync the value to global state. The node tree and selection will also be | ||
* updated if differences are found. | ||
* | ||
* @param {*} value The outside value, data type depends on props. | ||
* @return {Object} An internal rich-text value. | ||
* @param {Object} newRecord The record to sync and apply. | ||
* @param {Object} $2 Named options. | ||
* @param {boolean} $2.withoutHistory If true, no undo level will be | ||
* created. | ||
*/ | ||
formatToValue( value ) { | ||
const { | ||
format, | ||
__unstableMultilineTag: multilineTag, | ||
preserveWhiteSpace, | ||
__unstableDisableFormats: disableFormats, | ||
} = this.props; | ||
function handleChange( newRecord, { withoutHistory } = {} ) { | ||
if ( disableFormats ) { | ||
return { | ||
text: value, | ||
formats: Array( value.length ), | ||
replacements: Array( value.length ), | ||
}; | ||
newRecord.formats = Array( newRecord.text.length ); | ||
newRecord.replacements = Array( newRecord.text.length ); | ||
} | ||
if ( format !== 'string' ) { | ||
return value; | ||
} | ||
applyRecord( newRecord ); | ||
const prepare = createPrepareEditableTree( | ||
this.props, | ||
'format_value_functions' | ||
const { start, end, activeFormats: newActiveFormats = [] } = newRecord; | ||
const changeHandlers = pickBy( remainingProps, ( v, key ) => | ||
key.startsWith( 'format_on_change_functions_' ) | ||
); | ||
value = create( { | ||
html: value, | ||
multilineTag, | ||
multilineWrapperTags: | ||
multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, | ||
preserveWhiteSpace, | ||
Object.values( changeHandlers ).forEach( ( changeHandler ) => { | ||
changeHandler( newRecord.formats, newRecord.text ); | ||
} ); | ||
value.formats = prepare( value ); | ||
return value; | ||
_value.current = valueToFormat( newRecord ); | ||
record.current = newRecord; | ||
// Selection must be updated first, so it is recorded in history when | ||
// the content change happens. | ||
onSelectionChange( start, end ); | ||
onChange( _value.current ); | ||
setActiveFormats( newActiveFormats ); | ||
if ( ! withoutHistory ) { | ||
createUndoLevel(); | ||
} | ||
} | ||
/** | ||
* Removes editor only formats from the value. | ||
* Select object when they are clicked. The browser will not set any | ||
* selection when clicking e.g. an image. | ||
* | ||
* Editor only formats are applied using `prepareEditableTree`, so we need to | ||
* remove them before converting the internal state | ||
* | ||
* @param {Object} value The internal rich-text value. | ||
* @return {Object} A new rich-text value. | ||
* @param {WPSyntheticEvent} event Synthetic mousedown or touchstart event. | ||
*/ | ||
removeEditorOnlyFormats( value ) { | ||
this.props.formatTypes.forEach( ( formatType ) => { | ||
// Remove formats created by prepareEditableTree, because they are editor only. | ||
if ( formatType.__experimentalCreatePrepareEditableTree ) { | ||
value = removeFormat( | ||
value, | ||
formatType.name, | ||
0, | ||
value.text.length | ||
); | ||
} | ||
} ); | ||
function handlePointerDown( event ) { | ||
const { target } = event; | ||
return value; | ||
// If the child element has no text content, it must be an object. | ||
if ( target === ref.current || target.textContent ) { | ||
return; | ||
} | ||
const { parentNode } = target; | ||
const index = Array.from( parentNode.childNodes ).indexOf( target ); | ||
const range = getDoc().createRange(); | ||
const selection = getWin().getSelection(); | ||
range.setStart( target.parentNode, index ); | ||
range.setEnd( target.parentNode, index + 1 ); | ||
selection.removeAllRanges(); | ||
selection.addRange( range ); | ||
} | ||
const rafId = useRef(); | ||
/** | ||
* Converts the internal value to the external data format. | ||
* Handles a focus event on the contenteditable field, calling the | ||
* `unstableOnFocus` prop callback if one is defined. The callback does not | ||
* receive any arguments. | ||
* | ||
* @param {Object} value The internal rich-text value. | ||
* @return {*} The external data format, data type depends on props. | ||
* This is marked as a private API and the `unstableOnFocus` prop is not | ||
* documented, as the current requirements where it is used are subject to | ||
* future refactoring following `isSelected` handling. | ||
* | ||
* In contrast with `setFocusedElement`, this is only triggered in response | ||
* to focus within the contenteditable field, whereas `setFocusedElement` | ||
* is triggered on focus within any `RichText` descendent element. | ||
* | ||
* @see setFocusedElement | ||
* | ||
* @private | ||
*/ | ||
valueToFormat( value ) { | ||
const { | ||
format, | ||
__unstableMultilineTag: multilineTag, | ||
preserveWhiteSpace, | ||
__unstableDisableFormats: disableFormats, | ||
} = this.props; | ||
function handleFocus() { | ||
if ( onFocus ) { | ||
onFocus(); | ||
} | ||
if ( disableFormats ) { | ||
return value.text; | ||
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, | ||
}; | ||
onSelectionChange( index, index ); | ||
setActiveFormats( EMPTY_ACTIVE_FORMATS ); | ||
} else { | ||
onSelectionChange( record.current.start, record.current.end ); | ||
setActiveFormats( | ||
getActiveFormats( | ||
{ | ||
...record.current, | ||
activeFormats: undefined, | ||
}, | ||
EMPTY_ACTIVE_FORMATS | ||
) | ||
); | ||
} | ||
value = this.removeEditorOnlyFormats( value ); | ||
// Update selection as soon as possible, which is at the next animation | ||
// frame. The event listener for selection changes may be added too late | ||
// at this point, but this focus event is still too early to calculate | ||
// the selection. | ||
rafId.current = getWin().requestAnimationFrame( handleSelectionChange ); | ||
if ( format !== 'string' ) { | ||
return; | ||
getDoc().addEventListener( 'selectionchange', handleSelectionChange ); | ||
if ( setFocusedElement ) { | ||
deprecated( 'wp.blockEditor.RichText setFocusedElement prop', { | ||
alternative: 'selection state from the block editor store.', | ||
} ); | ||
setFocusedElement( instanceId ); | ||
} | ||
return toHTMLString( { value, multilineTag, preserveWhiteSpace } ); | ||
} | ||
Editable( props ) { | ||
const { | ||
tagName: TagName = 'div', | ||
style, | ||
className, | ||
placeholder, | ||
forwardedRef, | ||
disabled, | ||
} = this.props; | ||
const ariaProps = pickBy( this.props, ( value, key ) => | ||
startsWith( key, 'aria-' ) | ||
function handleBlur() { | ||
getDoc().removeEventListener( | ||
'selectionchange', | ||
handleSelectionChange | ||
); | ||
} | ||
return ( | ||
<TagName | ||
// Overridable props. | ||
role="textbox" | ||
aria-multiline | ||
aria-label={ placeholder } | ||
{ ...props } | ||
{ ...ariaProps } | ||
ref={ forwardedRef } | ||
style={ style ? { ...style, whiteSpace } : defaultStyle } | ||
className={ classnames( 'rich-text', className ) } | ||
onPaste={ this.onPaste } | ||
onInput={ this.onInput } | ||
onCompositionStart={ this.onCompositionStart } | ||
onCompositionEnd={ this.onCompositionEnd } | ||
onKeyDown={ | ||
props.onKeyDown | ||
? ( event ) => { | ||
props.onKeyDown( event ); | ||
this.onKeyDown( event ); | ||
} | ||
: this.onKeyDown | ||
} | ||
onFocus={ this.onFocus } | ||
onBlur={ this.onBlur } | ||
onMouseDown={ this.onPointerDown } | ||
onTouchStart={ this.onPointerDown } | ||
// Selection updates must be done at these events as they | ||
// happen before the `selectionchange` event. In some cases, | ||
// the `selectionchange` event may not even fire, for | ||
// example when the window receives focus again on click. | ||
onKeyUp={ this.onSelectionChange } | ||
onMouseUp={ this.onSelectionChange } | ||
onTouchEnd={ this.onSelectionChange } | ||
// Do not set the attribute if disabled. | ||
contentEditable={ disabled ? undefined : true } | ||
suppressContentEditableWarning={ ! disabled } | ||
/> | ||
); | ||
function applyFromProps() { | ||
_value.current = value; | ||
record.current = formatToValue( value ); | ||
record.current.start = selectionStart; | ||
record.current.end = selectionEnd; | ||
applyRecord( record.current ); | ||
} | ||
render() { | ||
const { | ||
__unstableIsSelected: isSelected, | ||
children, | ||
allowedFormats, | ||
withoutInteractiveFormatting, | ||
formatTypes, | ||
forwardedRef, | ||
} = this.props; | ||
const { activeFormats } = this.state; | ||
useEffect( () => { | ||
if ( didMount.current ) { | ||
applyFromProps(); | ||
} | ||
}, [ TagName, placeholder ] ); | ||
const onFocus = () => { | ||
forwardedRef.current.focus(); | ||
this.applyRecord( this.record ); | ||
useEffect( () => { | ||
if ( didMount.current && value !== _value.current ) { | ||
applyFromProps(); | ||
} | ||
}, [ value ] ); | ||
useEffect( () => { | ||
if ( ! didMount.current ) { | ||
return; | ||
} | ||
if ( | ||
isSelected && | ||
( selectionStart !== record.current.start || | ||
selectionEnd !== record.current.end ) | ||
) { | ||
applyFromProps(); | ||
} else { | ||
record.current = { | ||
...record.current, | ||
start: selectionStart, | ||
end: selectionEnd, | ||
}; | ||
} | ||
}, [ selectionStart, selectionEnd, isSelected ] ); | ||
const prefix = 'format_prepare_props_'; | ||
const predicate = ( v, key ) => key.startsWith( prefix ); | ||
const prepareProps = pickBy( remainingProps, predicate ); | ||
useEffect( () => { | ||
if ( didMount.current ) { | ||
applyFromProps(); | ||
} | ||
}, Object.values( prepareProps ) ); | ||
useLayoutEffect( () => { | ||
applyRecord( record.current, { domOnly: true } ); | ||
didMount.current = true; | ||
return () => { | ||
getDoc().removeEventListener( | ||
'selectionchange', | ||
handleSelectionChange | ||
); | ||
getWin().cancelAnimationFrame( rafId.current ); | ||
getWin().clearTimeout( timeout.current ); | ||
}; | ||
}, [] ); | ||
return ( | ||
<> | ||
<BoundaryStyle | ||
activeFormats={ activeFormats } | ||
forwardedRef={ forwardedRef } | ||
function focus() { | ||
ref.current.focus(); | ||
applyRecord( record.current ); | ||
} | ||
const ariaProps = pickBy( remainingProps, ( v, key ) => | ||
startsWith( key, 'aria-' ) | ||
); | ||
const editableProps = { | ||
// Overridable props. | ||
role: 'textbox', | ||
'aria-multiline': '', | ||
'aria-label': placeholder, | ||
...ariaProps, | ||
ref, | ||
style: style ? { ...style, whiteSpace } : defaultStyle, | ||
className: classnames( 'rich-text', className ), | ||
onPaste: handlePaste, | ||
onInput: handleInput, | ||
onCompositionStart: handleCompositionStart, | ||
onCompositionEnd: handleCompositionEnd, | ||
onKeyDown: handleKeyDown, | ||
onFocus: handleFocus, | ||
onBlur: handleBlur, | ||
onMouseDown: handlePointerDown, | ||
onTouchStart: handlePointerDown, | ||
// Selection updates must be done at these events as they | ||
// happen before the `selectionchange` event. In some cases, | ||
// the `selectionchange` event may not even fire, for | ||
// example when the window receives focus again on click. | ||
onKeyUp: handleSelectionChange, | ||
onMouseUp: handleSelectionChange, | ||
onTouchEnd: handleSelectionChange, | ||
// Do not set the attribute if disabled. | ||
contentEditable: disabled ? undefined : true, | ||
suppressContentEditableWarning: ! disabled, | ||
}; | ||
useBoundaryStyle( { ref, activeFormats } ); | ||
useInlineWarning( { ref } ); | ||
return ( | ||
<> | ||
{ isSelected && ( | ||
<FormatEdit | ||
allowedFormats={ allowedFormats } | ||
withoutInteractiveFormatting={ | ||
withoutInteractiveFormatting | ||
} | ||
value={ record.current } | ||
onChange={ handleChange } | ||
onFocus={ focus } | ||
formatTypes={ formatTypes } | ||
/> | ||
<InlineWarning forwardedRef={ forwardedRef } /> | ||
{ isSelected && ( | ||
<FormatEdit | ||
allowedFormats={ allowedFormats } | ||
withoutInteractiveFormatting={ | ||
withoutInteractiveFormatting | ||
} | ||
value={ this.record } | ||
onChange={ this.onChange } | ||
onFocus={ onFocus } | ||
formatTypes={ formatTypes } | ||
/> | ||
) } | ||
{ children && | ||
children( { | ||
isSelected, | ||
value: this.record, | ||
onChange: this.onChange, | ||
onFocus, | ||
Editable: this.Editable, | ||
} ) } | ||
{ ! children && <this.Editable /> } | ||
</> | ||
); | ||
} | ||
) } | ||
{ children && | ||
children( { | ||
isSelected, | ||
value: record.current, | ||
onChange: handleChange, | ||
onFocus: focus, | ||
editableProps, | ||
editableTagName: TagName, | ||
} ) } | ||
{ ! children && <TagName { ...editableProps } /> } | ||
</> | ||
); | ||
} | ||
RichText.defaultProps = { | ||
format: 'string', | ||
value: '', | ||
}; | ||
const RichTextWrapper = withFormatTypes( RichText ); | ||
const RichTextWrapper = compose( [ withSafeTimeout, withFormatTypes ] )( | ||
RichText | ||
); | ||
/** | ||
@@ -1264,0 +1203,0 @@ * Renders a rich content input, providing users with the option to format the |
/** | ||
* Gets the all format objects at the start of the selection. | ||
* | ||
* @param {Object} value Value to inspect. | ||
* @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no active | ||
* formats. | ||
* @param {Object} value Value to inspect. | ||
* @param {Array<Array>} value.formats Formats object data values. | ||
* @param {number} value.start Index to start from. | ||
* @param {number} value.end Index to end. | ||
* @param {Array} value.activeFormats Array to return if there are active formats. | ||
* @param {Array} EMPTY_ACTIVE_FORMATS Array to return if there are no active | ||
* formats. | ||
* | ||
@@ -8,0 +12,0 @@ * @return {?Object} Active format objects. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
0
1248701
17893
1