├── .nvmrc ├── .gitignore ├── .npmrc ├── tsconfig.dist.json ├── typing.d.ts ├── .circleci └── config.yml ├── LICENSE ├── tsconfig.json ├── src ├── types.ts ├── index.ts ├── README.md ├── helpers.ts ├── node.ts ├── selection.ts └── transforms.ts ├── .eslintrc.cjs ├── jestFrameworkSetup.ts ├── test-helpers ├── index.ts └── schema.ts ├── package.json ├── __tests__ ├── helpers.ts ├── node.ts ├── selection.ts └── transforms.ts ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.17.1 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | .vscode 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | package-lock=true 3 | -------------------------------------------------------------------------------- /tsconfig.dist.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /typing.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | export {}; 3 | 4 | declare global { 5 | namespace jest { 6 | 7 | interface Matchers { 8 | toEqualDocument(expected: unknown): CustomMatcherResult; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.1 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.1/language-javascript/ for more details 4 | # 5 | version: 2.1 6 | 7 | orbs: 8 | node: circleci/node@5.1.0 9 | 10 | jobs: 11 | build: 12 | executor: node/default # use the default executor defined within the orb 13 | steps: 14 | - checkout 15 | - node/install-packages: 16 | pkg-manager: npm 17 | 18 | - run: npm install 19 | 20 | # run tests! 21 | - run: npm run test-ci 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Atlassian Pty Ltd 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowSyntheticDefaultImports": true, 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "declaration": true, 14 | "emitDeclarationOnly": true, 15 | "declarationDir": "./dist" 16 | }, 17 | "include": ["src", "__tests__", "test-helpers"], 18 | "files": ["./typing.d.ts"] 19 | } 20 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { Node as PMNode, NodeType, Fragment } from 'prosemirror-model'; 2 | 3 | export type NodeWithPos = { 4 | pos: number; 5 | node: PMNode; 6 | }; 7 | 8 | export type ContentNodeWithPos = { 9 | start: number; 10 | depth: number; 11 | } & NodeWithPos; 12 | 13 | export type DomAtPos = (pos: number) => { node: Node; offset: number }; 14 | export type FindPredicate = (node: PMNode) => boolean; 15 | 16 | export type Predicate = FindPredicate; 17 | export type FindResult = ContentNodeWithPos | undefined; 18 | 19 | export type Attrs = { [key: string]: unknown }; 20 | export type NodeTypeParam = NodeType | Array; 21 | export type Content = PMNode | Fragment; 22 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | }, 6 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 7 | parser: '@typescript-eslint/parser', 8 | parserOptions: { 9 | 'ecmaVersion': 2020 10 | }, 11 | plugins: ['@typescript-eslint'], 12 | rules: { 13 | indent: 'off', 14 | 'no-debugger': 'off', 15 | '@typescript-eslint/indent': 'off', 16 | '@typescript-eslint/explicit-function-return-type': 'error', 17 | '@typescript-eslint/no-var-requires': 'off', 18 | '@typescript-eslint/no-explicit-any': 'error', 19 | '@typescript-eslint/no-unused-vars': 'error', 20 | '@typescript-eslint/no-this-alias': 'error', 21 | '@typescript-eslint/no-empty-function': 'error', 22 | 'import/prefer-default-export': 'off', 23 | '@typescript-eslint/camelcase': 'off', 24 | }, 25 | settings: { 26 | 'import/resolver': { 27 | node: { 28 | extensions: ['.js', '.ts', '.tsx'], 29 | }, 30 | }, 31 | }, 32 | root: true, 33 | }; 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | findParentNode, 3 | findParentNodeClosestToPos, 4 | findParentDomRef, 5 | hasParentNode, 6 | findParentNodeOfType, 7 | findParentNodeOfTypeClosestToPos, 8 | hasParentNodeOfType, 9 | findParentDomRefOfType, 10 | findSelectedNodeOfType, 11 | findPositionOfNodeBefore, 12 | findDomRefAtPos, 13 | } from './selection'; 14 | export { 15 | flatten, 16 | findChildren, 17 | findTextNodes, 18 | findInlineNodes, 19 | findBlockNodes, 20 | findChildrenByAttr, 21 | findChildrenByType, 22 | findChildrenByMark, 23 | contains, 24 | } from './node'; 25 | export { 26 | removeParentNodeOfType, 27 | replaceParentNodeOfType, 28 | removeSelectedNode, 29 | replaceSelectedNode, 30 | setTextSelection, 31 | safeInsert, 32 | setParentNodeMarkup, 33 | selectParentNodeOfType, 34 | removeNodeBefore, 35 | } from './transforms'; 36 | export { isNodeSelection, canInsert } from './helpers'; 37 | export type { 38 | DomAtPos, 39 | NodeTypeParam, 40 | Predicate, 41 | NodeWithPos, 42 | ContentNodeWithPos, 43 | } from './types'; 44 | -------------------------------------------------------------------------------- /jestFrameworkSetup.ts: -------------------------------------------------------------------------------- 1 | const diff = require('jest-diff'); 2 | 3 | expect.extend({ 4 | toEqualDocument(actual, expected) { 5 | const pass = this.equals(actual.toJSON(), expected.toJSON()); 6 | const message = pass 7 | ? (): string => 8 | `${this.utils.matcherHint('.not.toEqualDocument')}\n\n` + 9 | `Expected JSON value of document to not equal:\n ${this.utils.printExpected( 10 | expected 11 | )}\n` + 12 | `Actual JSON:\n ${this.utils.printReceived(actual)}` 13 | : (): string => { 14 | const diffString = diff(expected, actual, { 15 | expand: this.expand, 16 | }); 17 | return ( 18 | `${this.utils.matcherHint('.toEqualDocument')}\n\n` + 19 | `Expected JSON value of document to equal:\n${this.utils.printExpected( 20 | expected 21 | )}\n` + 22 | `Actual JSON:\n ${this.utils.printReceived(actual)}` + 23 | `${diffString ? `\n\nDifference:\n\n${diffString}` : ''}` 24 | ); 25 | }; 26 | 27 | return { 28 | pass, 29 | actual, 30 | expected, 31 | message, 32 | name: 'toEqualDocument', 33 | }; 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Utils library for ProseMirror 2 | 3 | [![npm](https://img.shields.io/npm/v/prosemirror-utils.svg?style=flat-square)](https://www.npmjs.com/package/prosemirror-utils) 4 | [![License](https://img.shields.io/npm/l/prosemirror-utils.svg?style=flat-square)](http://www.apache.org/licenses/LICENSE-2.0) 5 | [![Github Issues](https://img.shields.io/github/issues/atlassian/prosemirror-utils.svg?style=flat-square)](https://github.com/atlassian/prosemirror-utils/issues) 6 | [![CircleCI](https://img.shields.io/circleci/project/github/atlassian/prosemirror-utils.svg?style=flat-square)](https://circleci.com/gh/atlassian/prosemirror-utils) 7 | [![codecov](https://codecov.io/gh/atlassian/prosemirror-utils/branch/master/graph/badge.svg)](https://codecov.io/gh/atlassian/prosemirror-utils) 8 | [![Downloads](https://img.shields.io/npm/dw/prosemirror-utils.svg?style=flat-square)](https://www.npmjs.com/package/prosemirror-utils) 9 | [![Code size](https://img.shields.io/github/languages/code-size/atlassian/prosemirror-utils.svg?style=flat-square)](https://www.npmjs.com/package/prosemirror-utils) 10 | 11 | ## Quick Start 12 | 13 | Install `prosemirror-utils` package from npm: 14 | 15 | ```sh 16 | npm install prosemirror-utils 17 | ``` 18 | 19 | ## Public API documentation 20 | 21 | ### Utils for working with `selection` 22 | 23 | @findParentNode 24 | 25 | @findParentNodeClosestToPos 26 | 27 | @findParentDomRef 28 | 29 | @hasParentNode 30 | 31 | @findParentNodeOfType 32 | 33 | @findParentNodeOfTypeClosestToPos 34 | 35 | @hasParentNodeOfType 36 | 37 | @findParentDomRefOfType 38 | 39 | @findSelectedNodeOfType 40 | 41 | @isNodeSelection 42 | 43 | @findPositionOfNodeBefore 44 | 45 | @findDomRefAtPos 46 | 47 | ### Utils for working with ProseMirror `node` 48 | 49 | @flatten 50 | 51 | @findChildren 52 | 53 | @findTextNodes 54 | 55 | @findInlineNodes 56 | 57 | @findBlockNodes 58 | 59 | @findChildrenByAttr 60 | 61 | @findChildrenByType 62 | 63 | @findChildrenByMark 64 | 65 | @contains 66 | 67 | ### Utils for document transformation 68 | 69 | @removeParentNodeOfType 70 | 71 | @replaceParentNodeOfType 72 | 73 | @removeSelectedNode 74 | 75 | @replaceSelectedNode 76 | 77 | @canInsert 78 | 79 | @safeInsert 80 | 81 | @setParentNodeMarkup 82 | 83 | @selectParentNodeOfType 84 | 85 | @removeNodeBefore 86 | 87 | @setTextSelection 88 | 89 | ## License 90 | 91 | - **Apache 2.0** : http://www.apache.org/licenses/LICENSE-2.0 92 | -------------------------------------------------------------------------------- /test-helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { builders } from 'prosemirror-test-builder'; 2 | import { EditorState, TextSelection, NodeSelection } from 'prosemirror-state'; 3 | import { Node as PMNode } from 'prosemirror-model'; 4 | import { EditorView } from 'prosemirror-view'; 5 | import schema from './schema'; 6 | 7 | type Tag = { 8 | cursor?: number; 9 | node?: number; 10 | start?: number; 11 | end?: number; 12 | }; 13 | type Ref = { 14 | tag: Tag; 15 | }; 16 | type DocumentTest = PMNode & Ref; 17 | const initSelection = ( 18 | doc: DocumentTest 19 | ): TextSelection | NodeSelection | undefined => { 20 | const { cursor, node, start, end } = doc.tag; 21 | 22 | if (typeof node === 'number') { 23 | return new NodeSelection(doc.resolve(node)); 24 | } 25 | if (typeof cursor === 'number') { 26 | return new TextSelection(doc.resolve(cursor)); 27 | } 28 | if (typeof start === 'number' && typeof end === 'number') { 29 | return new TextSelection(doc.resolve(start), doc.resolve(end)); 30 | } 31 | }; 32 | 33 | const testHelpers = builders(schema, { 34 | doc: { nodeType: 'doc' }, 35 | p: { nodeType: 'paragraph' }, 36 | text: { nodeType: 'text' }, 37 | atomInline: { nodeType: 'atomInline' }, 38 | atomBlock: { nodeType: 'atomBlock' }, 39 | atomContainer: { nodeType: 'atomContainer' }, 40 | heading: { nodeType: 'heading' }, 41 | blockquote: { nodeType: 'blockquote' }, 42 | a: { markType: 'link', href: 'foo' }, 43 | strong: { markType: 'strong' }, 44 | em: { markType: 'em' }, 45 | code: { markType: 'code' }, 46 | code_block: { nodeType: 'code_block' }, 47 | hr: { markType: 'rule' }, 48 | }); 49 | 50 | type EditorHelper = { 51 | state: EditorState; 52 | view: EditorView; 53 | } & Tag; 54 | 55 | let view: EditorView; 56 | afterEach(() => { 57 | if (!view) { 58 | return; 59 | } 60 | 61 | view.destroy(); 62 | const editorMount = document.querySelector('#editor-mount'); 63 | if (editorMount && editorMount.parentNode) { 64 | editorMount.parentNode.removeChild(editorMount); 65 | } 66 | }); 67 | const createEditor = (doc: DocumentTest): EditorHelper => { 68 | const editorMount = document.createElement('div'); 69 | editorMount.setAttribute('id', 'editor-mount'); 70 | 71 | document.body.appendChild(editorMount); 72 | const state = EditorState.create({ 73 | doc, 74 | schema, 75 | selection: initSelection(doc), 76 | }); 77 | view = new EditorView(editorMount, { state }); 78 | 79 | return { state, view, ...doc.tag }; 80 | }; 81 | 82 | export { createEditor, testHelpers }; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prosemirror-utils", 3 | "version": "1.2.2", 4 | "description": "Utils library for ProseMirror", 5 | "type": "module", 6 | "main": "dist/index.cjs", 7 | "module": "dist/index.esm.js", 8 | "types": "dist/index.d.ts", 9 | "publishConfig": { 10 | "registry": "https://registry.npmjs.org" 11 | }, 12 | "author": { 13 | "name": "Eduard Shvedai", 14 | "email": "eshvedai@gmail.com", 15 | "url": "https://github.com/eshvedai" 16 | }, 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "import": "./dist/index.js", 21 | "require": "./dist/index.cjs" 22 | } 23 | }, 24 | "maintainers": [ 25 | { 26 | "name": "Eduard Shvedai", 27 | "email": "eshvedai@atlassian.com" 28 | }, 29 | { 30 | "name": "Rodrigo Vieira", 31 | "email": "rvieira@atlassian.com" 32 | } 33 | ], 34 | "license": "Apache-2.0", 35 | "repository": { 36 | "type": "git", 37 | "url": "git://github.com/atlassian/prosemirror-utils.git" 38 | }, 39 | "keywords": [ 40 | "ProseMirror", 41 | "utils", 42 | "helpers" 43 | ], 44 | "jest": { 45 | "preset": "ts-jest", 46 | "setupFilesAfterEnv": [ 47 | "./jestFrameworkSetup.ts" 48 | ], 49 | "testEnvironment": "jsdom" 50 | }, 51 | "files": [ 52 | "dist" 53 | ], 54 | "scripts": { 55 | "lint": "eslint ./src/ --ext .ts --fix", 56 | "build": "./build.js && tsc --project tsconfig.dist.json", 57 | "test": "jest", 58 | "test-ci": "NODE_ENV=testing jest --coverage && codecov", 59 | "prepare": "npm run build" 60 | }, 61 | "peerDependencies": { 62 | "prosemirror-model": "^1.19.2", 63 | "prosemirror-state": "^1.4.3" 64 | }, 65 | "devDependencies": { 66 | "@types/jest": "^29.5.3", 67 | "@typescript-eslint/eslint-plugin": "^6.0.0", 68 | "@typescript-eslint/parser": "^6.0.0", 69 | "codecov": "^3.1.0", 70 | "esbuild": "^0.18.12", 71 | "eslint": "^8.44.0", 72 | "eslint-config-prettier": "^8.8.0", 73 | "husky": "^1.3.0", 74 | "jest": "^29.6.1", 75 | "jest-environment-jsdom": "^29.6.1", 76 | "lint-staged": "^13.2.3", 77 | "prettier": "^2.8.8", 78 | "prosemirror-model": "1.19.2", 79 | "prosemirror-schema-basic": "^1.2.2", 80 | "prosemirror-state": "^1.4.3", 81 | "prosemirror-test-builder": "^1.1.1", 82 | "prosemirror-transform": "^1.7.3", 83 | "prosemirror-view": "^1.1.1", 84 | "ts-jest": "^29.1.1", 85 | "typescript": "^5.1.6" 86 | }, 87 | "lint-staged": { 88 | "*.{js, md}$": [ 89 | "prettier --write" 90 | ] 91 | }, 92 | "prettier": { 93 | "singleQuote": true, 94 | "trailing-comma": "es5" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /__tests__/helpers.ts: -------------------------------------------------------------------------------- 1 | import { createEditor, testHelpers } from '../test-helpers'; 2 | import { Fragment } from 'prosemirror-model'; 3 | import { canInsert, removeNodeAtPos } from '../src/helpers'; 4 | 5 | const { doc, p, strong, atomInline } = testHelpers; 6 | 7 | describe('helpers', () => { 8 | describe('canInsert', () => { 9 | it('should return true if insertion of a given node is allowed at the current cursor position', () => { 10 | const { state } = createEditor(doc(p('one'))); 11 | const { 12 | selection: { $from }, 13 | } = state; 14 | const node = state.schema.nodes.atomInline.createChecked(); 15 | expect(canInsert($from, node)).toBe(true); 16 | }); 17 | 18 | it('should return true if insertion of a given Fragment is allowed at the current cursor position', () => { 19 | const { state } = createEditor(doc(p('one'))); 20 | const { 21 | selection: { $from }, 22 | } = state; 23 | const node = state.schema.nodes.atomInline.createChecked(); 24 | expect(canInsert($from, Fragment.from(node))).toBe(true); 25 | }); 26 | 27 | it('should return false a insertion of a given node is not allowed', () => { 28 | const { state } = createEditor( 29 | doc(p(strong('zero'), 'one'), p('three')) 30 | ); 31 | const { 32 | selection: { $from }, 33 | } = state; 34 | const node = state.schema.nodes.paragraph.createChecked( 35 | {}, 36 | state.schema.text('two') 37 | ); 38 | expect(canInsert($from, node)).toBe(false); 39 | }); 40 | 41 | it('should return false a insertion of a given Fragment is not allowed', () => { 42 | const { state } = createEditor( 43 | doc(p(strong('zero'), 'one'), p('three')) 44 | ); 45 | const { 46 | selection: { $from }, 47 | } = state; 48 | const node = state.schema.nodes.paragraph.createChecked( 49 | {}, 50 | state.schema.text('two') 51 | ); 52 | expect(canInsert($from, Fragment.from(node))).toBe(false); 53 | }); 54 | }); 55 | 56 | describe('removeNodeAtPos', () => { 57 | it('should remove a block top level node at the given position', () => { 58 | const { 59 | state: { tr }, 60 | } = createEditor(doc(p('x'), p('one'))); 61 | const newTr = removeNodeAtPos(3)(tr); 62 | expect(newTr).not.toBe(tr); 63 | expect(newTr.doc).toEqualDocument(doc(p('x'))); 64 | }); 65 | 66 | it('should remove a nested inline node at the given position', () => { 67 | const { 68 | state: { tr }, 69 | } = createEditor(doc(p('one', atomInline()))); 70 | const newTr = removeNodeAtPos(4)(tr); 71 | expect(newTr).not.toBe(tr); 72 | expect(newTr.doc).toEqualDocument(doc(p('one'))); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.2 (2024-05-13) 2 | 3 | Fixing package version numbers. 4 | 5 | ## 1.2.1-0 (2023-07-14) 6 | 7 | findParentNode will not try to validate the parents by default 8 | 9 | ## 1.1.4 (2023-07-14) 10 | 11 | Fix build files 12 | 13 | ## 1.1.3 (2023-07-14) 14 | 15 | # Changed 16 | 17 | Fix #83: findParentNode is now returning undefined when selection head is not inside the same parent 18 | 19 | ## 1.1.2 (2023-07-14) 20 | 21 | # Changed 22 | 23 | Add esm format to the build output 24 | 25 | ## 1.1.1 (2023-07-14) 26 | 27 | # Changed 28 | 29 | Fix peerDependency issue on distribute packages 30 | 31 | ## 1.1.0 (2023-07-14) 32 | 33 | # Changed 34 | 35 | TypeScript project migration 36 | 37 | ## 1.0.0 (2020-09-21) 38 | 39 | ### Changed 40 | 41 | - Breaking change; removed `prosemirror-tables` dependency 42 | 43 | All utility functions related to tables will be moved to `editor-tables`. A link will be added once it is publicly available. 44 | 45 | We are in the process of deprecating `promisemirror-tables` as the code has become increasingly difficult to maintain. 46 | 47 | The functions removed in this release are: 48 | 49 | - addColumnAt 50 | - addRowAt 51 | - cloneRowAt 52 | - convertArrayOfRowsToTableNode 53 | - convertTableNodeToArrayOfRows 54 | - createCell 55 | - createTable 56 | - emptyCell 57 | - findCellClosestToPos 58 | - findCellRectClosestToPos 59 | - findTable 60 | - findTableClosestToPos 61 | - forEachCellInColumn 62 | - forEachCellInRow 63 | - getCellsInColumn 64 | - getCellsInRow 65 | - getCellsInTable 66 | - getSelectionRangeInColumn 67 | - getSelectionRangeInRow 68 | - getSelectionRect 69 | - isCellSelection 70 | - isColumnSelected 71 | - isRectSelected 72 | - isRowSelected 73 | - isTableSelected 74 | - moveColumn 75 | - moveRow 76 | - moveTableColumn 77 | - moveTableRow 78 | - removeColumnAt 79 | - removeColumnClosestToPos 80 | - removeRowAt 81 | - removeRowClosestToPos 82 | - removeSelectedColumns 83 | - removeSelectedRows 84 | - removeTable 85 | - selectColumn 86 | - selectRow 87 | - selectTable 88 | - setCellAttrs 89 | - tableNodeTypes 90 | - transpose 91 | 92 | ## 0.9.6 (2018-08-07) 93 | 94 | ### Changed 95 | 96 | - Upgrade prosemirror-tables dependecy to 0.9.1 97 | 98 | ### Fixed 99 | 100 | - Fix types for convertTableNodeToArrayOfRows and convertArrayOfRowsToTableNode, they were using ProsemirrorModel[] instead of Array 101 | 102 | ## 0.5.0 (2018-06-04) 103 | 104 | ### Breaking changes 105 | 106 | Changed returning value of all selection utils to `{ node, start, pos}`, where 107 | 108 | - `start` points to the start position of the node 109 | - `pos` points directly before the node 110 | - `node` ProseMirror node 111 | Previously, `pos` used to point to the `start` position of the node. 112 | -------------------------------------------------------------------------------- /test-helpers/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Schema, 3 | type Node as PMNode, 4 | type DOMOutputSpec, 5 | } from 'prosemirror-model'; 6 | import { nodes, marks } from 'prosemirror-schema-basic'; 7 | 8 | const { 9 | doc, 10 | paragraph, 11 | text, 12 | horizontal_rule: rule, 13 | blockquote, 14 | heading, 15 | code_block, 16 | } = nodes; 17 | 18 | type Attrs = { 19 | [key: string]: unknown; 20 | }; 21 | const atomInline = { 22 | inline: true, 23 | group: 'inline', 24 | atom: true, 25 | attrs: { 26 | color: { default: null }, 27 | }, 28 | selectable: true, 29 | parseDOM: [ 30 | { 31 | tag: 'span[data-node-type="atomInline"]', 32 | getAttrs: (dom: HTMLElement | string): Attrs => { 33 | return { 34 | color: (dom as HTMLElement).getAttribute('data-color'), 35 | }; 36 | }, 37 | }, 38 | ], 39 | toDOM(node: PMNode): DOMOutputSpec { 40 | const { color } = node.attrs; 41 | const attrs = { 42 | 'data-node-type': 'atomInline', 43 | 'data-color': color, 44 | }; 45 | return ['span', attrs]; 46 | }, 47 | }; 48 | 49 | const atomBlock = { 50 | inline: false, 51 | group: 'block', 52 | atom: true, 53 | attrs: { 54 | color: { default: null }, 55 | }, 56 | selectable: true, 57 | parseDOM: [ 58 | { 59 | tag: 'div[data-node-type="atomBlock"]', 60 | getAttrs: (dom: HTMLElement | string): Attrs => { 61 | return { 62 | color: (dom as HTMLElement).getAttribute('data-color'), 63 | }; 64 | }, 65 | }, 66 | ], 67 | toDOM(node: PMNode): DOMOutputSpec { 68 | const { color } = node.attrs; 69 | const attrs = { 70 | 'data-node-type': 'atomBlock', 71 | 'data-color': color, 72 | }; 73 | return ['div', attrs]; 74 | }, 75 | }; 76 | 77 | const atomContainer = { 78 | inline: false, 79 | group: 'block', 80 | content: 'atomBlock', 81 | parseDOM: [ 82 | { 83 | tag: 'div[data-node-type="atomBlockContainer"]', 84 | }, 85 | ], 86 | toDOM(): DOMOutputSpec { 87 | return ['div', { 'data-node-type': 'atomBlockContainer' }]; 88 | }, 89 | }; 90 | 91 | const containerWithRestrictedContent = { 92 | inline: false, 93 | group: 'block', 94 | content: 'paragraph+', 95 | parseDOM: [ 96 | { 97 | tag: 'div[data-node-type="containerWithRestrictedContent"]', 98 | }, 99 | ], 100 | toDOM(): DOMOutputSpec { 101 | return ['div', { 'data-node-type': 'containerWithRestrictedContent' }]; 102 | }, 103 | }; 104 | 105 | const article = { 106 | inline: false, 107 | group: 'block', 108 | content: 'section*', 109 | parseDOM: [ 110 | { 111 | tag: 'article', 112 | }, 113 | ], 114 | toDOM(): DOMOutputSpec { 115 | return ['article', 0]; 116 | }, 117 | }; 118 | 119 | const section = { 120 | inline: false, 121 | group: 'block', 122 | content: 'paragraph*', 123 | parseDOM: [ 124 | { 125 | tag: 'section', 126 | }, 127 | ], 128 | toDOM(): DOMOutputSpec { 129 | return ['section']; 130 | }, 131 | }; 132 | 133 | export default new Schema({ 134 | nodes: { 135 | doc, 136 | heading, 137 | paragraph, 138 | text, 139 | atomInline, 140 | atomBlock, 141 | atomContainer, 142 | containerWithRestrictedContent, 143 | blockquote, 144 | rule, 145 | code_block, 146 | article, 147 | section, 148 | }, 149 | marks, 150 | }); 151 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { Selection, NodeSelection, type Transaction } from 'prosemirror-state'; 2 | import { Fragment, Node as PMNode, type ResolvedPos } from 'prosemirror-model'; 3 | import { setTextSelection } from './transforms'; 4 | import type { NodeTypeParam, Content } from './types'; 5 | 6 | // Checks if current selection is a `NodeSelection`. 7 | // 8 | // ```javascript 9 | // if (isNodeSelection(tr.selection)) { 10 | // // ... 11 | // } 12 | // ``` 13 | export const isNodeSelection = ( 14 | selection: Selection 15 | ): selection is NodeSelection => { 16 | return selection instanceof NodeSelection; 17 | }; 18 | 19 | // Checks if the type a given `node` equals to a given `nodeType`. 20 | export const equalNodeType = ( 21 | nodeType: NodeTypeParam, 22 | node: PMNode 23 | ): boolean => { 24 | return ( 25 | (Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1) || 26 | node.type === nodeType 27 | ); 28 | }; 29 | 30 | // Creates a new transaction object from a given transaction 31 | export const cloneTr = (tr: Transaction): Transaction => { 32 | return Object.assign(Object.create(tr), tr).setTime(Date.now()); 33 | }; 34 | 35 | // Returns a `replace` transaction that replaces a node at a given position with the given `content`. 36 | // It will return the original transaction if replacing is not possible. 37 | // `position` should point at the position immediately before the node. 38 | export const replaceNodeAtPos = 39 | (position: number, content: Content) => 40 | (tr: Transaction): Transaction => { 41 | const node = tr.doc.nodeAt(position); 42 | const $pos = tr.doc.resolve(position); 43 | if (!node) { 44 | return tr; 45 | } 46 | 47 | if (canReplace($pos, content)) { 48 | tr = tr.replaceWith(position, position + node.nodeSize, content); 49 | const start = tr.selection.$from.pos - 1; 50 | // put cursor inside of the inserted node 51 | tr = setTextSelection(Math.max(start, 0), -1)(tr); 52 | // move cursor to the start of the node 53 | tr = setTextSelection(tr.selection.$from.start())(tr); 54 | return cloneTr(tr); 55 | } 56 | return tr; 57 | }; 58 | 59 | // Checks if replacing a node at a given `$pos` inside of the `doc` node with the given `content` is possible. 60 | export const canReplace = ($pos: ResolvedPos, content: Content): boolean => { 61 | const node = $pos.node($pos.depth); 62 | return ( 63 | node && 64 | node.type.validContent( 65 | content instanceof Fragment ? content : Fragment.from(content) 66 | ) 67 | ); 68 | }; 69 | 70 | // Returns a `delete` transaction that removes a node at a given position with the given `node`. 71 | // `position` should point at the position immediately before the node. 72 | export const removeNodeAtPos = 73 | (position: number) => 74 | (tr: Transaction): Transaction => { 75 | const node = tr.doc.nodeAt(position); 76 | if (!node) { 77 | return tr; 78 | } 79 | 80 | return cloneTr(tr.delete(position, position + node.nodeSize)); 81 | }; 82 | 83 | // Checks if a given `content` can be inserted at the given `$pos` 84 | // 85 | // ```javascript 86 | // const { selection: { $from } } = state; 87 | // const node = state.schema.nodes.atom.createChecked(); 88 | // if (canInsert($from, node)) { 89 | // // ... 90 | // } 91 | // ``` 92 | export const canInsert = ($pos: ResolvedPos, content: Content): boolean => { 93 | const index = $pos.index(); 94 | 95 | if (content instanceof Fragment) { 96 | return $pos.parent.canReplace(index, index, content); 97 | } else if (content instanceof PMNode) { 98 | return $pos.parent.canReplaceWith(index, index, content.type); 99 | } 100 | return false; 101 | }; 102 | 103 | // Checks if a given `node` is an empty paragraph 104 | export const isEmptyParagraph = (node: PMNode): boolean => { 105 | return !node || (node.type.name === 'paragraph' && node.nodeSize === 2); 106 | }; 107 | 108 | export const checkInvalidMovements = ( 109 | originIndex: number, 110 | targetIndex: number, 111 | targets: number[], 112 | type: unknown 113 | ): boolean => { 114 | const direction = originIndex > targetIndex ? -1 : 1; 115 | const errorMessage = `Target position is invalid, you can't move the ${type} ${originIndex} to ${targetIndex}, the target can't be split. You could use tryToFit option.`; 116 | 117 | if (direction === 1) { 118 | if (targets.slice(0, targets.length - 1).indexOf(targetIndex) !== -1) { 119 | throw new Error(errorMessage); 120 | } 121 | } else { 122 | if (targets.slice(1).indexOf(targetIndex) !== -1) { 123 | throw new Error(errorMessage); 124 | } 125 | } 126 | 127 | return true; 128 | }; 129 | -------------------------------------------------------------------------------- /src/node.ts: -------------------------------------------------------------------------------- 1 | import { type Node as PMNode, MarkType, NodeType } from 'prosemirror-model'; 2 | import type { Attrs } from './types'; 3 | 4 | type FindChildrenAttrsPredicate = (attrs: Attrs) => boolean; 5 | type FindNodesResult = Array<{ node: PMNode; pos: number }>; 6 | type FindChildrenPredicate = (node: PMNode) => boolean; 7 | 8 | // Flattens descendants of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`). 9 | // 10 | // ```javascript 11 | // const children = flatten(node); 12 | // ``` 13 | export const flatten = ( 14 | node: PMNode, 15 | descend: boolean = true 16 | ): FindNodesResult => { 17 | if (!node) { 18 | throw new Error('Invalid "node" parameter'); 19 | } 20 | const result: FindNodesResult = []; 21 | node.descendants((child, pos) => { 22 | result.push({ node: child, pos }); 23 | if (!descend) { 24 | return false; 25 | } 26 | }); 27 | return result; 28 | }; 29 | 30 | // Iterates over descendants of a given `node`, returning child nodes predicate returns truthy for. It doesn't descend into a node when descend argument is `false` (defaults to `true`). 31 | // 32 | // ```javascript 33 | // const textNodes = findChildren(node, child => child.isText, false); 34 | // ``` 35 | export const findChildren = ( 36 | node: PMNode, 37 | predicate: FindChildrenPredicate, 38 | descend: boolean = true 39 | ): FindNodesResult => { 40 | if (!node) { 41 | throw new Error('Invalid "node" parameter'); 42 | } else if (!predicate) { 43 | throw new Error('Invalid "predicate" parameter'); 44 | } 45 | return flatten(node, descend).filter((child) => predicate(child.node)); 46 | }; 47 | 48 | // Returns text nodes of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`). 49 | // 50 | // ```javascript 51 | // const textNodes = findTextNodes(node); 52 | // ``` 53 | export const findTextNodes = ( 54 | node: PMNode, 55 | descend: boolean = true 56 | ): FindNodesResult => { 57 | return findChildren(node, (child) => child.isText, descend); 58 | }; 59 | 60 | // Returns inline nodes of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`). 61 | // 62 | // ```javascript 63 | // const inlineNodes = findInlineNodes(node); 64 | // ``` 65 | export const findInlineNodes = ( 66 | node: PMNode, 67 | descend: boolean = true 68 | ): FindNodesResult => { 69 | return findChildren(node, (child) => child.isInline, descend); 70 | }; 71 | 72 | // Returns block descendants of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`). 73 | // 74 | // ```javascript 75 | // const blockNodes = findBlockNodes(node); 76 | // ``` 77 | export const findBlockNodes = ( 78 | node: PMNode, 79 | descend: boolean = true 80 | ): FindNodesResult => { 81 | return findChildren(node, (child) => child.isBlock, descend); 82 | }; 83 | 84 | // Iterates over descendants of a given `node`, returning child nodes predicate returns truthy for. It doesn't descend into a node when descend argument is `false` (defaults to `true`). 85 | // 86 | // ```javascript 87 | // const mergedCells = findChildrenByAttr(table, attrs => attrs.colspan === 2); 88 | // ``` 89 | export const findChildrenByAttr = ( 90 | node: PMNode, 91 | predicate: FindChildrenAttrsPredicate, 92 | descend: boolean = true 93 | ): FindNodesResult => { 94 | return findChildren(node, (child) => !!predicate(child.attrs), descend); 95 | }; 96 | 97 | // Iterates over descendants of a given `node`, returning child nodes of a given nodeType. It doesn't descend into a node when descend argument is `false` (defaults to `true`). 98 | // 99 | // ```javascript 100 | // const cells = findChildrenByType(table, schema.nodes.tableCell); 101 | // ``` 102 | export const findChildrenByType = ( 103 | node: PMNode, 104 | nodeType: NodeType, 105 | descend: boolean = true 106 | ): FindNodesResult => { 107 | return findChildren(node, (child) => child.type === nodeType, descend); 108 | }; 109 | 110 | // Iterates over descendants of a given `node`, returning child nodes that have a mark of a given markType. It doesn't descend into a `node` when descend argument is `false` (defaults to `true`). 111 | // 112 | // ```javascript 113 | // const nodes = findChildrenByMark(state.doc, schema.marks.strong); 114 | // ``` 115 | export const findChildrenByMark = ( 116 | node: PMNode, 117 | markType: MarkType, 118 | descend: boolean = true 119 | ): FindNodesResult => { 120 | return findChildren( 121 | node, 122 | (child) => Boolean(markType.isInSet(child.marks)), 123 | descend 124 | ); 125 | }; 126 | 127 | // Returns `true` if a given node contains nodes of a given `nodeType` 128 | // 129 | // ```javascript 130 | // if (contains(panel, schema.nodes.listItem)) { 131 | // // ... 132 | // } 133 | // ``` 134 | export const contains = (node: PMNode, nodeType: NodeType): boolean => { 135 | return !!findChildrenByType(node, nodeType).length; 136 | }; 137 | -------------------------------------------------------------------------------- /__tests__/node.ts: -------------------------------------------------------------------------------- 1 | import { createEditor, testHelpers } from '../test-helpers'; 2 | import { 3 | flatten, 4 | findChildren, 5 | findTextNodes, 6 | findBlockNodes, 7 | findChildrenByAttr, 8 | findChildrenByType, 9 | findChildrenByMark, 10 | contains, 11 | } from '../src'; 12 | const { blockquote, code, doc, p, atomInline, strong } = testHelpers; 13 | 14 | describe('node', () => { 15 | describe('flatten', () => { 16 | it('should throw an error if `node` param is missing', () => { 17 | expect(flatten).toThrow(); 18 | }); 19 | describe('when `descend` param = `false`', () => { 20 | it('should flatten a given node a single level deep', () => { 21 | const { state } = createEditor( 22 | doc(blockquote(blockquote(p()), blockquote(p()), blockquote(p()))) 23 | ); 24 | const result = flatten(state.doc.firstChild!, false); 25 | expect(result.length).toEqual(3); 26 | result.forEach((item) => { 27 | expect(Object.keys(item)).toEqual(['node', 'pos']); 28 | expect(typeof item.pos).toEqual('number'); 29 | expect(item.node.type.name).toEqual('blockquote'); 30 | }); 31 | }); 32 | }); 33 | describe('when `descend` param is missing (defaults to `true`)', () => { 34 | it('should deep flatten a given node', () => { 35 | const { state } = createEditor( 36 | doc( 37 | blockquote( 38 | blockquote(blockquote(p())), 39 | blockquote(blockquote(p())), 40 | blockquote(blockquote(p())) 41 | ) 42 | ) 43 | ); 44 | const result = flatten(state.doc.firstChild!); 45 | expect(result.length).toEqual(9); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('findChildren', () => { 51 | it('should return an array of matched nodes `predicate` returns truthy for', () => { 52 | const { state } = createEditor( 53 | doc(blockquote(p(), p(), blockquote(p()), code())) 54 | ); 55 | const result = findChildren( 56 | state.doc.firstChild!, 57 | (node) => node.type === state.schema.nodes.paragraph 58 | ); 59 | expect(result.length).toEqual(3); 60 | result.forEach((item) => { 61 | expect(item.node.type.name).toEqual('paragraph'); 62 | }); 63 | }); 64 | it('should return an empty array if `predicate` returns falthy', () => { 65 | const { state } = createEditor(doc(blockquote(p()))); 66 | const result = findChildren( 67 | state.doc.firstChild!, 68 | (node) => node.type === state.schema.nodes.atomInline 69 | ); 70 | expect(result.length).toEqual(0); 71 | }); 72 | }); 73 | 74 | describe('findTextNodes', () => { 75 | it('should return an empty array if a given node does not have text nodes', () => { 76 | const { state } = createEditor(doc(blockquote(p()))); 77 | const result = findTextNodes(state.doc.firstChild!); 78 | expect(result.length).toEqual(0); 79 | }); 80 | it('should return an array if text nodes of a given node', () => { 81 | const { state } = createEditor( 82 | doc(blockquote(p('one', atomInline(), 'two'), p('three'))) 83 | ); 84 | const result = findTextNodes(state.doc.firstChild!); 85 | expect(result.length).toEqual(3); 86 | result.forEach((item) => { 87 | expect(item.node.isText).toBe(true); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('findBlockNodes', () => { 93 | it('should return an empty array if a given node does not have block nodes', () => { 94 | const { state } = createEditor(doc(p(''))); 95 | const result = findBlockNodes(state.doc.firstChild!); 96 | expect(result.length).toEqual(0); 97 | }); 98 | it('should return an array if block nodes of a given node', () => { 99 | const { state } = createEditor(doc(blockquote(p(), p()))); 100 | const result = findBlockNodes(state.doc); 101 | expect(result.length).toEqual(3); 102 | result.forEach((item) => { 103 | expect(item.node.isBlock).toBe(true); 104 | }); 105 | }); 106 | }); 107 | 108 | describe('findChildrenByAttr', () => { 109 | it('should return an empty array if a given node does not have nodes with the given attribute', () => { 110 | const { state } = createEditor(doc(p(''))); 111 | const result = findChildrenByAttr( 112 | state.doc.firstChild!, 113 | (attrs) => attrs && attrs.colspan === 2 114 | ); 115 | expect(result.length).toEqual(0); 116 | }); 117 | it('should return an array if child nodes with the given attribute', () => { 118 | const { state } = createEditor( 119 | doc( 120 | blockquote( 121 | p(), 122 | p(atomInline({ color: 'red' })), 123 | p(atomInline({ color: 'green' })), 124 | p('3') 125 | ), 126 | blockquote(p(atomInline({ color: 'red' })), p('2'), p(), p()) 127 | ) 128 | ); 129 | const result = findChildrenByAttr( 130 | state.doc, 131 | (attrs) => attrs.color === 'red' 132 | ); 133 | expect(result.length).toEqual(2); 134 | result.forEach((item) => { 135 | expect(item.node.attrs.color).toEqual('red'); 136 | }); 137 | }); 138 | }); 139 | 140 | describe('findChildrenByType', () => { 141 | it('should return an empty array if a given node does not have nodes of a given `nodeType`', () => { 142 | const { state } = createEditor(doc(p(''))); 143 | const result = findChildrenByType( 144 | state.doc, 145 | state.schema.nodes.blockquote 146 | ); 147 | expect(result.length).toEqual(0); 148 | }); 149 | it('should return an array if child nodes of a given `nodeType`', () => { 150 | const { state } = createEditor(doc(blockquote(p(''), p(''), p('')))); 151 | const result = findChildrenByType( 152 | state.doc, 153 | state.schema.nodes.paragraph 154 | ); 155 | expect(result.length).toEqual(3); 156 | result.forEach((item) => { 157 | expect(item.node.type.name).toEqual('paragraph'); 158 | }); 159 | }); 160 | }); 161 | 162 | describe('findChildrenByMark', () => { 163 | it('should return an empty array if a given node does not have child nodes with the given mark', () => { 164 | const { state } = createEditor(doc(p(''))); 165 | const result = findChildrenByMark(state.doc, state.schema.marks.strong); 166 | expect(result.length).toEqual(0); 167 | }); 168 | it('should return an array if child nodes if a given node has child nodes with the given mark', () => { 169 | const { state } = createEditor( 170 | doc( 171 | blockquote(p(strong('one'), 'two')), 172 | blockquote(p('three', strong('four'))) 173 | ) 174 | ); 175 | const result = findChildrenByMark(state.doc, state.schema.marks.strong); 176 | expect(result.length).toEqual(2); 177 | result.forEach((item) => { 178 | expect(item.node.marks[0].type.name).toEqual('strong'); 179 | }); 180 | }); 181 | }); 182 | 183 | describe('contains', () => { 184 | it('should return `false` if a given `node` does not contain nodes of a given `nodeType`', () => { 185 | const { state } = createEditor(doc(p(''))); 186 | const result = contains(state.doc, state.schema.nodes.blockquote); 187 | expect(result).toBe(false); 188 | }); 189 | it('should return `true` if a given `node` contains nodes of a given `nodeType`', () => { 190 | const { state } = createEditor(doc(p(''))); 191 | const result = contains(state.doc, state.schema.nodes.paragraph); 192 | expect(result).toBe(true); 193 | }); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /src/selection.ts: -------------------------------------------------------------------------------- 1 | import type { Node as PMNode, ResolvedPos } from 'prosemirror-model'; 2 | import { Selection } from 'prosemirror-state'; 3 | import type { 4 | FindPredicate, 5 | FindResult, 6 | DomAtPos, 7 | NodeTypeParam, 8 | } from './types'; 9 | import { equalNodeType, isNodeSelection } from './helpers'; 10 | 11 | // Iterates over parent nodes, returning the closest node and its start position `predicate` returns truthy for. `start` points to the start position of the node, `pos` points directly before the node. 12 | // 13 | // ```javascript 14 | // const predicate = node => node.type === schema.nodes.blockquote; 15 | // const parent = findParentNode(predicate)(selection); 16 | // ``` 17 | export const findParentNode = 18 | (predicate: FindPredicate) => 19 | ( 20 | { $from, $to }: Selection, 21 | validateSameParent: boolean = false 22 | ): FindResult => { 23 | // Check if parent are different 24 | if (validateSameParent && !$from.sameParent($to)) { 25 | // If they are, I need to find a common parent 26 | let depth = Math.min($from.depth, $to.depth); 27 | while (depth >= 0) { 28 | const fromNode = $from.node(depth); 29 | const toNode = $to.node(depth); 30 | if (toNode === fromNode) { 31 | // The have the same parent 32 | if (predicate(fromNode)) { 33 | // Check the predicate 34 | return { 35 | // Return the resolved pos 36 | pos: depth > 0 ? $from.before(depth) : 0, 37 | start: $from.start(depth), 38 | depth: depth, 39 | node: fromNode, 40 | }; 41 | } 42 | } 43 | depth = depth - 1; // Keep looking 44 | } 45 | return; 46 | } 47 | 48 | return findParentNodeClosestToPos($from, predicate); 49 | }; 50 | 51 | // Iterates over parent nodes starting from the given `$pos`, returning the closest node and its start position `predicate` returns truthy for. `start` points to the start position of the node, `pos` points directly before the node. 52 | // 53 | // ```javascript 54 | // const predicate = node => node.type === schema.nodes.blockquote; 55 | // const parent = findParentNodeClosestToPos(state.doc.resolve(5), predicate); 56 | // ``` 57 | export const findParentNodeClosestToPos = ( 58 | $pos: ResolvedPos, 59 | predicate: FindPredicate 60 | ): FindResult => { 61 | for (let i = $pos.depth; i > 0; i--) { 62 | const node = $pos.node(i); 63 | if (predicate(node)) { 64 | return { 65 | pos: i > 0 ? $pos.before(i) : 0, 66 | start: $pos.start(i), 67 | depth: i, 68 | node, 69 | }; 70 | } 71 | } 72 | }; 73 | 74 | // Iterates over parent nodes, returning DOM reference of the closest node `predicate` returns truthy for. 75 | // 76 | // ```javascript 77 | // const domAtPos = view.domAtPos.bind(view); 78 | // const predicate = node => node.type === schema.nodes.table; 79 | // const parent = findParentDomRef(predicate, domAtPos)(selection); // 80 | // ``` 81 | export const findParentDomRef = 82 | (predicate: FindPredicate, domAtPos: DomAtPos) => 83 | (selection: Selection): Node | undefined => { 84 | const parent = findParentNode(predicate)(selection); 85 | if (parent) { 86 | return findDomRefAtPos(parent.pos, domAtPos); 87 | } 88 | }; 89 | 90 | // Checks if there's a parent node `predicate` returns truthy for. 91 | // 92 | // ```javascript 93 | // if (hasParentNode(node => node.type === schema.nodes.table)(selection)) { 94 | // // .... 95 | // } 96 | // ``` 97 | export const hasParentNode = 98 | (predicate: FindPredicate) => 99 | (selection: Selection): boolean => { 100 | return !!findParentNode(predicate)(selection); 101 | }; 102 | 103 | // Iterates over parent nodes, returning closest node of a given `nodeType`. `start` points to the start position of the node, `pos` points directly before the node. 104 | // 105 | // ```javascript 106 | // const parent = findParentNodeOfType(schema.nodes.paragraph)(selection); 107 | // ``` 108 | export const findParentNodeOfType = 109 | (nodeType: NodeTypeParam) => 110 | (selection: Selection): FindResult => { 111 | return findParentNode((node) => equalNodeType(nodeType, node))(selection); 112 | }; 113 | 114 | // Iterates over parent nodes starting from the given `$pos`, returning closest node of a given `nodeType`. `start` points to the start position of the node, `pos` points directly before the node. 115 | // 116 | // ```javascript 117 | // const parent = findParentNodeOfTypeClosestToPos(state.doc.resolve(10), schema.nodes.paragraph); 118 | // ``` 119 | export const findParentNodeOfTypeClosestToPos = ( 120 | $pos: ResolvedPos, 121 | nodeType: NodeTypeParam 122 | ): FindResult => { 123 | return findParentNodeClosestToPos($pos, (node: PMNode) => 124 | equalNodeType(nodeType, node) 125 | ); 126 | }; 127 | 128 | // Checks if there's a parent node of a given `nodeType`. 129 | // 130 | // ```javascript 131 | // if (hasParentNodeOfType(schema.nodes.table)(selection)) { 132 | // // .... 133 | // } 134 | // ``` 135 | export const hasParentNodeOfType = 136 | (nodeType: NodeTypeParam) => 137 | (selection: Selection): boolean => { 138 | return hasParentNode((node) => equalNodeType(nodeType, node))(selection); 139 | }; 140 | 141 | // Iterates over parent nodes, returning DOM reference of the closest node of a given `nodeType`. 142 | // 143 | // ```javascript 144 | // const domAtPos = view.domAtPos.bind(view); 145 | // const parent = findParentDomRefOfType(schema.nodes.codeBlock, domAtPos)(selection); //
146 | // ```
147 | export const findParentDomRefOfType =
148 |   (nodeType: NodeTypeParam, domAtPos: DomAtPos) =>
149 |   (selection: Selection): Node | undefined => {
150 |     return findParentDomRef(
151 |       (node) => equalNodeType(nodeType, node),
152 |       domAtPos
153 |     )(selection);
154 |   };
155 | 
156 | // Returns a node of a given `nodeType` if it is selected. `start` points to the start position of the node, `pos` points directly before the node.
157 | //
158 | // ```javascript
159 | // const { extension, inlineExtension, bodiedExtension } = schema.nodes;
160 | // const selectedNode = findSelectedNodeOfType([
161 | //   extension,
162 | //   inlineExtension,
163 | //   bodiedExtension,
164 | // ])(selection);
165 | // ```
166 | export const findSelectedNodeOfType =
167 |   (nodeType: NodeTypeParam) =>
168 |   (selection: Selection): FindResult => {
169 |     if (isNodeSelection(selection)) {
170 |       const { node, $from } = selection;
171 |       if (equalNodeType(nodeType, node)) {
172 |         return {
173 |           node,
174 |           start: $from.start(),
175 |           pos: $from.pos,
176 |           depth: $from.depth,
177 |         };
178 |       }
179 |     }
180 |   };
181 | 
182 | // Returns position of the previous node.
183 | //
184 | // ```javascript
185 | // const pos = findPositionOfNodeBefore(tr.selection);
186 | // ```
187 | export const findPositionOfNodeBefore = (
188 |   selection: Selection
189 | ): number | undefined => {
190 |   const { nodeBefore } = selection.$from;
191 |   const maybeSelection = Selection.findFrom(selection.$from, -1);
192 |   if (maybeSelection && nodeBefore) {
193 |     // leaf node
194 |     const parent = findParentNodeOfType(nodeBefore.type)(maybeSelection);
195 |     if (parent) {
196 |       return parent.pos;
197 |     }
198 |     return maybeSelection.$from.pos;
199 |   }
200 | };
201 | 
202 | // Returns DOM reference of a node at a given `position`. If the node type is of type `TEXT_NODE` it will return the reference of the parent node.
203 | //
204 | // ```javascript
205 | // const domAtPos = view.domAtPos.bind(view);
206 | // const ref = findDomRefAtPos($from.pos, domAtPos);
207 | // ```
208 | export const findDomRefAtPos = (position: number, domAtPos: DomAtPos): Node => {
209 |   const dom = domAtPos(position);
210 |   const node = dom.node.childNodes[dom.offset];
211 | 
212 |   if (dom.node.nodeType === Node.TEXT_NODE && dom.node.parentNode) {
213 |     return dom.node.parentNode;
214 |   }
215 | 
216 |   if (!node || node.nodeType === Node.TEXT_NODE) {
217 |     return dom.node;
218 |   }
219 | 
220 |   return node;
221 | };
222 | 


--------------------------------------------------------------------------------
/src/transforms.ts:
--------------------------------------------------------------------------------
  1 | import { NodeSelection, Selection, type Transaction } from 'prosemirror-state';
  2 | import {
  3 |   Fragment,
  4 |   Node as PMNode,
  5 |   type NodeType,
  6 |   Mark,
  7 | } from 'prosemirror-model';
  8 | import { findParentNodeOfType, findPositionOfNodeBefore } from './selection';
  9 | import {
 10 |   cloneTr,
 11 |   isNodeSelection,
 12 |   replaceNodeAtPos,
 13 |   removeNodeAtPos,
 14 |   canInsert,
 15 |   isEmptyParagraph,
 16 | } from './helpers';
 17 | import type { Attrs, NodeTypeParam, Content } from './types';
 18 | 
 19 | // Returns a new transaction that removes a node of a given `nodeType`. It will return an original transaction if parent node hasn't been found.
 20 | //
 21 | // ```javascript
 22 | // dispatch(
 23 | //   removeParentNodeOfType(schema.nodes.table)(tr)
 24 | // );
 25 | // ```
 26 | export const removeParentNodeOfType =
 27 |   (nodeType: NodeTypeParam) =>
 28 |   (tr: Transaction): Transaction => {
 29 |     const parent = findParentNodeOfType(nodeType)(tr.selection);
 30 |     if (parent) {
 31 |       return removeNodeAtPos(parent.pos)(tr);
 32 |     }
 33 |     return tr;
 34 |   };
 35 | 
 36 | // Returns a new transaction that replaces parent node of a given `nodeType` with the given `content`. It will return an original transaction if either parent node hasn't been found or replacing is not possible.
 37 | //
 38 | // ```javascript
 39 | // const node = schema.nodes.paragraph.createChecked({}, schema.text('new'));
 40 | //
 41 | // dispatch(
 42 | //  replaceParentNodeOfType(schema.nodes.table, node)(tr)
 43 | // );
 44 | // ```
 45 | export const replaceParentNodeOfType =
 46 |   (nodeType: NodeTypeParam, content: Content) =>
 47 |   (tr: Transaction): Transaction => {
 48 |     if (!Array.isArray(nodeType)) {
 49 |       nodeType = [nodeType];
 50 |     }
 51 |     for (let i = 0, count = nodeType.length; i < count; i++) {
 52 |       const parent = findParentNodeOfType(nodeType[i])(tr.selection);
 53 |       if (parent) {
 54 |         const newTr = replaceNodeAtPos(parent.pos, content)(tr);
 55 |         if (newTr !== tr) {
 56 |           return newTr;
 57 |         }
 58 |       }
 59 |     }
 60 |     return tr;
 61 |   };
 62 | 
 63 | // Returns a new transaction that removes selected node. It will return an original transaction if current selection is not a `NodeSelection`.
 64 | //
 65 | // ```javascript
 66 | // dispatch(
 67 | //   removeSelectedNode(tr)
 68 | // );
 69 | // ```
 70 | export const removeSelectedNode = (tr: Transaction): Transaction => {
 71 |   if (isNodeSelection(tr.selection)) {
 72 |     const from = tr.selection.$from.pos;
 73 |     const to = tr.selection.$to.pos;
 74 |     return cloneTr(tr.delete(from, to));
 75 |   }
 76 |   return tr;
 77 | };
 78 | 
 79 | // Returns a new transaction that replaces selected node with a given `node`, keeping NodeSelection on the new `node`.
 80 | // It will return the original transaction if either current selection is not a NodeSelection or replacing is not possible.
 81 | //
 82 | // ```javascript
 83 | // const node = schema.nodes.paragraph.createChecked({}, schema.text('new'));
 84 | // dispatch(
 85 | //   replaceSelectedNode(node)(tr)
 86 | // );
 87 | // ```
 88 | export const replaceSelectedNode =
 89 |   (content: Content) =>
 90 |   (tr: Transaction): Transaction => {
 91 |     if (isNodeSelection(tr.selection)) {
 92 |       const { $from, $to } = tr.selection;
 93 |       if (
 94 |         (content instanceof Fragment &&
 95 |           $from.parent.canReplace(
 96 |             $from.index(),
 97 |             $from.indexAfter(),
 98 |             content
 99 |           )) ||
100 |         (content instanceof PMNode &&
101 |           $from.parent.canReplaceWith(
102 |             $from.index(),
103 |             $from.indexAfter(),
104 |             content.type
105 |           ))
106 |       ) {
107 |         return cloneTr(
108 |           tr
109 |             .replaceWith($from.pos, $to.pos, content)
110 |             // restore node selection
111 |             .setSelection(new NodeSelection(tr.doc.resolve($from.pos)))
112 |         );
113 |       }
114 |     }
115 |     return tr;
116 |   };
117 | 
118 | // Returns a new transaction that tries to find a valid cursor selection starting at the given `position`
119 | // and searching back if `dir` is negative, and forward if positive.
120 | // If a valid cursor position hasn't been found, it will return the original transaction.
121 | //
122 | // ```javascript
123 | // dispatch(
124 | //   setTextSelection(5)(tr)
125 | // );
126 | // ```
127 | export const setTextSelection =
128 |   (position: number, dir = 1) =>
129 |   (tr: Transaction): Transaction => {
130 |     const nextSelection = Selection.findFrom(
131 |       tr.doc.resolve(position),
132 |       dir,
133 |       true
134 |     );
135 |     if (nextSelection) {
136 |       return tr.setSelection(nextSelection);
137 |     }
138 |     return tr;
139 |   };
140 | 
141 | const isSelectableNode = (node: Content): node is PMNode =>
142 |   Boolean(node instanceof PMNode && node.type && node.type.spec.selectable);
143 | const shouldSelectNode = (node: Content): boolean =>
144 |   isSelectableNode(node) && node.type.isLeaf;
145 | 
146 | const setSelection = (
147 |   node: Content,
148 |   pos: number,
149 |   tr: Transaction
150 | ): Transaction => {
151 |   if (shouldSelectNode(node)) {
152 |     return tr.setSelection(new NodeSelection(tr.doc.resolve(pos)));
153 |   }
154 |   return setTextSelection(pos)(tr);
155 | };
156 | 
157 | // Returns a new transaction that inserts a given `content` at the current cursor position, or at a given `position`, if it is allowed by schema. If schema restricts such nesting, it will try to find an appropriate place for a given node in the document, looping through parent nodes up until the root document node.
158 | // If `tryToReplace` is true and current selection is a NodeSelection, it will replace selected node with inserted content if its allowed by schema.
159 | // If cursor is inside of an empty paragraph, it will try to replace that paragraph with the given content. If insertion is successful and inserted node has content, it will set cursor inside of that content.
160 | // It will return an original transaction if the place for insertion hasn't been found.
161 | //
162 | // ```javascript
163 | // const node = schema.nodes.extension.createChecked({});
164 | // dispatch(
165 | //   safeInsert(node)(tr)
166 | // );
167 | // ```
168 | export const safeInsert =
169 |   (content: Content, position?: number, tryToReplace?: boolean) =>
170 |   (tr: Transaction): Transaction => {
171 |     const hasPosition = typeof position === 'number';
172 |     const { $from } = tr.selection;
173 |     const $insertPos = hasPosition
174 |       ? tr.doc.resolve(position)
175 |       : isNodeSelection(tr.selection)
176 |       ? tr.doc.resolve($from.pos + 1)
177 |       : $from;
178 |     const { parent } = $insertPos;
179 | 
180 |     // try to replace selected node
181 |     if (isNodeSelection(tr.selection) && tryToReplace) {
182 |       const oldTr = tr;
183 |       tr = replaceSelectedNode(content)(tr);
184 |       if (oldTr !== tr) {
185 |         return tr;
186 |       }
187 |     }
188 | 
189 |     // try to replace an empty paragraph
190 |     if (isEmptyParagraph(parent)) {
191 |       const oldTr = tr;
192 |       tr = replaceParentNodeOfType(parent.type, content)(tr);
193 |       if (oldTr !== tr) {
194 |         const pos = isSelectableNode(content)
195 |           ? // for selectable node, selection position would be the position of the replaced parent
196 |             $insertPos.before($insertPos.depth)
197 |           : $insertPos.pos;
198 |         return setSelection(content, pos, tr);
199 |       }
200 |     }
201 | 
202 |     // given node is allowed at the current cursor position
203 |     if (canInsert($insertPos, content)) {
204 |       tr.insert($insertPos.pos, content);
205 |       const pos = hasPosition
206 |         ? $insertPos.pos
207 |         : isSelectableNode(content)
208 |         ? // for atom nodes selection position after insertion is the previous pos
209 |           tr.selection.$anchor.pos - 1
210 |         : tr.selection.$anchor.pos;
211 |       return cloneTr(setSelection(content, pos, tr));
212 |     }
213 | 
214 |     // looking for a place in the doc where the node is allowed
215 |     for (let i = $insertPos.depth; i > 0; i--) {
216 |       const pos = $insertPos.after(i);
217 |       const $pos = tr.doc.resolve(pos);
218 |       if (canInsert($pos, content)) {
219 |         tr.insert(pos, content);
220 |         return cloneTr(setSelection(content, pos, tr));
221 |       }
222 |     }
223 |     return tr;
224 |   };
225 | 
226 | // Returns a transaction that changes the type, attributes, and/or marks of the parent node of a given `nodeType`.
227 | //
228 | // ```javascript
229 | // const node = schema.nodes.extension.createChecked({});
230 | // dispatch(
231 | //   setParentNodeMarkup(schema.nodes.panel, null, { panelType })(tr);
232 | // );
233 | // ```
234 | export const setParentNodeMarkup =
235 |   (
236 |     nodeType: NodeTypeParam,
237 |     type: NodeType | null,
238 |     attrs?: Attrs | null,
239 |     marks?: Array | ReadonlyArray
240 |   ) =>
241 |   (tr: Transaction): Transaction => {
242 |     const parent = findParentNodeOfType(nodeType)(tr.selection);
243 |     if (parent) {
244 |       return cloneTr(
245 |         tr.setNodeMarkup(
246 |           parent.pos,
247 |           type,
248 |           Object.assign({}, parent.node.attrs, attrs),
249 |           marks
250 |         )
251 |       );
252 |     }
253 |     return tr;
254 |   };
255 | 
256 | // Returns a new transaction that sets a `NodeSelection` on a parent node of a `given nodeType`.
257 | //
258 | // ```javascript
259 | // dispatch(
260 | //   selectParentNodeOfType([tableCell, tableHeader])(state.tr)
261 | // );
262 | // ```
263 | export const selectParentNodeOfType =
264 |   (nodeType: NodeTypeParam) =>
265 |   (tr: Transaction): Transaction => {
266 |     if (!isNodeSelection(tr.selection)) {
267 |       const parent = findParentNodeOfType(nodeType)(tr.selection);
268 |       if (parent) {
269 |         return cloneTr(
270 |           tr.setSelection(NodeSelection.create(tr.doc, parent.pos))
271 |         );
272 |       }
273 |     }
274 |     return tr;
275 |   };
276 | 
277 | // Returns a new transaction that deletes previous node.
278 | //
279 | // ```javascript
280 | // dispatch(
281 | //   removeNodeBefore(state.tr)
282 | // );
283 | // ```
284 | export const removeNodeBefore = (tr: Transaction): Transaction => {
285 |   const position = findPositionOfNodeBefore(tr.selection);
286 |   if (typeof position === 'number') {
287 |     return removeNodeAtPos(position)(tr);
288 |   }
289 |   return tr;
290 | };
291 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # Utils library for ProseMirror
  2 | 
  3 | [![npm](https://img.shields.io/npm/v/prosemirror-utils.svg?style=flat-square)](https://www.npmjs.com/package/prosemirror-utils)
  4 | [![License](https://img.shields.io/npm/l/prosemirror-utils.svg?style=flat-square)](http://www.apache.org/licenses/LICENSE-2.0)
  5 | [![Github Issues](https://img.shields.io/github/issues/atlassian/prosemirror-utils.svg?style=flat-square)](https://github.com/atlassian/prosemirror-utils/issues)
  6 | [![CircleCI](https://img.shields.io/circleci/project/github/atlassian/prosemirror-utils.svg?style=flat-square)](https://circleci.com/gh/atlassian/prosemirror-utils)
  7 | [![codecov](https://codecov.io/gh/atlassian/prosemirror-utils/branch/master/graph/badge.svg)](https://codecov.io/gh/atlassian/prosemirror-utils)
  8 | [![Downloads](https://img.shields.io/npm/dw/prosemirror-utils.svg?style=flat-square)](https://www.npmjs.com/package/prosemirror-utils)
  9 | [![Code size](https://img.shields.io/github/languages/code-size/atlassian/prosemirror-utils.svg?style=flat-square)](https://www.npmjs.com/package/prosemirror-utils)
 10 | 
 11 | 
 12 | ## How to
 13 | 
 14 | ### Test
 15 | 
 16 | ```sh
 17 | npm run test
 18 | ```
 19 | 
 20 | ### Build
 21 | 
 22 | ```sh
 23 | npm run build_all
 24 | ```
 25 | 
 26 | ## Quick Start
 27 | 
 28 | Install `prosemirror-utils` package from npm:
 29 | 
 30 | ```sh
 31 | npm install prosemirror-utils
 32 | ```
 33 | 
 34 | ## Public API documentation
 35 | 
 36 | ### Utils for working with `selection`
 37 | 
 38 |  * **`findParentNode`**`(predicate: fn(node: ProseMirrorNode) → boolean) → fn(selection: Selection) → ?{pos: number, start: number, depth: number, node: ProseMirrorNode}`\
 39 |    Iterates over parent nodes, returning the closest node and its start position `predicate` returns truthy for. `start` points to the start position of the node, `pos` points directly before the node.
 40 | 
 41 |    ```javascript
 42 |    const predicate = node => node.type === schema.nodes.blockquote;
 43 |    const parent = findParentNode(predicate)(selection);
 44 |    ```
 45 | 
 46 | 
 47 |  * **`findParentNodeClosestToPos`**`($pos: ResolvedPos, predicate: fn(node: ProseMirrorNode) → boolean) → ?{pos: number, start: number, depth: number, node: ProseMirrorNode}`\
 48 |    Iterates over parent nodes starting from the given `$pos`, returning the closest node and its start position `predicate` returns truthy for. `start` points to the start position of the node, `pos` points directly before the node.
 49 | 
 50 |    ```javascript
 51 |    const predicate = node => node.type === schema.nodes.blockquote;
 52 |    const parent = findParentNodeClosestToPos(state.doc.resolve(5), predicate);
 53 |    ```
 54 | 
 55 | 
 56 |  * **`findParentDomRef`**`(predicate: fn(node: ProseMirrorNode) → boolean, domAtPos: fn(pos: number) → {node: dom.Node, offset: number}) → fn(selection: Selection) → ?dom.Node`\
 57 |    Iterates over parent nodes, returning DOM reference of the closest node `predicate` returns truthy for.
 58 | 
 59 |    ```javascript
 60 |    const domAtPos = view.domAtPos.bind(view);
 61 |    const predicate = node => node.type === schema.nodes.table;
 62 |    const parent = findParentDomRef(predicate, domAtPos)(selection); // 
63 | ``` 64 | 65 | 66 | * **`hasParentNode`**`(predicate: fn(node: ProseMirrorNode) → boolean) → fn(selection: Selection) → boolean`\ 67 | Checks if there's a parent node `predicate` returns truthy for. 68 | 69 | ```javascript 70 | if (hasParentNode(node => node.type === schema.nodes.table)(selection)) { 71 | // .... 72 | } 73 | ``` 74 | 75 | 76 | * **`findParentNodeOfType`**`(nodeType: NodeType | [NodeType]) → fn(selection: Selection) → ?{pos: number, start: number, depth: number, node: ProseMirrorNode}`\ 77 | Iterates over parent nodes, returning closest node of a given `nodeType`. `start` points to the start position of the node, `pos` points directly before the node. 78 | 79 | ```javascript 80 | const parent = findParentNodeOfType(schema.nodes.paragraph)(selection); 81 | ``` 82 | 83 | 84 | * **`findParentNodeOfTypeClosestToPos`**`($pos: ResolvedPos, nodeType: NodeType | [NodeType]) → ?{pos: number, start: number, depth: number, node: ProseMirrorNode}`\ 85 | Iterates over parent nodes starting from the given `$pos`, returning closest node of a given `nodeType`. `start` points to the start position of the node, `pos` points directly before the node. 86 | 87 | ```javascript 88 | const parent = findParentNodeOfTypeClosestToPos(state.doc.resolve(10), schema.nodes.paragraph); 89 | ``` 90 | 91 | 92 | * **`hasParentNodeOfType`**`(nodeType: NodeType | [NodeType]) → fn(selection: Selection) → boolean`\ 93 | Checks if there's a parent node of a given `nodeType`. 94 | 95 | ```javascript 96 | if (hasParentNodeOfType(schema.nodes.table)(selection)) { 97 | // .... 98 | } 99 | ``` 100 | 101 | 102 | * **`findParentDomRefOfType`**`(nodeType: NodeType | [NodeType], domAtPos: fn(pos: number) → {node: dom.Node, offset: number}) → fn(selection: Selection) → ?dom.Node`\ 103 | Iterates over parent nodes, returning DOM reference of the closest node of a given `nodeType`. 104 | 105 | ```javascript 106 | const domAtPos = view.domAtPos.bind(view); 107 | const parent = findParentDomRefOfType(schema.nodes.codeBlock, domAtPos)(selection); //
108 |    ```
109 | 
110 | 
111 |  * **`findSelectedNodeOfType`**`(nodeType: NodeType | [NodeType]) → fn(selection: Selection) → ?{pos: number, start: number, depth: number, node: ProseMirrorNode}`\
112 |    Returns a node of a given `nodeType` if it is selected. `start` points to the start position of the node, `pos` points directly before the node.
113 | 
114 |    ```javascript
115 |    const { extension, inlineExtension, bodiedExtension } = schema.nodes;
116 |    const selectedNode = findSelectedNodeOfType([
117 |      extension,
118 |      inlineExtension,
119 |      bodiedExtension,
120 |    ])(selection);
121 |    ```
122 | 
123 | 
124 |  * **`isNodeSelection`**`(selection: Selection) → boolean`\
125 |    Checks if current selection is a `NodeSelection`.
126 | 
127 |    ```javascript
128 |    if (isNodeSelection(tr.selection)) {
129 |      // ...
130 |    }
131 |    ```
132 | 
133 | 
134 |  * **`findPositionOfNodeBefore`**`(selection: Selection) → ?number`\
135 |    Returns position of the previous node.
136 | 
137 |    ```javascript
138 |    const pos = findPositionOfNodeBefore(tr.selection);
139 |    ```
140 | 
141 | 
142 |  * **`findDomRefAtPos`**`(position: number, domAtPos: fn(pos: number) → {node: dom.Node, offset: number}) → dom.Node`\
143 |    Returns DOM reference of a node at a given `position`. If the node type is of type `TEXT_NODE` it will return the reference of the parent node.
144 | 
145 |    ```javascript
146 |    const domAtPos = view.domAtPos.bind(view);
147 |    const ref = findDomRefAtPos($from.pos, domAtPos);
148 |    ```
149 | 
150 | 
151 | ### Utils for working with ProseMirror `node`
152 | 
153 |  * **`flatten`**`(node: ProseMirrorNode, descend: ?boolean = true) → [{node: ProseMirrorNode, pos: number}]`\
154 |    Flattens descendants of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`).
155 | 
156 |    ```javascript
157 |    const children = flatten(node);
158 |    ```
159 | 
160 | 
161 |  * **`findChildren`**`(node: ProseMirrorNode, predicate: fn(node: ProseMirrorNode) → boolean, descend: ?boolean) → [{node: ProseMirrorNode, pos: number}]`\
162 |    Iterates over descendants of a given `node`, returning child nodes predicate returns truthy for. It doesn't descend into a node when descend argument is `false` (defaults to `true`).
163 | 
164 |    ```javascript
165 |    const textNodes = findChildren(node, child => child.isText, false);
166 |    ```
167 | 
168 | 
169 |  * **`findTextNodes`**`(node: ProseMirrorNode, descend: ?boolean) → [{node: ProseMirrorNode, pos: number}]`\
170 |    Returns text nodes of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`).
171 | 
172 |    ```javascript
173 |    const textNodes = findTextNodes(node);
174 |    ```
175 | 
176 | 
177 |  * **`findInlineNodes`**`(node: ProseMirrorNode, descend: ?boolean) → [{node: ProseMirrorNode, pos: number}]`\
178 |    Returns inline nodes of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`).
179 | 
180 |    ```javascript
181 |    const inlineNodes = findInlineNodes(node);
182 |    ```
183 | 
184 | 
185 |  * **`findBlockNodes`**`(node: ProseMirrorNode, descend: ?boolean) → [{node: ProseMirrorNode, pos: number}]`\
186 |    Returns block descendants of a given `node`. It doesn't descend into a node when descend argument is `false` (defaults to `true`).
187 | 
188 |    ```javascript
189 |    const blockNodes = findBlockNodes(node);
190 |    ```
191 | 
192 | 
193 |  * **`findChildrenByAttr`**`(node: ProseMirrorNode, predicate: fn(attrs: ?Object) → boolean, descend: ?boolean) → [{node: ProseMirrorNode, pos: number}]`\
194 |    Iterates over descendants of a given `node`, returning child nodes predicate returns truthy for. It doesn't descend into a node when descend argument is `false` (defaults to `true`).
195 | 
196 |    ```javascript
197 |    const mergedCells = findChildrenByAttr(table, attrs => attrs.colspan === 2);
198 |    ```
199 | 
200 | 
201 |  * **`findChildrenByType`**`(node: ProseMirrorNode, nodeType: NodeType, descend: ?boolean) → [{node: ProseMirrorNode, pos: number}]`\
202 |    Iterates over descendants of a given `node`, returning child nodes of a given nodeType. It doesn't descend into a node when descend argument is `false` (defaults to `true`).
203 | 
204 |    ```javascript
205 |    const cells = findChildrenByType(table, schema.nodes.tableCell);
206 |    ```
207 | 
208 | 
209 |  * **`findChildrenByMark`**`(node: ProseMirrorNode, markType: markType, descend: ?boolean) → [{node: ProseMirrorNode, pos: number}]`\
210 |    Iterates over descendants of a given `node`, returning child nodes that have a mark of a given markType. It doesn't descend into a `node` when descend argument is `false` (defaults to `true`).
211 | 
212 |    ```javascript
213 |    const nodes = findChildrenByMark(state.doc, schema.marks.strong);
214 |    ```
215 | 
216 | 
217 |  * **`contains`**`(node: ProseMirrorNode, nodeType: NodeType) → boolean`\
218 |    Returns `true` if a given node contains nodes of a given `nodeType`
219 | 
220 |    ```javascript
221 |    if (contains(panel, schema.nodes.listItem)) {
222 |      // ...
223 |    }
224 |    ```
225 | 
226 | 
227 | ### Utils for document transformation
228 | 
229 |  * **`removeParentNodeOfType`**`(nodeType: NodeType | [NodeType]) → fn(tr: Transaction) → Transaction`\
230 |    Returns a new transaction that removes a node of a given `nodeType`. It will return an original transaction if parent node hasn't been found.
231 | 
232 |    ```javascript
233 |    dispatch(
234 |      removeParentNodeOfType(schema.nodes.table)(tr)
235 |    );
236 |    ```
237 | 
238 | 
239 |  * **`replaceParentNodeOfType`**`(nodeType: NodeType | [NodeType], content: ProseMirrorNode | Fragment) → fn(tr: Transaction) → Transaction`\
240 |    Returns a new transaction that replaces parent node of a given `nodeType` with the given `content`. It will return an original transaction if either parent node hasn't been found or replacing is not possible.
241 | 
242 |    ```javascript
243 |    const node = schema.nodes.paragraph.createChecked({}, schema.text('new'));
244 | 
245 |    dispatch(
246 |     replaceParentNodeOfType(schema.nodes.table, node)(tr)
247 |    );
248 |    ```
249 | 
250 | 
251 |  * **`removeSelectedNode`**`(tr: Transaction) → Transaction`\
252 |    Returns a new transaction that removes selected node. It will return an original transaction if current selection is not a `NodeSelection`.
253 | 
254 |    ```javascript
255 |    dispatch(
256 |      removeSelectedNode(tr)
257 |    );
258 |    ```
259 | 
260 | 
261 |  * **`replaceSelectedNode`**`(content: ProseMirrorNode | ProseMirrorFragment) → fn(tr: Transaction) → Transaction`\
262 |    Returns a new transaction that replaces selected node with a given `node`, keeping NodeSelection on the new `node`.
263 |    It will return the original transaction if either current selection is not a NodeSelection or replacing is not possible.
264 | 
265 |    ```javascript
266 |    const node = schema.nodes.paragraph.createChecked({}, schema.text('new'));
267 |    dispatch(
268 |      replaceSelectedNode(node)(tr)
269 |    );
270 |    ```
271 | 
272 | 
273 |  * **`canInsert`**`($pos: ResolvedPos, content: ProseMirrorNode | Fragment) → boolean`\
274 |    Checks if a given `content` can be inserted at the given `$pos`
275 | 
276 |    ```javascript
277 |    const { selection: { $from } } = state;
278 |    const node = state.schema.nodes.atom.createChecked();
279 |    if (canInsert($from, node)) {
280 |      // ...
281 |    }
282 |    ```
283 | 
284 | 
285 |  * **`safeInsert`**`(content: ProseMirrorNode | Fragment, position: ?number, tryToReplace: ?boolean) → fn(tr: Transaction) → Transaction`\
286 |    Returns a new transaction that inserts a given `content` at the current cursor position, or at a given `position`, if it is allowed by schema. If schema restricts such nesting, it will try to find an appropriate place for a given node in the document, looping through parent nodes up until the root document node.
287 |    If `tryToReplace` is true and current selection is a NodeSelection, it will replace selected node with inserted content if its allowed by schema.
288 |    If cursor is inside of an empty paragraph, it will try to replace that paragraph with the given content. If insertion is successful and inserted node has content, it will set cursor inside of that content.
289 |    It will return an original transaction if the place for insertion hasn't been found.
290 | 
291 |    ```javascript
292 |    const node = schema.nodes.extension.createChecked({});
293 |    dispatch(
294 |      safeInsert(node)(tr)
295 |    );
296 |    ```
297 | 
298 | 
299 |  * **`setParentNodeMarkup`**`(nodeType: NodeType | [NodeType], type: ?NodeType | null, attrs: ?Object | null, marks: ?[Mark]) → fn(tr: Transaction) → Transaction`\
300 |    Returns a transaction that changes the type, attributes, and/or marks of the parent node of a given `nodeType`.
301 | 
302 |    ```javascript
303 |    const node = schema.nodes.extension.createChecked({});
304 |    dispatch(
305 |      setParentNodeMarkup(schema.nodes.panel, null, { panelType })(tr);
306 |    );
307 |    ```
308 | 
309 | 
310 |  * **`selectParentNodeOfType`**`(nodeType: NodeType | [NodeType]) → fn(tr: Transaction) → Transaction`\
311 |    Returns a new transaction that sets a `NodeSelection` on a parent node of a `given nodeType`.
312 | 
313 |    ```javascript
314 |    dispatch(
315 |      selectParentNodeOfType([tableCell, tableHeader])(state.tr)
316 |    );
317 |    ```
318 | 
319 | 
320 |  * **`removeNodeBefore`**`(tr: Transaction) → Transaction`\
321 |    Returns a new transaction that deletes previous node.
322 | 
323 |    ```javascript
324 |    dispatch(
325 |      removeNodeBefore(state.tr)
326 |    );
327 |    ```
328 | 
329 | 
330 |  * **`setTextSelection`**`(position: number, dir: ?number = 1) → fn(tr: Transaction) → Transaction`\
331 |    Returns a new transaction that tries to find a valid cursor selection starting at the given `position`
332 |    and searching back if `dir` is negative, and forward if positive.
333 |    If a valid cursor position hasn't been found, it will return the original transaction.
334 | 
335 |    ```javascript
336 |    dispatch(
337 |      setTextSelection(5)(tr)
338 |    );
339 |    ```
340 | 
341 | 
342 | ## License
343 | 
344 | - **Apache 2.0** : http://www.apache.org/licenses/LICENSE-2.0
345 | 
346 | 


--------------------------------------------------------------------------------
/__tests__/selection.ts:
--------------------------------------------------------------------------------
  1 | import { NodeSelection } from 'prosemirror-state';
  2 | import { createEditor, testHelpers } from '../test-helpers';
  3 | import {
  4 |   findParentNode,
  5 |   findParentDomRef,
  6 |   hasParentNode,
  7 |   findParentNodeOfType,
  8 |   hasParentNodeOfType,
  9 |   findParentDomRefOfType,
 10 |   findSelectedNodeOfType,
 11 |   findPositionOfNodeBefore,
 12 |   findDomRefAtPos,
 13 |   findParentNodeClosestToPos,
 14 |   findParentNodeOfTypeClosestToPos,
 15 | } from '../src';
 16 | 
 17 | const {
 18 |   doc,
 19 |   p,
 20 |   blockquote,
 21 |   atomContainer,
 22 |   atomInline,
 23 |   atomBlock,
 24 |   article,
 25 |   section,
 26 | } = testHelpers;
 27 | 
 28 | describe('selection', () => {
 29 |   describe('findParentNode', () => {
 30 |     it('should find parent node if cursor is directly inside it', () => {
 31 |       const {
 32 |         state: { schema, selection },
 33 |       } = createEditor(doc(p('hello ')));
 34 |       const { node } = findParentNode(
 35 |         (node) => node.type === schema.nodes.paragraph
 36 |       )(selection)!;
 37 |       expect(node.type.name).toEqual('paragraph');
 38 |     });
 39 |     it('should find parent node if cursor is inside nested child', () => {
 40 |       const {
 41 |         state: { schema, selection },
 42 |       } = createEditor(doc(blockquote(p(''))));
 43 |       const { node } = findParentNode(
 44 |         (node) => node.type === schema.nodes.blockquote
 45 |       )(selection)!;
 46 |       expect(node.type.name).toEqual('blockquote');
 47 |     });
 48 |     it('should return `undefined` if parent node has not been found', () => {
 49 |       const {
 50 |         state: { schema, selection },
 51 |       } = createEditor(doc(p('')));
 52 |       const result = findParentNode(
 53 |         (node) => node.type === schema.nodes.heading
 54 |       )(selection);
 55 |       expect(result).toBeUndefined();
 56 |     });
 57 |   });
 58 | 
 59 |   describe('findParentNodeClosestToPos', () => {
 60 |     it('should find parent node if a given `$pos` is directly inside it', () => {
 61 |       const { state } = createEditor(doc(p('hello')));
 62 |       const { paragraph } = state.schema.nodes;
 63 |       const { node } = findParentNodeClosestToPos(
 64 |         state.doc.resolve(2),
 65 |         (node) => node.type === paragraph
 66 |       )!;
 67 |       expect(node.type.name).toEqual('paragraph');
 68 |     });
 69 |     it('should find parent node if a given `$pos` is inside nested child', () => {
 70 |       const { state } = createEditor(doc(blockquote(p())));
 71 |       const { nodes } = state.schema;
 72 |       const { node } = findParentNodeClosestToPos(
 73 |         state.doc.resolve(2),
 74 |         (node) => node.type === nodes.blockquote
 75 |       )!;
 76 |       expect(node.type.name).toEqual('blockquote');
 77 |     });
 78 |     it('should return `undefined` if a parent node has not been found', () => {
 79 |       const { state } = createEditor(doc(blockquote(p())));
 80 |       const result = findParentNodeClosestToPos(
 81 |         state.doc.resolve(3),
 82 |         (node) => node.type === state.schema.nodes.heading
 83 |       );
 84 |       expect(result).toBeUndefined();
 85 |     });
 86 |   });
 87 | 
 88 |   describe('findParentDomRef', () => {
 89 |     it('should find DOM ref of the parent node if cursor is directly inside it', () => {
 90 |       const {
 91 |         state: { schema, selection },
 92 |         view,
 93 |       } = createEditor(doc(p('hello ')));
 94 |       const domAtPos = view.domAtPos.bind(view);
 95 |       const ref = findParentDomRef(
 96 |         (node) => node.type === schema.nodes.paragraph,
 97 |         domAtPos
 98 |       )(selection);
 99 |       expect(ref instanceof HTMLParagraphElement).toBe(true);
100 |     });
101 |     it('should find DOM ref of the parent node if cursor is inside nested child', () => {
102 |       const {
103 |         state: { schema, selection },
104 |         view,
105 |       } = createEditor(doc(blockquote(p(''))));
106 |       const domAtPos = view.domAtPos.bind(view);
107 |       const ref = findParentDomRef(
108 |         (node) => node.type === schema.nodes.paragraph,
109 |         domAtPos
110 |       )(selection);
111 |       expect(ref instanceof HTMLParagraphElement).toBe(true);
112 |     });
113 |     it('should return `undefined` if parent node has not been found', () => {
114 |       const {
115 |         state: { schema, selection },
116 |         view,
117 |       } = createEditor(doc(blockquote(p('hello'))));
118 |       const domAtPos = view.domAtPos.bind(view);
119 |       const ref = findParentDomRef(
120 |         (node) => node.type === schema.nodes.heading,
121 |         domAtPos
122 |       )(selection);
123 |       expect(ref).toBeUndefined();
124 |     });
125 |   });
126 | 
127 |   describe('hasParentNode', () => {
128 |     it('should return `true` if parent node has been found', () => {
129 |       const {
130 |         state: { schema, selection },
131 |       } = createEditor(doc(p('hello ')));
132 |       const result = hasParentNode(
133 |         (node) => node.type === schema.nodes.paragraph
134 |       )(selection);
135 |       expect(result).toBe(true);
136 |     });
137 |     it('should return `false` if parent node has not been found', () => {
138 |       const {
139 |         state: { schema, selection },
140 |       } = createEditor(doc(p('hello ')));
141 |       const result = hasParentNode((node) => node.type === schema.nodes.table)(
142 |         selection
143 |       );
144 |       expect(result).toBe(false);
145 |     });
146 |   });
147 | 
148 |   describe('findParentNodeOfType', () => {
149 |     it('should find parent node of a given `nodeType` if cursor is directly inside it', () => {
150 |       const {
151 |         state: { schema, selection },
152 |       } = createEditor(doc(p('hello ')));
153 |       const { node } = findParentNodeOfType(schema.nodes.paragraph)(selection)!;
154 |       expect(node.type.name).toEqual('paragraph');
155 |     });
156 |     it('should return `undefined` if parent node of a given `nodeType` has not been found', () => {
157 |       const {
158 |         state: { schema, selection },
159 |       } = createEditor(doc(p('hello ')));
160 |       const result = findParentNodeOfType(schema.nodes.table)(selection);
161 |       expect(result).toBeUndefined();
162 |     });
163 |     it('should find parent node of a given `nodeType`, if `nodeType` is an array', () => {
164 |       const {
165 |         state: {
166 |           schema: {
167 |             nodes: { paragraph, blockquote, table },
168 |           },
169 |           selection,
170 |         },
171 |       } = createEditor(doc(p('hello ')));
172 |       const { node } = findParentNodeOfType([table, blockquote, paragraph])(
173 |         selection
174 |       )!;
175 |       expect(node.type.name).toEqual('paragraph');
176 |     });
177 |   });
178 | 
179 |   describe('findParentNodeOfTypeClosestToPos', () => {
180 |     it('should find parent node of a given `nodeType` if a given `$pos` is directly inside it', () => {
181 |       const { state } = createEditor(doc(p('hello')));
182 |       const { paragraph } = state.schema.nodes;
183 |       const { node } = findParentNodeOfTypeClosestToPos(
184 |         state.doc.resolve(2),
185 |         paragraph
186 |       )!;
187 |       expect(node.type.name).toEqual('paragraph');
188 |     });
189 |     it('should return `undefined` if parent node of a given `nodeType` has not been found at a given `$pos`', () => {
190 |       const { state } = createEditor(doc(p('hello')));
191 |       const { table } = state.schema.nodes;
192 |       const result = findParentNodeOfTypeClosestToPos(
193 |         state.doc.resolve(2),
194 |         table
195 |       );
196 |       expect(result).toBeUndefined();
197 |     });
198 |     it('should find parent node of a given `nodeType` at a given `$pos`, if `nodeType` is an array', () => {
199 |       const { state } = createEditor(doc(p('hello')));
200 |       const { table, blockquote, paragraph } = state.schema.nodes;
201 |       const { node } = findParentNodeOfTypeClosestToPos(state.doc.resolve(2), [
202 |         table,
203 |         blockquote,
204 |         paragraph,
205 |       ])!;
206 |       expect(node.type.name).toEqual('paragraph');
207 |     });
208 |   });
209 | 
210 |   describe('hasParentNodeOfType', () => {
211 |     it('should return `true` if parent node of a given `nodeType` has been found', () => {
212 |       const {
213 |         state: { schema, selection },
214 |       } = createEditor(doc(p('hello ')));
215 |       const result = hasParentNodeOfType(schema.nodes.paragraph)(selection);
216 |       expect(result).toBe(true);
217 |     });
218 |     it('should return `false` if parent node of a given `nodeType` has not been found', () => {
219 |       const {
220 |         state: { schema, selection },
221 |       } = createEditor(doc(p('hello ')));
222 |       const result = hasParentNodeOfType(schema.nodes.table)(selection);
223 |       expect(result).toBe(false);
224 |     });
225 |   });
226 | 
227 |   describe('findParentDomRefOfType', () => {
228 |     it('should find DOM ref of the parent node of a given `nodeType` if cursor is directly inside it', () => {
229 |       const {
230 |         state: { schema, selection },
231 |         view,
232 |       } = createEditor(doc(p('hello ')));
233 |       const domAtPos = view.domAtPos.bind(view);
234 |       const ref = findParentDomRefOfType(
235 |         schema.nodes.paragraph,
236 |         domAtPos
237 |       )(selection);
238 |       expect(ref instanceof HTMLParagraphElement).toBe(true);
239 |     });
240 |     it('should return `undefined` if parent node of a given `nodeType` has not been found', () => {
241 |       const {
242 |         state: { schema, selection },
243 |         view,
244 |       } = createEditor(doc(p('hello ')));
245 |       const domAtPos = view.domAtPos.bind(view);
246 |       const ref = findParentDomRefOfType(
247 |         schema.nodes.table,
248 |         domAtPos
249 |       )(selection);
250 |       expect(ref).toBeUndefined();
251 |     });
252 |   });
253 | 
254 |   describe('findSelectedNodeOfType', () => {
255 |     it('should return `undefined` if selection is not a NodeSelection', () => {
256 |       const {
257 |         state: { schema, selection },
258 |       } = createEditor(doc(p('')));
259 |       const node = findSelectedNodeOfType(schema.nodes.paragraph)(selection);
260 |       expect(node).toBeUndefined();
261 |     });
262 |     it('should return selected node of a given `nodeType`', () => {
263 |       const { state } = createEditor(doc(p('one')));
264 |       const tr = state.tr.setSelection(NodeSelection.create(state.doc, 0));
265 |       const selectedNode = findSelectedNodeOfType(state.schema.nodes.paragraph)(
266 |         tr.selection
267 |       )!;
268 |       expect(selectedNode.node.type.name).toEqual('paragraph');
269 |     });
270 |     it('should return selected node of one of the given `nodeType`s', () => {
271 |       const { state } = createEditor(doc(p('one')));
272 |       const { paragraph, table } = state.schema.nodes;
273 |       const tr = state.tr.setSelection(NodeSelection.create(state.doc, 0));
274 |       const selectedNode = findSelectedNodeOfType([paragraph, table])(
275 |         tr.selection
276 |       )!;
277 |       expect(selectedNode.node.type.name).toEqual('paragraph');
278 |     });
279 |   });
280 | 
281 |   describe('findPositionOfNodeBefore', () => {
282 |     it('should return `undefined` if there is no nodeBefore', () => {
283 |       const {
284 |         state: { selection },
285 |       } = createEditor(doc(p('')));
286 |       const result = findPositionOfNodeBefore(selection);
287 |       expect(result).toBeUndefined();
288 |     });
289 |     it('should return position of nodeBefore if its a table', () => {
290 |       const {
291 |         state: { selection },
292 |       } = createEditor(doc(p('text'), blockquote(p(), p()), ''));
293 |       const position = findPositionOfNodeBefore(selection);
294 |       expect(position).toEqual(6);
295 |     });
296 |     it('should return position of nodeBefore if its a blockquote', () => {
297 |       const {
298 |         state: { selection },
299 |       } = createEditor(doc(p('text'), blockquote(p('')), ''));
300 |       const position = findPositionOfNodeBefore(selection);
301 |       expect(position).toEqual(6);
302 |     });
303 |     it('should return position of nodeBefore if its a nested leaf node', () => {
304 |       const {
305 |         state: { selection },
306 |       } = createEditor(
307 |         doc(
308 |           p('text'),
309 |           blockquote(blockquote(blockquote(p('1'), atomBlock(), '')))
310 |         )
311 |       );
312 |       const position = findPositionOfNodeBefore(selection);
313 |       expect(position).toEqual(12);
314 |     });
315 |     it('should return position of nodeBefore if its a leaf node', () => {
316 |       const {
317 |         state: { selection },
318 |       } = createEditor(doc(p('text'), atomBlock(), ''));
319 |       const position = findPositionOfNodeBefore(selection);
320 |       expect(position).toEqual(6);
321 |     });
322 |     it('should return position of nodeBefore if its a leaf node with nested inline atom node', () => {
323 |       const {
324 |         state: { selection },
325 |       } = createEditor(doc(p('text'), atomContainer(atomBlock()), ''));
326 |       const position = findPositionOfNodeBefore(selection);
327 |       expect(position).toEqual(6);
328 |     });
329 |   });
330 | 
331 |   describe('findDomRefAtPos', () => {
332 |     it('should return DOM reference of a top level block leaf node', () => {
333 |       const { view } = createEditor(doc(p('text'), atomBlock()));
334 |       const ref = findDomRefAtPos(6, view.domAtPos.bind(view));
335 |       expect(ref instanceof HTMLDivElement).toBe(true);
336 |       expect((ref as HTMLElement).getAttribute('data-node-type')).toEqual(
337 |         'atomBlock'
338 |       );
339 |     });
340 | 
341 |     it('should return DOM reference of a nested inline leaf node', () => {
342 |       const { view } = createEditor(doc(p('one', atomInline(), 'two')));
343 |       const ref = findDomRefAtPos(4, view.domAtPos.bind(view));
344 |       expect(ref instanceof HTMLSpanElement).toBe(true);
345 |       expect((ref as HTMLElement).getAttribute('data-node-type')).toEqual(
346 |         'atomInline'
347 |       );
348 |     });
349 | 
350 |     it('should return DOM reference of a content block node', () => {
351 |       const { view } = createEditor(doc(p('one'), blockquote(p('two'))));
352 |       const ref = findDomRefAtPos(5, view.domAtPos.bind(view));
353 |       expect(ref instanceof HTMLQuoteElement).toBe(true);
354 |     });
355 | 
356 |     it('should return DOM reference of a text node when offset=0', () => {
357 |       const { view } = createEditor(doc(p('text')));
358 |       const ref = findDomRefAtPos(1, view.domAtPos.bind(view));
359 |       expect(ref instanceof HTMLParagraphElement).toBe(true);
360 |     });
361 | 
362 |     it('should return DOM reference of a paragraph if cursor is inside of a text node', () => {
363 |       const { view } = createEditor(doc(p(atomInline(), 'text')));
364 |       const ref = findDomRefAtPos(3, view.domAtPos.bind(view));
365 |       expect(ref instanceof HTMLParagraphElement).toBe(true);
366 |     });
367 |   });
368 | 
369 |   it('should return `undefined` if the whole selection doesnt share the same parent', () => {
370 |     const {
371 |       state: { selection },
372 |     } = createEditor(
373 |       doc(
374 |         article(section(p('hello ')), section(p(' world'))),
375 |         p(' !!!!')
376 |       )
377 |     );
378 |     const result = findParentNode((node) => node.type.name === 'article')(
379 |       selection,
380 |       true
381 |     );
382 |     expect(result).toBeUndefined();
383 |   });
384 | 
385 |   it('should return `section` if the whole selection is inside the section', () => {
386 |     const {
387 |       state: { selection },
388 |     } = createEditor(
389 |       doc(article(section(p('hello'), p(' world'))), p(' !!!!'))
390 |     );
391 |     const { node } = findParentNode((node) => node.type.name === 'section')(
392 |       selection
393 |     )!;
394 |     expect(node.type.name).toEqual('section');
395 |   });
396 | });
397 | 


--------------------------------------------------------------------------------
/__tests__/transforms.ts:
--------------------------------------------------------------------------------
  1 | import { createEditor, testHelpers } from '../test-helpers';
  2 | import { NodeSelection, TextSelection } from 'prosemirror-state';
  3 | import { Fragment } from 'prosemirror-model';
  4 | import {
  5 |   removeParentNodeOfType,
  6 |   replaceParentNodeOfType,
  7 |   removeSelectedNode,
  8 |   safeInsert,
  9 |   replaceSelectedNode,
 10 |   setParentNodeMarkup,
 11 |   selectParentNodeOfType,
 12 |   removeNodeBefore,
 13 |   isNodeSelection,
 14 | } from '../src';
 15 | 
 16 | const {
 17 |   code_block,
 18 |   doc,
 19 |   hr,
 20 |   p,
 21 |   heading: h1,
 22 |   strong,
 23 |   containerWithRestrictedContent,
 24 |   blockquote,
 25 |   atomInline,
 26 |   atomBlock,
 27 | } = testHelpers;
 28 | 
 29 | describe('transforms', () => {
 30 |   describe('removeParentNodeOfType', () => {
 31 |     it('should return an original transaction if there is no parent node of a given NodeType', () => {
 32 |       const {
 33 |         state: { schema, tr },
 34 |       } = createEditor(doc(p('')));
 35 |       const newTr = removeParentNodeOfType(schema.nodes.blockquote)(tr);
 36 |       expect(tr).toBe(newTr);
 37 |     });
 38 |     describe('when there is a p("one") before the blockquote node and p("two") after', () => {
 39 |       it('should remove blockquote and preserve p("one") and p("two")', () => {
 40 |         const {
 41 |           state: { schema, tr },
 42 |         } = createEditor(doc(p('one'), blockquote(p('')), p('two')));
 43 |         const newTr = removeParentNodeOfType(schema.nodes.blockquote)(tr);
 44 |         expect(newTr).not.toBe(tr);
 45 |         expect(newTr.doc).toEqualDocument(doc(p('one'), p('two')));
 46 |       });
 47 |     });
 48 |   });
 49 | 
 50 |   describe('replaceParentNodeOfType', () => {
 51 |     describe('returning an original tr', () => {
 52 |       it('should return an original transaction if there is no parent node of a given NodeType', () => {
 53 |         const {
 54 |           state: { schema, tr },
 55 |         } = createEditor(doc(p('')));
 56 |         const node = schema.nodes.paragraph.createChecked(
 57 |           {},
 58 |           schema.text('new')
 59 |         );
 60 |         const newTr = replaceParentNodeOfType(
 61 |           schema.nodes.blockquote,
 62 |           node
 63 |         )(tr);
 64 |         expect(tr).toBe(newTr);
 65 |       });
 66 |       it('should return an original transaction if replacing is not possible', () => {
 67 |         const {
 68 |           state: { schema, tr },
 69 |         } = createEditor(doc(p('one'), blockquote(p('')), p('two')));
 70 |         const node = schema.text('new');
 71 |         const newTr = replaceParentNodeOfType(
 72 |           schema.nodes.blockquote,
 73 |           node
 74 |         )(tr);
 75 |         expect(tr).toBe(newTr);
 76 |       });
 77 |     });
 78 |     describe('replacing a parent node', () => {
 79 |       it('should replace parent if array of nodeTypes is given', () => {
 80 |         const {
 81 |           state: { schema, tr },
 82 |         } = createEditor(
 83 |           doc(p('one'), blockquote(p('two')), p('three'))
 84 |         );
 85 |         const { paragraph, blockquote: quote } = schema.nodes;
 86 |         const node = paragraph.createChecked({}, schema.text('new'));
 87 | 
 88 |         const newTr = replaceParentNodeOfType([quote, paragraph], node)(tr);
 89 |         expect(newTr).not.toBe(tr);
 90 |         expect(newTr.doc).toEqualDocument(doc(p('one'), p('new'), p('three')));
 91 |       });
 92 |       describe('when there is a p("one") before the blockquote node and p("two") after', () => {
 93 |         it('should replace blockquote with p("new"), preserve p("one") and p("two"), and put cursor inside of the new node', () => {
 94 |           const {
 95 |             state: { schema, tr },
 96 |           } = createEditor(doc(p('one'), blockquote(p('')), p('two')));
 97 |           const node = schema.nodes.paragraph.createChecked(
 98 |             {},
 99 |             schema.text('new')
100 |           );
101 |           const newTr = replaceParentNodeOfType(
102 |             schema.nodes.blockquote,
103 |             node
104 |           )(tr);
105 |           expect(newTr).not.toBe(tr);
106 |           expect(newTr.doc).toEqualDocument(doc(p('one'), p('new'), p('two')));
107 |           expect(newTr.selection.$from.pos).toEqual(6);
108 |         });
109 |       });
110 |       describe('when there are tree paragraphs', () => {
111 |         it('should replace the middle paragraph with p("new"), preserve p("one") and p("two"), and put cursor inside of the new node', () => {
112 |           const {
113 |             state: { schema, tr },
114 |           } = createEditor(doc(p('one'), p('hellothere'), p('two')));
115 |           const node = schema.nodes.paragraph.createChecked(
116 |             {},
117 |             schema.text('new')
118 |           );
119 |           const newTr = replaceParentNodeOfType(
120 |             schema.nodes.paragraph,
121 |             node
122 |           )(tr);
123 |           expect(newTr).not.toBe(tr);
124 |           expect(newTr.doc).toEqualDocument(doc(p('one'), p('new'), p('two')));
125 |           expect(newTr.selection.$from.pos).toEqual(6);
126 |         });
127 |       });
128 |     });
129 |     describe('composing with other tr', () => {
130 |       it('should be composable with other transforms', () => {
131 |         const {
132 |           state: { schema, tr },
133 |         } = createEditor(
134 |           doc(p('one'), blockquote(p('hellothere')), p('two'))
135 |         );
136 |         const { paragraph, blockquote: blockquoteNode } = schema.nodes;
137 |         const node = paragraph.createChecked({}, schema.text('new'));
138 | 
139 |         const newTr = replaceParentNodeOfType(blockquoteNode, node)(tr);
140 |         expect(newTr).not.toBe(tr);
141 |         expect(newTr.doc).toEqualDocument(doc(p('one'), p('new'), p('two')));
142 | 
143 |         const newTr2 = removeParentNodeOfType(paragraph)(newTr);
144 |         expect(newTr2).not.toBe(newTr);
145 |         expect(newTr2.doc).toEqualDocument(doc(p('one'), p('two')));
146 |       });
147 |     });
148 |   });
149 | 
150 |   describe('removeSelectedNode', () => {
151 |     it('should return an original transaction if selection is not a NodeSelection', () => {
152 |       const {
153 |         state: { tr },
154 |       } = createEditor(doc(p('one')));
155 |       const newTr = removeSelectedNode(tr);
156 |       expect(newTr).toBe(tr);
157 |     });
158 | 
159 |     it('should remove selected inline node', () => {
160 |       const {
161 |         state: { tr },
162 |       } = createEditor(doc(p('one', atomInline(), 'two')));
163 |       const newTr = removeSelectedNode(tr);
164 |       expect(newTr).not.toBe(tr);
165 |       expect(newTr.doc).toEqualDocument(doc(p('onetwo')));
166 |     });
167 | 
168 |     it('should remove selected block node', () => {
169 |       const { state } = createEditor(doc(p('one'), p('test'), p('two')));
170 |       const tr = state.tr.setSelection(NodeSelection.create(state.doc, 5));
171 |       const newTr = removeSelectedNode(tr);
172 |       expect(newTr).not.toBe(tr);
173 |       expect(newTr.doc).toEqualDocument(doc(p('one'), p('two')));
174 |     });
175 |   });
176 | 
177 |   describe('safeInsert', () => {
178 |     describe('inserting at the cursor position', () => {
179 |       describe('inserting into the current node', () => {
180 |         it('should insert an inline node into a non-empty paragraph', () => {
181 |           const {
182 |             state: { schema, tr },
183 |           } = createEditor(doc(p('one')));
184 |           const node = schema.nodes.atomInline.createChecked();
185 |           const newTr = safeInsert(node)(tr);
186 |           expect(newTr).not.toBe(tr);
187 |           expect(newTr.doc).toEqualDocument(doc(p('one', atomInline())));
188 |         });
189 |         it('should insert an inline node into an empty paragraph', () => {
190 |           const {
191 |             state: { schema, tr },
192 |           } = createEditor(doc(p('')));
193 |           const node = schema.nodes.atomInline.createChecked();
194 |           const newTr = safeInsert(node)(tr);
195 |           expect(newTr).not.toBe(tr);
196 |           expect(newTr.doc).toEqualDocument(doc(p(atomInline())));
197 |         });
198 |         it('should insert a Fragment into a non-empty paragraph', () => {
199 |           const {
200 |             state: { schema, tr },
201 |           } = createEditor(doc(p('one')));
202 |           const node = schema.nodes.atomInline.createChecked();
203 |           const newTr = safeInsert(Fragment.from(node))(tr);
204 |           expect(newTr).not.toBe(tr);
205 |           expect(newTr.doc).toEqualDocument(doc(p('one', atomInline())));
206 |         });
207 |         it('should insert a Fragment into an empty paragraph', () => {
208 |           const {
209 |             state: { schema, tr },
210 |           } = createEditor(doc(p('')));
211 |           const node = schema.nodes.atomInline.createChecked();
212 |           const newTr = safeInsert(Fragment.from(node))(tr);
213 |           expect(newTr).not.toBe(tr);
214 |           expect(newTr.doc).toEqualDocument(doc(p(atomInline())));
215 |         });
216 |       });
217 | 
218 |       describe('appending after the current node', () => {
219 |         it('should insert a paragraph after the parent node if its not allowed and move cursor inside of the new paragraph', () => {
220 |           const {
221 |             state: { schema, tr },
222 |           } = createEditor(doc(p(strong('zero'), 'one'), p('three')));
223 |           const node = schema.nodes.paragraph.createChecked(
224 |             {},
225 |             schema.text('two')
226 |           );
227 |           const newTr = safeInsert(node)(tr);
228 |           expect(newTr).not.toBe(tr);
229 |           expect(newTr.doc).toEqualDocument(
230 |             doc(p(strong('zero'), 'one'), p('two'), p('three'))
231 |           );
232 |           expect(newTr.selection.$from.parent.textContent).toEqual('two');
233 |         });
234 |         it('should insert a Fragment after the parent node if its not allowed and move cursor inside of the new paragraph', () => {
235 |           const {
236 |             state: { schema, tr },
237 |           } = createEditor(doc(p(strong('zero'), 'one'), p('three')));
238 |           const node = schema.nodes.paragraph.createChecked(
239 |             {},
240 |             schema.text('two')
241 |           );
242 |           const newTr = safeInsert(Fragment.from(node))(tr);
243 |           expect(newTr).not.toBe(tr);
244 |           expect(newTr.doc).toEqualDocument(
245 |             doc(p(strong('zero'), 'one'), p('two'), p('three'))
246 |           );
247 |           expect(newTr.selection.$from.parent.textContent).toEqual('two');
248 |         });
249 |         it("should not split a node when it's impossible to replace it, should append instead", () => {
250 |           const {
251 |             state: { schema, tr },
252 |           } = createEditor(doc(containerWithRestrictedContent(p(''))));
253 |           const node =
254 |             schema.nodes.containerWithRestrictedContent.createChecked(
255 |               {},
256 |               schema.nodes.paragraph.createChecked({}, schema.text('new'))
257 |             );
258 |           const newTr = safeInsert(node)(tr);
259 |           expect(newTr).not.toBe(tr);
260 |           expect(newTr.doc).toEqualDocument(
261 |             doc(
262 |               containerWithRestrictedContent(p('')),
263 |               containerWithRestrictedContent(p('new'))
264 |             )
265 |           );
266 |           expect(newTr.selection.$from.parent.textContent).toEqual('new');
267 |         });
268 |       });
269 |     });
270 | 
271 |     describe('When selection is a NodeSelection', () => {
272 |       describe('when tryToReplace = true', () => {
273 |         it('should replace selected block node with the given block node', () => {
274 |           const { state } = createEditor(doc(atomBlock({ color: 'green' })));
275 |           const node = state.schema.nodes.atomBlock.createChecked({
276 |             color: 'red',
277 |           });
278 |           const tr = state.tr.setSelection(NodeSelection.create(state.doc, 0));
279 |           const newTr = safeInsert(node, undefined, true)(tr);
280 |           expect(newTr).not.toBe(tr);
281 |           expect(newTr.doc).toEqualDocument(doc(atomBlock({ color: 'red' })));
282 |           expect(isNodeSelection(newTr.selection)).toBe(true);
283 |         });
284 | 
285 |         it('should replace selected inline node with the given inline node', () => {
286 |           const { state } = createEditor(
287 |             doc(p(atomInline({ color: 'green' })))
288 |           );
289 |           const node = state.schema.nodes.atomInline.createChecked({
290 |             color: 'red',
291 |           });
292 |           const tr = state.tr.setSelection(NodeSelection.create(state.doc, 1));
293 |           const newTr = safeInsert(node, undefined, true)(tr);
294 |           expect(newTr).not.toBe(tr);
295 |           expect(newTr.doc).toEqualDocument(
296 |             doc(p(atomInline({ color: 'red' })))
297 |           );
298 |           expect(isNodeSelection(newTr.selection)).toBe(true);
299 |         });
300 |       });
301 | 
302 |       describe('when tryToReplace = false', () => {
303 |         it('should append a node', () => {
304 |           const { state } = createEditor(
305 |             doc(blockquote(atomBlock({ color: 'green' })))
306 |           );
307 |           const tr = state.tr.setSelection(NodeSelection.create(state.doc, 1));
308 |           const node = state.schema.nodes.atomBlock.createChecked({
309 |             color: 'red',
310 |           });
311 |           const newTr = safeInsert(node)(tr);
312 |           expect(newTr).not.toBe(tr);
313 |           expect(newTr.doc).toEqualDocument(
314 |             doc(
315 |               blockquote(
316 |                 atomBlock({ color: 'green' }),
317 |                 atomBlock({ color: 'red' })
318 |               )
319 |             )
320 |           );
321 |           expect(isNodeSelection(newTr.selection)).toBe(true);
322 |         });
323 |       });
324 |     });
325 | 
326 |     describe('replacing an empty parent paragraph', () => {
327 |       it('should replace an empty parent paragraph with the given node', () => {
328 |         const {
329 |           state: { schema, tr },
330 |         } = createEditor(doc(p('one'), p(''), p('three')));
331 |         const node = schema.nodes.blockquote.createChecked(
332 |           {},
333 |           schema.nodes.paragraph.createChecked({}, schema.text('two'))
334 |         );
335 |         const newTr = safeInsert(Fragment.from(node))(tr);
336 |         expect(newTr).not.toBe(tr);
337 |         expect(newTr.doc).toEqualDocument(
338 |           doc(p('one'), blockquote(p('two')), p('three'))
339 |         );
340 |         expect(newTr.selection.$from.parent.textContent).toEqual('two');
341 |       });
342 |       it("should replace an empty paragraph inside other node if it's allowed by schema", () => {
343 |         const {
344 |           state: { schema, tr },
345 |         } = createEditor(doc(blockquote(p(''))));
346 |         const node = schema.nodes.containerWithRestrictedContent.createChecked(
347 |           {},
348 |           schema.nodes.paragraph.createChecked({}, schema.text('two'))
349 |         );
350 |         const newTr = safeInsert(node)(tr);
351 |         expect(newTr).not.toBe(tr);
352 |         expect(newTr.doc).toEqualDocument(
353 |           doc(blockquote(containerWithRestrictedContent(p('two'))))
354 |         );
355 |         expect(newTr.selection.$from.parent.textContent).toEqual('two');
356 |       });
357 |       describe('when inserting a selectable atom node', () => {
358 |         it('should replace a parent if its the only node in the doc and retain the node selection', () => {
359 |           const {
360 |             state: { schema, tr },
361 |           } = createEditor(doc(p('')));
362 |           const node = schema.nodes.atomBlock.createChecked({
363 |             color: 'yellow',
364 |           });
365 |           const newTr = safeInsert(node)(tr);
366 |           expect(newTr).not.toBe(tr);
367 |           expect(newTr.doc).toEqualDocument(
368 |             doc(atomBlock({ color: 'yellow' }))
369 |           );
370 |           expect(isNodeSelection(newTr.selection)).toBe(true);
371 |         });
372 |       });
373 |     });
374 | 
375 |     describe('inserting at given position', () => {
376 |       it('should insert a node at position 0 (start of the doc) and move cursor inside of the new paragraph', () => {
377 |         const {
378 |           state: { schema, tr },
379 |         } = createEditor(doc(p('one'), p('two')));
380 |         const node = schema.nodes.paragraph.createChecked(
381 |           {},
382 |           schema.text('new')
383 |         );
384 |         const newTr = safeInsert(node, 0)(tr);
385 |         expect(newTr).not.toBe(tr);
386 |         expect(newTr.doc).toEqualDocument(doc(p('new'), p('one'), p('two')));
387 |         expect(newTr.selection.$from.parent.textContent).toEqual('new');
388 |         expect(!!(newTr.selection as TextSelection).$cursor).toBe(true);
389 |       });
390 |       it('should insert a Fragment at position 0 (start of the doc) and move cursor inside of the new paragraph', () => {
391 |         const {
392 |           state: { schema, tr },
393 |         } = createEditor(doc(p('one'), p('two')));
394 |         const node = schema.nodes.paragraph.createChecked(
395 |           {},
396 |           schema.text('new')
397 |         );
398 |         const newTr = safeInsert(Fragment.from(node), 0)(tr);
399 |         expect(newTr).not.toBe(tr);
400 |         expect(newTr.doc).toEqualDocument(doc(p('new'), p('one'), p('two')));
401 |         expect(newTr.selection.$from.parent.textContent).toEqual('new');
402 |         expect(!!(newTr.selection as TextSelection).$cursor).toBe(true);
403 |       });
404 |       it('should insert a node at position 1 and move cursor inside of the new paragraph', () => {
405 |         const {
406 |           state: { schema, tr },
407 |         } = createEditor(doc(p('one'), p('two')));
408 |         const node = schema.nodes.paragraph.createChecked(
409 |           {},
410 |           schema.text('new')
411 |         );
412 |         const newTr = safeInsert(node, 1)(tr);
413 |         expect(newTr).not.toBe(tr);
414 |         expect(newTr.doc).toEqualDocument(doc(p('one'), p('new'), p('two')));
415 |         expect(newTr.selection.$from.parent.textContent).toEqual('new');
416 |         expect(!!(newTr.selection as TextSelection).$cursor).toBe(true);
417 |       });
418 |       it('should insert a node at position in between two nodes and move cursor inside of the new paragraph', () => {
419 |         const {
420 |           state: { schema, tr },
421 |         } = createEditor(doc(p('one'), p('two')));
422 |         const node = schema.nodes.paragraph.createChecked(
423 |           {},
424 |           schema.text('new')
425 |         );
426 |         const newTr = safeInsert(node, 5)(tr);
427 |         expect(newTr).not.toBe(tr);
428 |         expect(newTr.doc).toEqualDocument(doc(p('one'), p('new'), p('two')));
429 |         expect(newTr.selection.$from.parent.textContent).toEqual('new');
430 |         expect(!!(newTr.selection as TextSelection).$cursor).toBe(true);
431 |       });
432 |     });
433 | 
434 |     describe('setting selection after insertion', () => {
435 |       it('should set the selection after the inserted content (text)', () => {
436 |         const {
437 |           state: { schema, tr },
438 |         } = createEditor(doc(p('')));
439 |         const newTr = safeInsert(Fragment.from(schema.text('new')))(tr);
440 |         expect(newTr).not.toBe(tr);
441 |         expect(newTr.doc).toEqualDocument(doc(p('new')));
442 |         expect(newTr.selection.head).toEqual(4);
443 |         expect(!!(newTr.selection as TextSelection).$cursor).toBe(true);
444 |       });
445 |       it('should move cursor to the inserted paragraph', () => {
446 |         const {
447 |           state: { schema, tr },
448 |         } = createEditor(doc(p('old')));
449 |         const node = schema.nodes.paragraph.createChecked(
450 |           {},
451 |           schema.text('new')
452 |         );
453 |         const newTr = safeInsert(node)(tr);
454 |         expect(newTr).not.toBe(tr);
455 |         expect(newTr.doc).toEqualDocument(doc(p('old'), p('new')));
456 |         expect(newTr.selection.head).toEqual(6);
457 |         expect(!!(newTr.selection as TextSelection).$cursor).toBe(true);
458 |       });
459 | 
460 |       it('should set NodeSelection when the node is selectable and inserted after the current node', () => {
461 |         const {
462 |           state: { tr, schema },
463 |         } = createEditor(doc(p('text')));
464 |         const node = schema.nodes.atomBlock.createChecked({
465 |           color: 'red',
466 |         });
467 |         const newTr = safeInsert(node)(tr);
468 |         expect(newTr).not.toBe(tr);
469 |         expect(newTr.doc).toEqualDocument(
470 |           doc(p('text'), atomBlock({ color: 'red' }))
471 |         );
472 |         expect(isNodeSelection(newTr.selection)).toBe(true);
473 |       });
474 | 
475 |       it('should set NodeSelection when the node is selectable and empty paragraph is replaced', () => {
476 |         const {
477 |           state: { tr, schema },
478 |         } = createEditor(doc(p('')));
479 |         const node = schema.nodes.atomBlock.createChecked({
480 |           color: 'red',
481 |         });
482 |         const newTr = safeInsert(node)(tr);
483 |         expect(newTr).not.toBe(tr);
484 |         expect(newTr.doc).toEqualDocument(doc(atomBlock({ color: 'red' })));
485 |         expect(isNodeSelection(newTr.selection)).toBe(true);
486 |       });
487 | 
488 |       it('should set NodeSelection when the node is selectable and inserted at the given position', () => {
489 |         const {
490 |           state: { tr, schema },
491 |         } = createEditor(doc(p('')));
492 |         const node = schema.nodes.atomInline.createChecked({
493 |           color: 'red',
494 |         });
495 |         const newTr = safeInsert(node)(tr);
496 |         expect(newTr).not.toBe(tr);
497 |         expect(newTr.doc).toEqualDocument(doc(p(atomInline({ color: 'red' }))));
498 |         expect(isNodeSelection(newTr.selection)).toBe(true);
499 |       });
500 |     });
501 |   });
502 | 
503 |   describe('replaceSelectedNode', () => {
504 |     it('should return an original transaction if current selection is not a NodeSelection', () => {
505 |       const {
506 |         state: { schema, tr },
507 |       } = createEditor(doc(p('')));
508 |       const node = schema.nodes.paragraph.createChecked({}, schema.text('new'));
509 |       const newTr = replaceSelectedNode(node)(tr);
510 |       expect(tr).toBe(newTr);
511 |     });
512 |     it('should return an original transaction if replacing is not possible', () => {
513 |       const { state } = createEditor(doc(p('one')));
514 |       const tr = state.tr.setSelection(NodeSelection.create(state.doc, 0));
515 |       const node = state.schema.text('new');
516 |       const newTr = replaceSelectedNode(node)(tr);
517 |       expect(tr).toBe(newTr);
518 |     });
519 | 
520 |     it('should replace selected node with the given `node`', () => {
521 |       const { state } = createEditor(doc(p('one'), p('test'), p('two')));
522 |       const tr = state.tr.setSelection(NodeSelection.create(state.doc, 5));
523 |       const node = state.schema.nodes.paragraph.createChecked(
524 |         {},
525 |         state.schema.text('new')
526 |       );
527 |       const newTr = replaceSelectedNode(node)(tr);
528 |       expect(newTr).not.toBe(tr);
529 |       expect(newTr.doc).toEqualDocument(doc(p('one'), p('new'), p('two')));
530 |     });
531 | 
532 |     it('should replace selected node with the given `fragment`', () => {
533 |       const { state } = createEditor(doc(p('one'), p('test'), p('two')));
534 |       const tr = state.tr.setSelection(NodeSelection.create(state.doc, 5));
535 |       const fragment = Fragment.fromArray([
536 |         state.schema.nodes.paragraph.createChecked(
537 |           {},
538 |           state.schema.text('new')
539 |         ),
540 |         state.schema.nodes.paragraph.createChecked(
541 |           {},
542 |           state.schema.text('paragraphs')
543 |         ),
544 |       ]);
545 |       const newTr = replaceSelectedNode(fragment)(tr);
546 |       expect(newTr).not.toBe(tr);
547 |       expect(newTr.doc).toEqualDocument(
548 |         doc(p('one'), p('new'), p('paragraphs'), p('two'))
549 |       );
550 |     });
551 |   });
552 | 
553 |   describe('setParentNodeMarkup', () => {
554 |     it('should return an original transaction if there is not parent node of a given nodeType', () => {
555 |       const {
556 |         state: { schema, tr },
557 |       } = createEditor(doc(p('')));
558 |       const newTr = setParentNodeMarkup(
559 |         schema.nodes.blockquote,
560 |         schema.nodes.paragraph
561 |       )(tr);
562 |       expect(tr).toBe(newTr);
563 |     });
564 | 
565 |     it('should update nodeType', () => {
566 |       const {
567 |         state: { schema, tr },
568 |       } = createEditor(doc(p('text')));
569 |       const newTr = setParentNodeMarkup(
570 |         schema.nodes.paragraph,
571 |         schema.nodes.code_block
572 |       )(tr);
573 |       expect(newTr).not.toBe(tr);
574 |       expect(newTr.doc).toEqualDocument(doc(code_block('text')));
575 |     });
576 | 
577 |     it('should update attributes', () => {
578 |       const { state } = createEditor(doc(h1('text')));
579 |       const {
580 |         schema: {
581 |           nodes: { heading },
582 |         },
583 |       } = state;
584 |       const newTr = setParentNodeMarkup(heading, null, {
585 |         level: 5,
586 |       })(state.tr);
587 |       expect(newTr).not.toBe(state.tr);
588 |       newTr.doc.content.descendants((child) => {
589 |         if (child.type === heading) {
590 |           expect(child.attrs).toEqual({
591 |             level: 5,
592 |           });
593 |         }
594 |       });
595 |     });
596 |   });
597 | 
598 |   describe('selectParentNodeOfType', () => {
599 |     it('should return an original transaction if current selection is a NodeSelection', () => {
600 |       const { state } = createEditor(doc(p('one')));
601 |       const tr = state.tr.setSelection(NodeSelection.create(state.doc, 1));
602 |       const newTr = selectParentNodeOfType(state.schema.nodes.paragraph)(tr);
603 |       expect(tr).toBe(newTr);
604 |     });
605 |     it('should return an original transaction if there is no parent node of a given `nodeType`', () => {
606 |       const {
607 |         state: { tr, schema },
608 |       } = createEditor(doc(p('one')));
609 |       const newTr = selectParentNodeOfType(schema.nodes.blockquote)(tr);
610 |       expect(tr).toBe(newTr);
611 |     });
612 |     it('should return a new transaction that selects a parent node of a given `nodeType`', () => {
613 |       const {
614 |         state: { tr, schema },
615 |       } = createEditor(doc(p('one')));
616 |       const newTr = selectParentNodeOfType(schema.nodes.paragraph)(tr);
617 |       expect(newTr).not.toBe(tr);
618 |       expect((newTr.selection as NodeSelection).node.type.name).toEqual(
619 |         'paragraph'
620 |       );
621 |     });
622 |     it('should return a new transaction that selects a parent node of a given `nodeType`, if `nodeType` an array', () => {
623 |       const {
624 |         state: {
625 |           tr,
626 |           schema: {
627 |             nodes: { paragraph, blockquote: blockquoteNode },
628 |           },
629 |         },
630 |       } = createEditor(doc(p('one')));
631 |       const newTr = selectParentNodeOfType([blockquoteNode, paragraph])(tr);
632 |       expect(newTr).not.toBe(tr);
633 |       expect((newTr.selection as NodeSelection).node.type.name).toEqual(
634 |         'paragraph'
635 |       );
636 |     });
637 |   });
638 | 
639 |   describe('removeNodeBefore', () => {
640 |     it('should return an original transaction if there is no nodeBefore', () => {
641 |       const {
642 |         state: { tr },
643 |       } = createEditor(doc(p('')));
644 |       const newTr = removeNodeBefore(tr);
645 |       expect(tr).toBe(newTr);
646 |     });
647 |     it('should a new transaction that removes nodeBefore if its a hr', () => {
648 |       const {
649 |         state: { tr },
650 |       } = createEditor(doc(p('one'), hr(), '', p('two')));
651 |       const newTr = removeNodeBefore(tr);
652 |       expect(newTr).not.toBe(tr);
653 |       expect(newTr.doc).toEqualDocument(doc(p('one'), p('two')));
654 |     });
655 |     it('should a new transaction that removes nodeBefore if its a blockquote', () => {
656 |       const {
657 |         state: { tr },
658 |       } = createEditor(doc(p('one'), blockquote(p('')), '', p('two')));
659 |       const newTr = removeNodeBefore(tr);
660 |       expect(newTr).not.toBe(tr);
661 |       expect(newTr.doc).toEqualDocument(doc(p('one'), p('two')));
662 |     });
663 |     it('should a new transaction that removes nodeBefore if its a leaf node', () => {
664 |       const {
665 |         state: { tr },
666 |       } = createEditor(doc(p('one'), atomBlock(), '', p('two')));
667 |       const newTr = removeNodeBefore(tr);
668 |       expect(newTr).not.toBe(tr);
669 |       expect(newTr.doc).toEqualDocument(doc(p('one'), p('two')));
670 |     });
671 |   });
672 | });
673 | 


--------------------------------------------------------------------------------