mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-08-12 10:14:02 +02:00
Optimize isText
, isElement
, isNodeList
and isEditor
(#5859)
* Remove the `isPlainObject` check from `isText` and `isElement` for performance * Optimise `isElement`, `isNodeList` and `isText` further * Update changeset * Fix changeset * Refactor object check into `isObject`
This commit is contained in:
8
.changeset/tough-pianos-train.md
Normal file
8
.changeset/tough-pianos-train.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'slate': patch
|
||||
'slate-history': patch
|
||||
'slate-hyperscript': patch
|
||||
'slate-react': patch
|
||||
---
|
||||
|
||||
Optimize `isElement`, `isText`, `isNodeList` and `isEditor` by removing dependency on `is-plain-object` and by performing shallow checks by default. To perform a full check, including all descendants, pass the `{ deep: true }` option to `isElement`, `isNodeList` or `isEditor`.
|
@@ -13,9 +13,6 @@
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"dependencies": {
|
||||
"is-plain-object": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Operation, Range } from 'slate'
|
||||
import { Operation, Range, isObject } from 'slate'
|
||||
|
||||
interface Batch {
|
||||
operations: Operation[]
|
||||
@@ -24,7 +23,7 @@ export const History = {
|
||||
|
||||
isHistory(value: any): value is History {
|
||||
return (
|
||||
isPlainObject(value) &&
|
||||
isObject(value) &&
|
||||
Array.isArray(value.redos) &&
|
||||
Array.isArray(value.undos) &&
|
||||
(value.redos.length === 0 ||
|
||||
|
@@ -13,9 +13,6 @@
|
||||
"files": [
|
||||
"dist/"
|
||||
],
|
||||
"dependencies": {
|
||||
"is-plain-object": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "^7.23.2",
|
||||
"slate": "^0.114.0",
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Element, createEditor as makeEditor } from 'slate'
|
||||
import { Element, createEditor as makeEditor, isObject } from 'slate'
|
||||
import {
|
||||
createAnchor,
|
||||
createCursor,
|
||||
@@ -86,7 +85,7 @@ const createFactory = <T extends HyperscriptCreators>(creators: T) => {
|
||||
attributes = {}
|
||||
}
|
||||
|
||||
if (!isPlainObject(attributes)) {
|
||||
if (!isObject(attributes)) {
|
||||
children = [attributes].concat(children)
|
||||
attributes = {}
|
||||
}
|
||||
|
@@ -17,7 +17,6 @@
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"direction": "^1.0.4",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"scroll-into-view-if-needed": "^3.1.0",
|
||||
"tiny-invariant": "1.3.1"
|
||||
|
@@ -15,7 +15,6 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"immer": "^10.0.3",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"tiny-warning": "^1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -1,20 +1,14 @@
|
||||
import { Editor, EditorInterface } from '../interfaces/editor'
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Range } from '../interfaces/range'
|
||||
import { Node } from '../interfaces/node'
|
||||
import { Operation } from '../interfaces/operation'
|
||||
|
||||
const IS_EDITOR_CACHE = new WeakMap<object, boolean>()
|
||||
import { isObject } from '../utils'
|
||||
|
||||
export const isEditor: EditorInterface['isEditor'] = (
|
||||
value: any
|
||||
value: any,
|
||||
{ deep = false } = {}
|
||||
): value is Editor => {
|
||||
const cachedIsEditor = IS_EDITOR_CACHE.get(value)
|
||||
if (cachedIsEditor !== undefined) {
|
||||
return cachedIsEditor
|
||||
}
|
||||
|
||||
if (!isPlainObject(value)) {
|
||||
if (!isObject(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -35,10 +29,10 @@ export const isEditor: EditorInterface['isEditor'] = (
|
||||
typeof value.onChange === 'function' &&
|
||||
typeof value.removeMark === 'function' &&
|
||||
typeof value.getDirtyPaths === 'function' &&
|
||||
(value.marks === null || isPlainObject(value.marks)) &&
|
||||
(value.marks === null || isObject(value.marks)) &&
|
||||
(value.selection === null || Range.isRange(value.selection)) &&
|
||||
Node.isNodeList(value.children) &&
|
||||
(!deep || Node.isNodeList(value.children)) &&
|
||||
Operation.isOperationList(value.operations)
|
||||
IS_EDITOR_CACHE.set(value, isEditor)
|
||||
|
||||
return isEditor
|
||||
}
|
||||
|
@@ -6,3 +6,4 @@ export * from './transforms-node'
|
||||
export * from './transforms-selection'
|
||||
export * from './transforms-text'
|
||||
export * from './types'
|
||||
export * from './utils/is-object'
|
||||
|
@@ -213,6 +213,10 @@ export interface EditorFragmentDeletionOptions {
|
||||
direction?: TextDirection
|
||||
}
|
||||
|
||||
export interface EditorIsEditorOptions {
|
||||
deep?: boolean
|
||||
}
|
||||
|
||||
export interface EditorLeafOptions {
|
||||
depth?: number
|
||||
edge?: LeafEdge
|
||||
@@ -469,7 +473,7 @@ export interface EditorInterface {
|
||||
/**
|
||||
* Check if a value is an `Editor` object.
|
||||
*/
|
||||
isEditor: (value: any) => value is Editor
|
||||
isEditor: (value: any, options?: EditorIsEditorOptions) => value is Editor
|
||||
|
||||
/**
|
||||
* Check if a value is a read-only `Element` object.
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Ancestor, Descendant, Editor, ExtendedType, Node, Path } from '..'
|
||||
import {
|
||||
Ancestor,
|
||||
Descendant,
|
||||
Editor,
|
||||
ExtendedType,
|
||||
Node,
|
||||
Path,
|
||||
isObject,
|
||||
} from '..'
|
||||
|
||||
/**
|
||||
* `Element` objects are a type of node in a Slate document that contain other
|
||||
@@ -13,21 +20,31 @@ export interface BaseElement {
|
||||
|
||||
export type Element = ExtendedType<'Element', BaseElement>
|
||||
|
||||
export interface ElementIsElementOptions {
|
||||
deep?: boolean
|
||||
}
|
||||
|
||||
export interface ElementInterface {
|
||||
/**
|
||||
* Check if a value implements the 'Ancestor' interface.
|
||||
*/
|
||||
isAncestor: (value: any) => value is Ancestor
|
||||
isAncestor: (
|
||||
value: any,
|
||||
options?: ElementIsElementOptions
|
||||
) => value is Ancestor
|
||||
|
||||
/**
|
||||
* Check if a value implements the `Element` interface.
|
||||
*/
|
||||
isElement: (value: any) => value is Element
|
||||
isElement: (value: any, options?: ElementIsElementOptions) => value is Element
|
||||
|
||||
/**
|
||||
* Check if a value is an array of `Element` objects.
|
||||
*/
|
||||
isElementList: (value: any) => value is Element[]
|
||||
isElementList: (
|
||||
value: any,
|
||||
options?: ElementIsElementOptions
|
||||
) => value is Element[]
|
||||
|
||||
/**
|
||||
* Check if a set of props is a partial of Element.
|
||||
@@ -56,24 +73,42 @@ export interface ElementInterface {
|
||||
/**
|
||||
* Shared the function with isElementType utility
|
||||
*/
|
||||
const isElement = (value: any): value is Element => {
|
||||
return (
|
||||
isPlainObject(value) &&
|
||||
Node.isNodeList(value.children) &&
|
||||
!Editor.isEditor(value)
|
||||
)
|
||||
const isElement = (
|
||||
value: any,
|
||||
{ deep = false }: ElementIsElementOptions = {}
|
||||
): value is Element => {
|
||||
if (!isObject(value)) return false
|
||||
|
||||
// PERF: No need to use the full Editor.isEditor here
|
||||
const isEditor = typeof value.apply === 'function'
|
||||
if (isEditor) return false
|
||||
|
||||
const isChildrenValid = deep
|
||||
? Node.isNodeList(value.children)
|
||||
: Array.isArray(value.children)
|
||||
|
||||
return isChildrenValid
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Element: ElementInterface = {
|
||||
isAncestor(value: any): value is Ancestor {
|
||||
return isPlainObject(value) && Node.isNodeList(value.children)
|
||||
isAncestor(
|
||||
value: any,
|
||||
{ deep = false }: ElementIsElementOptions = {}
|
||||
): value is Ancestor {
|
||||
return isObject(value) && Node.isNodeList(value.children, { deep })
|
||||
},
|
||||
|
||||
isElement,
|
||||
|
||||
isElementList(value: any): value is Element[] {
|
||||
return Array.isArray(value) && value.every(val => Element.isElement(val))
|
||||
isElementList(
|
||||
value: any,
|
||||
{ deep = false }: ElementIsElementOptions = {}
|
||||
): value is Element[] {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.every(val => Element.isElement(val, { deep }))
|
||||
)
|
||||
},
|
||||
|
||||
isElementProps(props: any): props is Partial<Element> {
|
||||
|
@@ -32,6 +32,10 @@ export interface NodeElementsOptions {
|
||||
pass?: (node: NodeEntry) => boolean
|
||||
}
|
||||
|
||||
export interface NodeIsNodeOptions {
|
||||
deep?: boolean
|
||||
}
|
||||
|
||||
export interface NodeLevelsOptions {
|
||||
reverse?: boolean
|
||||
}
|
||||
@@ -144,12 +148,12 @@ export interface NodeInterface {
|
||||
/**
|
||||
* Check if a value implements the `Node` interface.
|
||||
*/
|
||||
isNode: (value: any) => value is Node
|
||||
isNode: (value: any, options?: NodeIsNodeOptions) => value is Node
|
||||
|
||||
/**
|
||||
* Check if a value is a list of `Node` objects.
|
||||
*/
|
||||
isNodeList: (value: any) => value is Node[]
|
||||
isNodeList: (value: any, options?: NodeIsNodeOptions) => value is Node[]
|
||||
|
||||
/**
|
||||
* Get the last node entry in a root node from a path.
|
||||
@@ -211,8 +215,6 @@ export interface NodeInterface {
|
||||
) => Generator<NodeEntry<Text>, void, undefined>
|
||||
}
|
||||
|
||||
const IS_NODE_LIST_CACHE = new WeakMap<any[], boolean>()
|
||||
|
||||
// eslint-disable-next-line no-redeclare
|
||||
export const Node: NodeInterface = {
|
||||
ancestor(root: Node, path: Path): Ancestor {
|
||||
@@ -437,23 +439,21 @@ export const Node: NodeInterface = {
|
||||
return true
|
||||
},
|
||||
|
||||
isNode(value: any): value is Node {
|
||||
isNode(value: any, { deep = false }: NodeIsNodeOptions = {}): value is Node {
|
||||
return (
|
||||
Text.isText(value) || Element.isElement(value) || Editor.isEditor(value)
|
||||
Text.isText(value) ||
|
||||
Element.isElement(value, { deep }) ||
|
||||
Editor.isEditor(value, { deep })
|
||||
)
|
||||
},
|
||||
|
||||
isNodeList(value: any): value is Node[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return false
|
||||
}
|
||||
const cachedResult = IS_NODE_LIST_CACHE.get(value)
|
||||
if (cachedResult !== undefined) {
|
||||
return cachedResult
|
||||
}
|
||||
const isNodeList = value.every(val => Node.isNode(val))
|
||||
IS_NODE_LIST_CACHE.set(value, isNodeList)
|
||||
return isNodeList
|
||||
isNodeList(
|
||||
value: any,
|
||||
{ deep = false }: NodeIsNodeOptions = {}
|
||||
): value is Node[] {
|
||||
return (
|
||||
Array.isArray(value) && value.every(val => Node.isNode(val, { deep }))
|
||||
)
|
||||
},
|
||||
|
||||
last(root: Node, path: Path): NodeEntry {
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { ExtendedType, Node, Path, Range } from '..'
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { ExtendedType, Node, Path, Range, isObject } from '..'
|
||||
|
||||
export type BaseInsertNodeOperation = {
|
||||
type: 'insert_node'
|
||||
@@ -178,7 +177,7 @@ export const Operation: OperationInterface = {
|
||||
},
|
||||
|
||||
isOperation(value: any): value is Operation {
|
||||
if (!isPlainObject(value)) {
|
||||
if (!isObject(value)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -195,7 +194,7 @@ export const Operation: OperationInterface = {
|
||||
return (
|
||||
typeof value.position === 'number' &&
|
||||
Path.isPath(value.path) &&
|
||||
isPlainObject(value.properties)
|
||||
isObject(value.properties)
|
||||
)
|
||||
case 'move_node':
|
||||
return Path.isPath(value.path) && Path.isPath(value.newPath)
|
||||
@@ -210,21 +209,20 @@ export const Operation: OperationInterface = {
|
||||
case 'set_node':
|
||||
return (
|
||||
Path.isPath(value.path) &&
|
||||
isPlainObject(value.properties) &&
|
||||
isPlainObject(value.newProperties)
|
||||
isObject(value.properties) &&
|
||||
isObject(value.newProperties)
|
||||
)
|
||||
case 'set_selection':
|
||||
return (
|
||||
(value.properties === null && Range.isRange(value.newProperties)) ||
|
||||
(value.newProperties === null && Range.isRange(value.properties)) ||
|
||||
(isPlainObject(value.properties) &&
|
||||
isPlainObject(value.newProperties))
|
||||
(isObject(value.properties) && isObject(value.newProperties))
|
||||
)
|
||||
case 'split_node':
|
||||
return (
|
||||
Path.isPath(value.path) &&
|
||||
typeof value.position === 'number' &&
|
||||
isPlainObject(value.properties)
|
||||
isObject(value.properties)
|
||||
)
|
||||
default:
|
||||
return false
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { produce } from 'immer'
|
||||
import { ExtendedType, Operation, Path } from '..'
|
||||
import { ExtendedType, Operation, Path, isObject } from '..'
|
||||
import { TextDirection } from '../types/types'
|
||||
|
||||
/**
|
||||
@@ -89,7 +88,7 @@ export const Point: PointInterface = {
|
||||
|
||||
isPoint(value: any): value is Point {
|
||||
return (
|
||||
isPlainObject(value) &&
|
||||
isObject(value) &&
|
||||
typeof value.offset === 'number' &&
|
||||
Path.isPath(value.path)
|
||||
)
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { produce } from 'immer'
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { ExtendedType, Operation, Path, Point, PointEntry } from '..'
|
||||
import { ExtendedType, Operation, Path, Point, PointEntry, isObject } from '..'
|
||||
import { RangeDirection } from '../types/types'
|
||||
|
||||
/**
|
||||
@@ -200,7 +199,7 @@ export const Range: RangeInterface = {
|
||||
|
||||
isRange(value: any): value is Range {
|
||||
return (
|
||||
isPlainObject(value) &&
|
||||
isObject(value) &&
|
||||
Point.isPoint(value.anchor) &&
|
||||
Point.isPoint(value.focus)
|
||||
)
|
||||
|
@@ -1,5 +1,4 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { Path, Range } from '..'
|
||||
import { Range, isObject } from '..'
|
||||
import { ExtendedType } from '../types/custom-types'
|
||||
import { isDeepEqual } from '../utils/deep-equal'
|
||||
|
||||
@@ -93,7 +92,7 @@ export const Text: TextInterface = {
|
||||
},
|
||||
|
||||
isText(value: any): value is Text {
|
||||
return isPlainObject(value) && typeof value.text === 'string'
|
||||
return isObject(value) && typeof value.text === 'string'
|
||||
},
|
||||
|
||||
isTextList(value: any): value is Text[] {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { isPlainObject } from 'is-plain-object'
|
||||
import { isObject } from './is-object'
|
||||
|
||||
/*
|
||||
Custom deep equal comparison for Slate nodes.
|
||||
@@ -17,13 +17,13 @@ export const isDeepEqual = (
|
||||
for (const key in node) {
|
||||
const a = node[key]
|
||||
const b = another[key]
|
||||
if (isPlainObject(a) && isPlainObject(b)) {
|
||||
if (!isDeepEqual(a, b)) return false
|
||||
} else if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
} else if (isObject(a) && isObject(b)) {
|
||||
if (!isDeepEqual(a, b)) return false
|
||||
} else if (a !== b) {
|
||||
return false
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export * from './deep-equal'
|
||||
export * from './get-default-insert-location'
|
||||
export * from './is-object'
|
||||
export * from './match-path'
|
||||
export * from './string'
|
||||
export * from './types'
|
||||
|
2
packages/slate/src/utils/is-object.ts
Normal file
2
packages/slate/src/utils/is-object.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const isObject = (value: any) =>
|
||||
typeof value === 'object' && value !== null
|
@@ -13302,7 +13302,6 @@ __metadata:
|
||||
resolution: "slate-history@workspace:packages/slate-history"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.23.2"
|
||||
is-plain-object: "npm:^5.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
slate: "npm:^0.114.0"
|
||||
slate-hyperscript: "npm:^0.100.0"
|
||||
@@ -13317,7 +13316,6 @@ __metadata:
|
||||
resolution: "slate-hyperscript@workspace:packages/slate-hyperscript"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.23.2"
|
||||
is-plain-object: "npm:^5.0.0"
|
||||
slate: "npm:^0.114.0"
|
||||
source-map-loader: "npm:^4.0.1"
|
||||
peerDependencies:
|
||||
@@ -13427,7 +13425,6 @@ __metadata:
|
||||
"@types/resize-observer-browser": "npm:^0.1.8"
|
||||
direction: "npm:^1.0.4"
|
||||
is-hotkey: "npm:^0.2.0"
|
||||
is-plain-object: "npm:^5.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
@@ -13451,7 +13448,6 @@ __metadata:
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.23.2"
|
||||
immer: "npm:^10.0.3"
|
||||
is-plain-object: "npm:^5.0.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
slate-hyperscript: "npm:^0.100.0"
|
||||
source-map-loader: "npm:^4.0.1"
|
||||
|
Reference in New Issue
Block a user