diff --git a/.changeset/tough-pianos-train.md b/.changeset/tough-pianos-train.md new file mode 100644 index 000000000..dbfbc1d82 --- /dev/null +++ b/.changeset/tough-pianos-train.md @@ -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`. diff --git a/packages/slate-history/package.json b/packages/slate-history/package.json index df9258a56..58d2061a8 100644 --- a/packages/slate-history/package.json +++ b/packages/slate-history/package.json @@ -13,9 +13,6 @@ "files": [ "dist/" ], - "dependencies": { - "is-plain-object": "^5.0.0" - }, "devDependencies": { "@babel/runtime": "^7.23.2", "lodash": "^4.17.21", diff --git a/packages/slate-history/src/history.ts b/packages/slate-history/src/history.ts index 9a43e209c..03b3badc7 100644 --- a/packages/slate-history/src/history.ts +++ b/packages/slate-history/src/history.ts @@ -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 || diff --git a/packages/slate-hyperscript/package.json b/packages/slate-hyperscript/package.json index 45e2513e1..0b64ffec0 100644 --- a/packages/slate-hyperscript/package.json +++ b/packages/slate-hyperscript/package.json @@ -13,9 +13,6 @@ "files": [ "dist/" ], - "dependencies": { - "is-plain-object": "^5.0.0" - }, "devDependencies": { "@babel/runtime": "^7.23.2", "slate": "^0.114.0", diff --git a/packages/slate-hyperscript/src/hyperscript.ts b/packages/slate-hyperscript/src/hyperscript.ts index d52172132..f08216cc7 100644 --- a/packages/slate-hyperscript/src/hyperscript.ts +++ b/packages/slate-hyperscript/src/hyperscript.ts @@ -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 = (creators: T) => { attributes = {} } - if (!isPlainObject(attributes)) { + if (!isObject(attributes)) { children = [attributes].concat(children) attributes = {} } diff --git a/packages/slate-react/package.json b/packages/slate-react/package.json index 7e7d16483..aa8d3aa99 100644 --- a/packages/slate-react/package.json +++ b/packages/slate-react/package.json @@ -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" diff --git a/packages/slate/package.json b/packages/slate/package.json index 4736210ed..2f6903c89 100644 --- a/packages/slate/package.json +++ b/packages/slate/package.json @@ -15,7 +15,6 @@ ], "dependencies": { "immer": "^10.0.3", - "is-plain-object": "^5.0.0", "tiny-warning": "^1.0.3" }, "devDependencies": { diff --git a/packages/slate/src/editor/is-editor.ts b/packages/slate/src/editor/is-editor.ts index 8196bcc35..0c8112eb0 100644 --- a/packages/slate/src/editor/is-editor.ts +++ b/packages/slate/src/editor/is-editor.ts @@ -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() +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 } diff --git a/packages/slate/src/index.ts b/packages/slate/src/index.ts index 5d30415b9..1bac5ed10 100644 --- a/packages/slate/src/index.ts +++ b/packages/slate/src/index.ts @@ -6,3 +6,4 @@ export * from './transforms-node' export * from './transforms-selection' export * from './transforms-text' export * from './types' +export * from './utils/is-object' diff --git a/packages/slate/src/interfaces/editor.ts b/packages/slate/src/interfaces/editor.ts index c9827e4a8..b22174f6e 100644 --- a/packages/slate/src/interfaces/editor.ts +++ b/packages/slate/src/interfaces/editor.ts @@ -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. diff --git a/packages/slate/src/interfaces/element.ts b/packages/slate/src/interfaces/element.ts index 9d1f437af..19b7fcaa7 100644 --- a/packages/slate/src/interfaces/element.ts +++ b/packages/slate/src/interfaces/element.ts @@ -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 { diff --git a/packages/slate/src/interfaces/node.ts b/packages/slate/src/interfaces/node.ts index 6ab334e7f..f2e759203 100644 --- a/packages/slate/src/interfaces/node.ts +++ b/packages/slate/src/interfaces/node.ts @@ -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, void, undefined> } -const IS_NODE_LIST_CACHE = new WeakMap() - // 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 { diff --git a/packages/slate/src/interfaces/operation.ts b/packages/slate/src/interfaces/operation.ts index 0b2a6cd3f..3c0bdda45 100644 --- a/packages/slate/src/interfaces/operation.ts +++ b/packages/slate/src/interfaces/operation.ts @@ -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 diff --git a/packages/slate/src/interfaces/point.ts b/packages/slate/src/interfaces/point.ts index 2d8fa81e8..cb4acebbd 100644 --- a/packages/slate/src/interfaces/point.ts +++ b/packages/slate/src/interfaces/point.ts @@ -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) ) diff --git a/packages/slate/src/interfaces/range.ts b/packages/slate/src/interfaces/range.ts index b2f797017..4a6711cc9 100644 --- a/packages/slate/src/interfaces/range.ts +++ b/packages/slate/src/interfaces/range.ts @@ -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) ) diff --git a/packages/slate/src/interfaces/text.ts b/packages/slate/src/interfaces/text.ts index e8178ae65..96a1faff1 100644 --- a/packages/slate/src/interfaces/text.ts +++ b/packages/slate/src/interfaces/text.ts @@ -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[] { diff --git a/packages/slate/src/utils/deep-equal.ts b/packages/slate/src/utils/deep-equal.ts index 6677c3dfe..85302190d 100644 --- a/packages/slate/src/utils/deep-equal.ts +++ b/packages/slate/src/utils/deep-equal.ts @@ -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 } diff --git a/packages/slate/src/utils/index.ts b/packages/slate/src/utils/index.ts index cd6e4d4ee..4d56496be 100644 --- a/packages/slate/src/utils/index.ts +++ b/packages/slate/src/utils/index.ts @@ -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' diff --git a/packages/slate/src/utils/is-object.ts b/packages/slate/src/utils/is-object.ts new file mode 100644 index 000000000..d0c6e35f5 --- /dev/null +++ b/packages/slate/src/utils/is-object.ts @@ -0,0 +1,2 @@ +export const isObject = (value: any) => + typeof value === 'object' && value !== null diff --git a/yarn.lock b/yarn.lock index 361db44a5..f15d695d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"