feat(richtext-lexical): add table hover actions to quickly add rows or columns
This commit is contained in:
@@ -58,7 +58,7 @@
|
||||
"uuid": "10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lexical/eslint-plugin": " 0.17.0",
|
||||
"@lexical/eslint-plugin": "0.17.0",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/next": "workspace:*",
|
||||
"@payloadcms/translations": "workspace:*",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { slashMenuBasicGroupWithItems } from '../shared/slashMenu/basicGroup.js'
|
||||
import { toolbarAddDropdownGroupWithItems } from '../shared/toolbar/addDropdownGroup.js'
|
||||
import { TableActionMenuPlugin } from './plugins/TableActionMenuPlugin/index.js'
|
||||
import { TableCellResizerPlugin } from './plugins/TableCellResizerPlugin/index.js'
|
||||
import { TableHoverActionsPlugin } from './plugins/TableHoverActionsPlugin/index.js'
|
||||
import {
|
||||
OPEN_TABLE_DRAWER_COMMAND,
|
||||
TableContext,
|
||||
@@ -29,6 +30,10 @@ export const TableFeatureClient = createClientFeature({
|
||||
Component: TableActionMenuPlugin,
|
||||
position: 'floatingAnchorElem',
|
||||
},
|
||||
{
|
||||
Component: TableHoverActionsPlugin,
|
||||
position: 'floatingAnchorElem',
|
||||
},
|
||||
],
|
||||
providers: [TableContext],
|
||||
slashMenu: {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.TableCellResizer__resizer {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -20,9 +20,9 @@ import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
import type { PluginComponent, PluginComponentWithAnchor } from '../../../typesClient.js'
|
||||
import type { PluginComponent } from '../../../typesClient.js'
|
||||
|
||||
import './index.scss'
|
||||
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
|
||||
|
||||
type MousePosition = {
|
||||
x: number
|
||||
@@ -38,6 +38,7 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
|
||||
const targetRef = useRef<HTMLElement | null>(null)
|
||||
const resizerRef = useRef<HTMLDivElement | null>(null)
|
||||
const tableRectRef = useRef<ClientRect | null>(null)
|
||||
const editorConfig = useEditorConfigContext()
|
||||
|
||||
const mouseStartPosRef = useRef<MousePosition | null>(null)
|
||||
const [mouseCurrentPos, updateMouseCurrentPos] = useState<MousePosition | null>(null)
|
||||
@@ -378,12 +379,12 @@ function TableCellResizer({ editor }: { editor: LexicalEditor }): JSX.Element {
|
||||
{activeCell != null && !isMouseDown && (
|
||||
<React.Fragment>
|
||||
<div
|
||||
className="TableCellResizer__resizer TableCellResizer__ui"
|
||||
className={`${editorConfig.editorConfig.lexical.theme.tableCellResizer} TableCellResizer__ui`}
|
||||
onMouseDown={toggleResize('right')}
|
||||
style={resizerStyles.right || undefined}
|
||||
/>
|
||||
<div
|
||||
className="TableCellResizer__resizer TableCellResizer__ui"
|
||||
className={`${editorConfig.editorConfig.lexical.theme.tableCellResizer} TableCellResizer__ui`}
|
||||
onMouseDown={toggleResize('bottom')}
|
||||
style={resizerStyles.bottom || undefined}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import type { TableCellNode, TableRowNode } from '@lexical/table'
|
||||
import type { EditorConfig, NodeKey } from 'lexical'
|
||||
import type { JSX } from 'react'
|
||||
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import {
|
||||
$getTableColumnIndexFromTableCellNode,
|
||||
$getTableRowIndexFromTableCellNode,
|
||||
$insertTableColumn__EXPERIMENTAL,
|
||||
$insertTableRow__EXPERIMENTAL,
|
||||
$isTableCellNode,
|
||||
$isTableNode,
|
||||
TableNode,
|
||||
} from '@lexical/table'
|
||||
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
||||
import { $getNearestNodeFromDOMNode } from 'lexical'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import * as React from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
import { useEditorConfigContext } from '../../../../lexical/config/client/EditorConfigProvider.js'
|
||||
import { useDebounce } from '../../utils/useDebounce.js'
|
||||
|
||||
const BUTTON_WIDTH_PX = 20
|
||||
|
||||
function TableHoverActionsContainer({ anchorElem }: { anchorElem: HTMLElement }): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const editorConfig = useEditorConfigContext()
|
||||
const [isShownRow, setShownRow] = useState<boolean>(false)
|
||||
const [isShownColumn, setShownColumn] = useState<boolean>(false)
|
||||
const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(false)
|
||||
const [position, setPosition] = useState({})
|
||||
const codeSetRef = useRef<Set<NodeKey>>(new Set())
|
||||
const tableDOMNodeRef = useRef<HTMLElement | null>(null)
|
||||
|
||||
const debouncedOnMouseMove = useDebounce(
|
||||
(event: MouseEvent) => {
|
||||
const { isOutside, tableDOMNode } = getMouseInfo(event, editorConfig.editorConfig?.lexical)
|
||||
|
||||
if (isOutside) {
|
||||
setShownRow(false)
|
||||
setShownColumn(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (!tableDOMNode) {
|
||||
return
|
||||
}
|
||||
|
||||
tableDOMNodeRef.current = tableDOMNode
|
||||
|
||||
let hoveredRowNode: TableCellNode | null = null
|
||||
let hoveredColumnNode: TableCellNode | null = null
|
||||
let tableDOMElement: HTMLElement | null = null
|
||||
|
||||
editor.update(() => {
|
||||
const maybeTableCell = $getNearestNodeFromDOMNode(tableDOMNode)
|
||||
|
||||
if ($isTableCellNode(maybeTableCell)) {
|
||||
const table = $findMatchingParent(maybeTableCell, (node) => $isTableNode(node))
|
||||
if (!$isTableNode(table)) {
|
||||
return
|
||||
}
|
||||
|
||||
tableDOMElement = editor.getElementByKey(table?.getKey())
|
||||
|
||||
if (tableDOMElement) {
|
||||
const rowCount = table.getChildrenSize()
|
||||
const colCount =
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
((table as TableNode).getChildAtIndex(0) as TableRowNode)?.getChildrenSize()
|
||||
|
||||
const rowIndex = $getTableRowIndexFromTableCellNode(maybeTableCell)
|
||||
const colIndex = $getTableColumnIndexFromTableCellNode(maybeTableCell)
|
||||
|
||||
if (rowIndex === rowCount - 1) {
|
||||
hoveredRowNode = maybeTableCell
|
||||
} else if (colIndex === colCount - 1) {
|
||||
hoveredColumnNode = maybeTableCell
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (tableDOMElement) {
|
||||
const {
|
||||
bottom: tableElemBottom,
|
||||
height: tableElemHeight,
|
||||
right: tableElemRight,
|
||||
width: tableElemWidth,
|
||||
x: tableElemX,
|
||||
y: tableElemY,
|
||||
} = (tableDOMElement as HTMLTableElement).getBoundingClientRect()
|
||||
|
||||
const { left: editorElemLeft, y: editorElemY } = anchorElem.getBoundingClientRect()
|
||||
|
||||
if (hoveredRowNode) {
|
||||
setShownColumn(false)
|
||||
|
||||
setShownRow(true)
|
||||
setPosition({
|
||||
height: BUTTON_WIDTH_PX,
|
||||
left: tableElemX - editorElemLeft,
|
||||
top: tableElemBottom - editorElemY + 5,
|
||||
width: tableElemWidth,
|
||||
})
|
||||
} else if (hoveredColumnNode) {
|
||||
setShownColumn(true)
|
||||
setShownRow(false)
|
||||
setPosition({
|
||||
height: tableElemHeight,
|
||||
left: tableElemRight - editorElemLeft + 5,
|
||||
top: tableElemY - editorElemY,
|
||||
width: BUTTON_WIDTH_PX,
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
50,
|
||||
250,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldListenMouseMove) {
|
||||
return
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', debouncedOnMouseMove)
|
||||
|
||||
return () => {
|
||||
setShownRow(false)
|
||||
setShownColumn(false)
|
||||
|
||||
document.removeEventListener('mousemove', debouncedOnMouseMove)
|
||||
}
|
||||
}, [shouldListenMouseMove, debouncedOnMouseMove])
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerMutationListener(
|
||||
TableNode,
|
||||
(mutations) => {
|
||||
editor.getEditorState().read(() => {
|
||||
for (const [key, type] of mutations) {
|
||||
switch (type) {
|
||||
case 'created':
|
||||
codeSetRef.current.add(key)
|
||||
setShouldListenMouseMove(codeSetRef.current.size > 0)
|
||||
break
|
||||
|
||||
case 'destroyed':
|
||||
codeSetRef.current.delete(key)
|
||||
setShouldListenMouseMove(codeSetRef.current.size > 0)
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
{ skipInitialization: false },
|
||||
),
|
||||
)
|
||||
}, [editor])
|
||||
|
||||
const insertAction = (insertRow: boolean) => {
|
||||
editor.update(() => {
|
||||
if (tableDOMNodeRef.current) {
|
||||
const maybeTableNode = $getNearestNodeFromDOMNode(tableDOMNodeRef.current)
|
||||
maybeTableNode?.selectEnd()
|
||||
if (insertRow) {
|
||||
$insertTableRow__EXPERIMENTAL()
|
||||
setShownRow(false)
|
||||
} else {
|
||||
$insertTableColumn__EXPERIMENTAL()
|
||||
setShownColumn(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isShownRow && (
|
||||
<button
|
||||
className={editorConfig.editorConfig.lexical.theme.tableAddRows}
|
||||
onClick={() => insertAction(true)}
|
||||
style={{ ...position }}
|
||||
/>
|
||||
)}
|
||||
{isShownColumn && (
|
||||
<button
|
||||
className={editorConfig.editorConfig.lexical.theme.tableAddColumns}
|
||||
onClick={() => insertAction(false)}
|
||||
style={{ ...position }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function getMouseInfo(
|
||||
event: MouseEvent,
|
||||
editorConfig: EditorConfig,
|
||||
): {
|
||||
isOutside: boolean
|
||||
tableDOMNode: HTMLElement | null
|
||||
} {
|
||||
const target = event.target
|
||||
|
||||
if (target && target instanceof HTMLElement) {
|
||||
const tableDOMNode = target.closest<HTMLElement>(
|
||||
`td.${editorConfig.theme.tableCell}, th.${editorConfig.theme.tableCell}`,
|
||||
)
|
||||
|
||||
const isOutside = !(
|
||||
tableDOMNode ||
|
||||
target.closest<HTMLElement>(`button.${editorConfig.theme.tableAddRows}`) ||
|
||||
target.closest<HTMLElement>(`button.${editorConfig.theme.tableAddColumns}`) ||
|
||||
target.closest<HTMLElement>(`div.${editorConfig.theme.tableCellResizer}`)
|
||||
)
|
||||
|
||||
return { isOutside, tableDOMNode }
|
||||
} else {
|
||||
return { isOutside: true, tableDOMNode: null }
|
||||
}
|
||||
}
|
||||
|
||||
export function TableHoverActionsPlugin({
|
||||
anchorElem = document.body,
|
||||
}: {
|
||||
anchorElem?: HTMLElement
|
||||
}): React.ReactPortal | null {
|
||||
return createPortal(<TableHoverActionsContainer anchorElem={anchorElem} />, anchorElem)
|
||||
}
|
||||
@@ -4,11 +4,11 @@
|
||||
&__table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
max-width: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: scroll;
|
||||
table-layout: fixed;
|
||||
width: calc(100% - 25px);
|
||||
margin: 30px 0;
|
||||
width: max-content;
|
||||
margin: 0 25px 30px 0;
|
||||
|
||||
::selection {
|
||||
background: rgba(172, 206, 247);
|
||||
@@ -82,36 +82,41 @@
|
||||
|
||||
&__tableAddColumns {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 20px;
|
||||
background-color: #eee;
|
||||
background-color: var(--theme-elevation-100);
|
||||
height: 100%;
|
||||
right: 0;
|
||||
animation: table-controls 0.2s ease;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__tableAddColumns:hover {
|
||||
background-color: #c9dbf0;
|
||||
&__tableAddColumns:after, &__tableAddRows:after {
|
||||
background-image: url(../../../../lexical/ui/icons/Add/index.svg);
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&__tableAddColumns:hover, &__tableAddRows:hover {
|
||||
background-color: var(--theme-elevation-200);
|
||||
}
|
||||
|
||||
&__tableAddRows {
|
||||
position: absolute;
|
||||
bottom: -25px;
|
||||
width: calc(100% - 25px);
|
||||
background-color: #eee;
|
||||
height: 20px;
|
||||
left: 0;
|
||||
background-color: var(--theme-elevation-100);
|
||||
animation: table-controls 0.2s ease;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__tableAddRows:hover {
|
||||
background-color: #c9dbf0;
|
||||
}
|
||||
|
||||
@keyframes table-controls {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@@ -162,5 +167,9 @@ html[data-theme='dark'] {
|
||||
&__tableCellHeader {
|
||||
background-color: var(--theme-elevation-50);
|
||||
}
|
||||
|
||||
&__tableAddColumns:after, &__tableAddRows:after {
|
||||
background-image: url(../../../../lexical/ui/icons/Add/light.svg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// Copied & modified from https://github.com/lodash/lodash/blob/main/src/debounce.ts
|
||||
/*
|
||||
The MIT License
|
||||
|
||||
Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||
|
||||
Based on Underscore.js, copyright Jeremy Ashkenas,
|
||||
DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
|
||||
|
||||
This software consists of voluntary contributions made by many
|
||||
individuals. For exact contribution history, see the revision history
|
||||
available at https://github.com/lodash/lodash
|
||||
|
||||
The following license applies to all parts of this software except as
|
||||
documented below:
|
||||
|
||||
====
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
Copyright and related rights for sample code are waived via CC0. Sample
|
||||
code is defined as all source code displayed within the prose of the
|
||||
documentation.
|
||||
|
||||
CC0: http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
====
|
||||
|
||||
Files located in the node_modules and vendor directories are externally
|
||||
maintained libraries used by this software which have their own
|
||||
licenses; we recommend you read them, as their terms may differ from the
|
||||
terms above.
|
||||
*/
|
||||
|
||||
/** Error message constants. */
|
||||
const FUNC_ERROR_TEXT = 'Expected a function'
|
||||
|
||||
/* Built-in method references for those with the same name as other `lodash` methods. */
|
||||
const nativeMax = Math.max,
|
||||
nativeMin = Math.min
|
||||
|
||||
/**
|
||||
* Creates a debounced function that delays invoking `func` until after `wait`
|
||||
* milliseconds have elapsed since the last time the debounced function was
|
||||
* invoked. The debounced function comes with a `cancel` method to cancel
|
||||
* delayed `func` invocations and a `flush` method to immediately invoke them.
|
||||
* Provide `options` to indicate whether `func` should be invoked on the
|
||||
* leading and/or trailing edge of the `wait` timeout. The `func` is invoked
|
||||
* with the last arguments provided to the debounced function. Subsequent
|
||||
* calls to the debounced function return the result of the last `func`
|
||||
* invocation.
|
||||
*
|
||||
* **Note:** If `leading` and `trailing` options are `true`, `func` is
|
||||
* invoked on the trailing edge of the timeout only if the debounced function
|
||||
* is invoked more than once during the `wait` timeout.
|
||||
*
|
||||
* If `wait` is `0` and `leading` is `false`, `func` invocation is deferred
|
||||
* until to the next tick, similar to `setTimeout` with a timeout of `0`.
|
||||
*
|
||||
* See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
|
||||
* for details over the differences between `_.debounce` and `_.throttle`.
|
||||
*
|
||||
* @static
|
||||
* @memberOf _
|
||||
* @since 0.1.0
|
||||
* @category Function
|
||||
* @param {Function} func The function to debounce.
|
||||
* @param {number} [wait=0] The number of milliseconds to delay.
|
||||
* @param {Object} [options={}] The options object.
|
||||
* @param {boolean} [options.leading=false]
|
||||
* Specify invoking on the leading edge of the timeout.
|
||||
* @param {number} [options.maxWait]
|
||||
* The maximum time `func` is allowed to be delayed before it's invoked.
|
||||
* @param {boolean} [options.trailing=true]
|
||||
* Specify invoking on the trailing edge of the timeout.
|
||||
* @returns {Function} Returns the new debounced function.
|
||||
* @example
|
||||
*
|
||||
* // Avoid costly calculations while the window size is in flux.
|
||||
* jQuery(window).on('resize', _.debounce(calculateLayout, 150));
|
||||
*
|
||||
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
|
||||
* jQuery(element).on('click', _.debounce(sendMail, 300, {
|
||||
* 'leading': true,
|
||||
* 'trailing': false
|
||||
* }));
|
||||
*
|
||||
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
|
||||
* var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
|
||||
* var source = new EventSource('/stream');
|
||||
* jQuery(source).on('message', debounced);
|
||||
*
|
||||
* // Cancel the trailing debounced invocation.
|
||||
* jQuery(window).on('popstate', debounced.cancel);
|
||||
*/
|
||||
function debounce(func, wait, options) {
|
||||
let lastArgs,
|
||||
lastThis,
|
||||
maxWait,
|
||||
result,
|
||||
timerId,
|
||||
lastCallTime,
|
||||
lastInvokeTime = 0,
|
||||
leading = false,
|
||||
maxing = false,
|
||||
trailing = true
|
||||
|
||||
if (typeof func != 'function') {
|
||||
throw new TypeError(FUNC_ERROR_TEXT)
|
||||
}
|
||||
wait = wait || 0
|
||||
if (typeof options === 'object') {
|
||||
leading = !!options.leading
|
||||
maxing = 'maxWait' in options
|
||||
maxWait = maxing ? nativeMax(options.maxWait || 0, wait) : maxWait
|
||||
trailing = 'trailing' in options ? !!options.trailing : trailing
|
||||
}
|
||||
|
||||
function invokeFunc(time) {
|
||||
const args = lastArgs,
|
||||
thisArg = lastThis
|
||||
|
||||
lastArgs = lastThis = undefined
|
||||
lastInvokeTime = time
|
||||
result = func.apply(thisArg, args)
|
||||
return result
|
||||
}
|
||||
|
||||
function leadingEdge(time) {
|
||||
// Reset any `maxWait` timer.
|
||||
lastInvokeTime = time
|
||||
// Start the timer for the trailing edge.
|
||||
timerId = setTimeout(timerExpired, wait)
|
||||
// Invoke the leading edge.
|
||||
return leading ? invokeFunc(time) : result
|
||||
}
|
||||
|
||||
function remainingWait(time) {
|
||||
const timeSinceLastCall = time - lastCallTime,
|
||||
timeSinceLastInvoke = time - lastInvokeTime,
|
||||
timeWaiting = wait - timeSinceLastCall
|
||||
|
||||
return maxing ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting
|
||||
}
|
||||
|
||||
function shouldInvoke(time) {
|
||||
const timeSinceLastCall = time - lastCallTime,
|
||||
timeSinceLastInvoke = time - lastInvokeTime
|
||||
|
||||
// Either this is the first call, activity has stopped and we're at the
|
||||
// trailing edge, the system time has gone backwards and we're treating
|
||||
// it as the trailing edge, or we've hit the `maxWait` limit.
|
||||
return (
|
||||
lastCallTime === undefined ||
|
||||
timeSinceLastCall >= wait ||
|
||||
timeSinceLastCall < 0 ||
|
||||
(maxing && timeSinceLastInvoke >= maxWait)
|
||||
)
|
||||
}
|
||||
|
||||
function timerExpired() {
|
||||
const time = Date.now()
|
||||
if (shouldInvoke(time)) {
|
||||
return trailingEdge(time)
|
||||
}
|
||||
// Restart the timer.
|
||||
timerId = setTimeout(timerExpired, remainingWait(time))
|
||||
}
|
||||
|
||||
function trailingEdge(time) {
|
||||
timerId = undefined
|
||||
|
||||
// Only invoke if we have `lastArgs` which means `func` has been
|
||||
// debounced at least once.
|
||||
if (trailing && lastArgs) {
|
||||
return invokeFunc(time)
|
||||
}
|
||||
lastArgs = lastThis = undefined
|
||||
return result
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
if (timerId !== undefined) {
|
||||
clearTimeout(timerId)
|
||||
}
|
||||
lastInvokeTime = 0
|
||||
lastArgs = lastCallTime = lastThis = timerId = undefined
|
||||
}
|
||||
|
||||
function flush() {
|
||||
return timerId === undefined ? result : trailingEdge(Date.now())
|
||||
}
|
||||
|
||||
function debounced() {
|
||||
const time = Date.now(),
|
||||
isInvoking = shouldInvoke(time)
|
||||
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
lastArgs = arguments
|
||||
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
||||
lastThis = this
|
||||
lastCallTime = time
|
||||
|
||||
if (isInvoking) {
|
||||
if (timerId === undefined) {
|
||||
return leadingEdge(lastCallTime)
|
||||
}
|
||||
if (maxing) {
|
||||
// Handle invocations in a tight loop.
|
||||
clearTimeout(timerId)
|
||||
timerId = setTimeout(timerExpired, wait)
|
||||
return invokeFunc(lastCallTime)
|
||||
}
|
||||
}
|
||||
if (timerId === undefined) {
|
||||
timerId = setTimeout(timerExpired, wait)
|
||||
}
|
||||
return result
|
||||
}
|
||||
debounced.cancel = cancel
|
||||
debounced.flush = flush
|
||||
return debounced
|
||||
}
|
||||
|
||||
export default debounce
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
import debounce from './debounce.js'
|
||||
|
||||
export function useDebounce<T extends (...args: never[]) => void>(
|
||||
fn: T,
|
||||
ms: number,
|
||||
maxWait?: number,
|
||||
) {
|
||||
const funcRef = useRef<T | null>(null)
|
||||
funcRef.current = fn
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
(...args: Parameters<T>) => {
|
||||
if (funcRef.current) {
|
||||
funcRef.current(...args)
|
||||
}
|
||||
},
|
||||
ms,
|
||||
{ maxWait },
|
||||
),
|
||||
[ms, maxWait],
|
||||
)
|
||||
}
|
||||
@@ -99,6 +99,7 @@ export function convertParagraphNode(
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const defaultRichTextValue: SerializedEditorState = {
|
||||
format: '',
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
textStyle: '',
|
||||
version: 1,
|
||||
} as SerializedParagraphNode,
|
||||
],
|
||||
|
||||
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@@ -1232,7 +1232,7 @@ importers:
|
||||
version: 10.0.0
|
||||
devDependencies:
|
||||
'@lexical/eslint-plugin':
|
||||
specifier: ' 0.17.0'
|
||||
specifier: 0.17.0
|
||||
version: 0.17.0(eslint@9.6.0)
|
||||
'@payloadcms/eslint-config':
|
||||
specifier: workspace:*
|
||||
|
||||
@@ -594,6 +594,19 @@ export interface BlockField {
|
||||
blockType: 'text';
|
||||
}[]
|
||||
| null;
|
||||
blocksWithLocalizedArray?:
|
||||
| {
|
||||
array?:
|
||||
| {
|
||||
text?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'localizedArray';
|
||||
}[]
|
||||
| null;
|
||||
blocksWithSimilarConfigs?:
|
||||
| (
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user