From c91cdd9af3bd5020ec65d2d77353e728df365ad1 Mon Sep 17 00:00:00 2001 From: yousefed Date: Tue, 22 Nov 2022 12:26:53 +0100 Subject: [PATCH 01/55] upgrade to react 18 --- examples/editor/package.json | 8 +- examples/editor/src/main.tsx | 9 +- package-lock.json | 480 ++++-------------- package.json | 4 +- packages/core/package.json | 14 +- .../BubbleMenu/BubbleMenuExtension.tsx | 8 +- .../DraggableBlocks/DraggableBlocksPlugin.tsx | 32 +- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 11 +- 8 files changed, 143 insertions(+), 423 deletions(-) diff --git a/examples/editor/package.json b/examples/editor/package.json index 9ad54b2fe1..a14029b314 100644 --- a/examples/editor/package.json +++ b/examples/editor/package.json @@ -10,12 +10,12 @@ }, "dependencies": { "@blocknote/core": "^0.1.2", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.12", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^1.0.7", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 3a568f9845..0808f1a4d8 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,14 +1,13 @@ import React from "react"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import App from "./App"; import "./index.css"; window.React = React; -window.ReactDOM = ReactDOM; -ReactDOM.render( +const root = createRoot(document.getElementById("root")!); +root.render( - , - document.getElementById("root") + ); diff --git a/package-lock.json b/package-lock.json index 1b635d9666..a040621796 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,12 +21,12 @@ "version": "0.1.2", "dependencies": { "@blocknote/core": "^0.1.2", - "react": "^17.0.2", - "react-dom": "^17.0.2" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.12", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^1.0.7", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", @@ -180,6 +180,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, "dependencies": { "@babel/types": "^7.18.6" }, @@ -2109,14 +2110,6 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, - "node_modules/@emotion/is-prop-valid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", - "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", - "dependencies": { - "@emotion/memoize": "^0.8.0" - } - }, "node_modules/@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", @@ -2167,11 +2160,6 @@ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, "node_modules/@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", @@ -2252,24 +2240,24 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz", - "integrity": "sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.2.tgz", + "integrity": "sha512-Skfy0YS3NJ5nV9us0uuPN0HDk1Q4edljaOhRBJGDWs9EBa7ZVMYBHRFlhLvvmwEoaIM9BlH6QJFn9/uZg0bACg==" }, "node_modules/@floating-ui/dom": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.3.tgz", - "integrity": "sha512-6H1kwjkOZKabApNtXRiYHvMmYJToJ1DV7rQ3xc/WJpOABhQIOJJOdz2AOejj8X+gcybaFmBpisVTZxBZAM3V0w==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.6.tgz", + "integrity": "sha512-kt/tg1oip9OAH1xjCTcx1OpcUpu9rjDw3GKJ/rEhUqhO7QyJWfrHU0DpLTNsH67+JyFL5Kv9X1utsXwKFVtyEQ==", "dependencies": { - "@floating-ui/core": "^1.0.1" + "@floating-ui/core": "^1.0.2" } }, "node_modules/@floating-ui/react-dom": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.0.tgz", - "integrity": "sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.1.tgz", + "integrity": "sha512-UW0t1Gi8ikbDRr8cQPVcqIDMBwUEENe5V4wlHWdrJ5egFnRQFBV9JirauTBFI6S8sM1qFUC1i+qa3g87E6CLTw==", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.0.5" }, "peerDependencies": { "react": ">=16.8.0", @@ -2277,9 +2265,9 @@ } }, "node_modules/@floating-ui/react-dom-interactions": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.2.tgz", - "integrity": "sha512-KhF+UN+MVqUx1bG1fe0aAiBl1hbz07Uin6UW70mxwUDhaGpitM16CYvGri1EqGY4hnWK8TQknDSP8iQFOxjhsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.3.tgz", + "integrity": "sha512-UEHqdnzyoiWNU5az/tAljr9iXFzN18DcvpMqW+/cXz4FEhDEB1ogLtWldOWCujLerPBnSRocADALafelOReMpw==", "dependencies": { "@floating-ui/react-dom": "^1.0.0", "aria-hidden": "^1.1.3" @@ -4493,16 +4481,6 @@ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, - "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "dev": true, - "dependencies": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "node_modules/@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -4557,9 +4535,9 @@ "devOptional": true }, "node_modules/@types/react": { - "version": "17.0.52", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz", - "integrity": "sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==", + "version": "18.0.25", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", + "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", "devOptional": true, "dependencies": { "@types/prop-types": "*", @@ -4568,12 +4546,12 @@ } }, "node_modules/@types/react-dom": { - "version": "17.0.18", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz", - "integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==", + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", + "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", "dev": true, "dependencies": { - "@types/react": "^17" + "@types/react": "*" } }, "node_modules/@types/scheduler": { @@ -4588,17 +4566,6 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, - "node_modules/@types/styled-components": { - "version": "5.1.26", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz", - "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==", - "dev": true, - "dependencies": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, "node_modules/@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -5091,9 +5058,9 @@ "dev": true }, "node_modules/aria-hidden": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.1.tgz", - "integrity": "sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.2.tgz", + "integrity": "sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==", "dependencies": { "tslib": "^2.0.0" }, @@ -5332,26 +5299,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/babel-plugin-styled-components": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", - "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.16.0", - "@babel/helper-module-imports": "^7.16.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11", - "picomatch": "^2.3.0" - }, - "peerDependencies": { - "styled-components": ">= 2" - } - }, - "node_modules/babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" - }, "node_modules/babel-plugin-transform-react-remove-prop-types": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", @@ -5633,14 +5580,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001426", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz", @@ -6161,24 +6100,6 @@ "node": ">= 8" } }, - "node_modules/css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", - "engines": { - "node": ">=4" - } - }, - "node_modules/css-to-react-native": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "dependencies": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, "node_modules/csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", @@ -8252,6 +8173,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "peer": true, "dependencies": { "react-is": "^16.7.0" } @@ -8259,7 +8181,8 @@ "node_modules/hoist-non-react-statics/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true }, "node_modules/hosted-git-info": { "version": "4.1.0", @@ -10603,6 +10526,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -11291,6 +11215,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -11410,11 +11335,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11679,28 +11599,26 @@ } }, "node_modules/react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.0" }, "peerDependencies": { - "react": "17.0.2" + "react": "^18.2.0" } }, "node_modules/react-icons": { @@ -11711,12 +11629,6 @@ "react": "*" } }, - "node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true - }, "node_modules/react-refresh": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", @@ -12385,12 +12297,11 @@ "dev": true }, "node_modules/scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dependencies": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "node_modules/semver": { @@ -12438,11 +12349,6 @@ "node": ">=8" } }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12791,60 +12697,6 @@ "node": ">=4" } }, - "node_modules/styled-components": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", - "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==", - "hasInstallScript": true, - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/styled-components" - }, - "peerDependencies": { - "react": ">= 16.8.0", - "react-dom": ">= 16.8.0", - "react-is": ">= 16.8.0" - } - }, - "node_modules/styled-components/node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "node_modules/styled-components/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/styled-components/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/stylis": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", @@ -13816,17 +13668,13 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", "react-icons": "^4.3.1", - "styled-components": "^5.3.3", "uuid": "^8.3.2" }, "devDependencies": { "@types/lodash": "^4.14.179", - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.12", - "@types/styled-components": "^5.1.24", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", @@ -13834,6 +13682,10 @@ "typescript": "^4.5.4", "vite": "^3.0.5", "vite-plugin-eslint": "^1.7.0" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18" } } }, @@ -13945,6 +13797,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", + "dev": true, "requires": { "@babel/types": "^7.18.6" } @@ -15257,9 +15110,8 @@ "@tiptap/extension-underline": "^2.0.0-beta.25", "@tiptap/react": "^2.0.0-beta.114", "@types/lodash": "^4.14.179", - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.12", - "@types/styled-components": "^5.1.24", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", @@ -15268,10 +15120,7 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", "react-icons": "^4.3.1", - "styled-components": "^5.3.3", "typescript": "^4.5.4", "uuid": "^8.3.2", "vite": "^3.0.5", @@ -15282,13 +15131,13 @@ "version": "file:examples/editor", "requires": { "@blocknote/core": "^0.1.2", - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.12", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^1.0.7", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "typescript": "^4.5.4", "vite": "^3.0.5", "vite-plugin-eslint": "^1.7.0" @@ -15339,14 +15188,6 @@ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" }, - "@emotion/is-prop-valid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", - "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", - "requires": { - "@emotion/memoize": "^0.8.0" - } - }, "@emotion/memoize": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", @@ -15385,11 +15226,6 @@ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" }, - "@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, "@emotion/unitless": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", @@ -15444,30 +15280,30 @@ } }, "@floating-ui/core": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.1.tgz", - "integrity": "sha512-bO37brCPfteXQfFY0DyNDGB3+IMe4j150KFQcgJ5aBP295p9nBGeHEs/p0czrRbtlHq4Px/yoPXO/+dOCcF4uA==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.0.2.tgz", + "integrity": "sha512-Skfy0YS3NJ5nV9us0uuPN0HDk1Q4edljaOhRBJGDWs9EBa7ZVMYBHRFlhLvvmwEoaIM9BlH6QJFn9/uZg0bACg==" }, "@floating-ui/dom": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.3.tgz", - "integrity": "sha512-6H1kwjkOZKabApNtXRiYHvMmYJToJ1DV7rQ3xc/WJpOABhQIOJJOdz2AOejj8X+gcybaFmBpisVTZxBZAM3V0w==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.0.6.tgz", + "integrity": "sha512-kt/tg1oip9OAH1xjCTcx1OpcUpu9rjDw3GKJ/rEhUqhO7QyJWfrHU0DpLTNsH67+JyFL5Kv9X1utsXwKFVtyEQ==", "requires": { - "@floating-ui/core": "^1.0.1" + "@floating-ui/core": "^1.0.2" } }, "@floating-ui/react-dom": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.0.tgz", - "integrity": "sha512-uiOalFKPG937UCLm42RxjESTWUVpbbatvlphQAU6bsv+ence6IoVG8JOUZcy8eW81NkU+Idiwvx10WFLmR4MIg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-1.0.1.tgz", + "integrity": "sha512-UW0t1Gi8ikbDRr8cQPVcqIDMBwUEENe5V4wlHWdrJ5egFnRQFBV9JirauTBFI6S8sM1qFUC1i+qa3g87E6CLTw==", "requires": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.0.5" } }, "@floating-ui/react-dom-interactions": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.2.tgz", - "integrity": "sha512-KhF+UN+MVqUx1bG1fe0aAiBl1hbz07Uin6UW70mxwUDhaGpitM16CYvGri1EqGY4hnWK8TQknDSP8iQFOxjhsg==", + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom-interactions/-/react-dom-interactions-0.10.3.tgz", + "integrity": "sha512-UEHqdnzyoiWNU5az/tAljr9iXFzN18DcvpMqW+/cXz4FEhDEB1ogLtWldOWCujLerPBnSRocADALafelOReMpw==", "requires": { "@floating-ui/react-dom": "^1.0.0", "aria-hidden": "^1.1.3" @@ -17172,16 +17008,6 @@ "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==", "dev": true }, - "@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "dev": true, - "requires": { - "@types/react": "*", - "hoist-non-react-statics": "^3.3.0" - } - }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -17236,9 +17062,9 @@ "devOptional": true }, "@types/react": { - "version": "17.0.52", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.52.tgz", - "integrity": "sha512-vwk8QqVODi0VaZZpDXQCmEmiOuyjEFPY7Ttaw5vjM112LOq37yz1CDJGrRJwA1fYEq4Iitd5rnjd1yWAc/bT+A==", + "version": "18.0.25", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.25.tgz", + "integrity": "sha512-xD6c0KDT4m7n9uD4ZHi02lzskaiqcBxf4zi+tXZY98a04wvc0hi/TcCPC2FOESZi51Nd7tlUeOJY8RofL799/g==", "devOptional": true, "requires": { "@types/prop-types": "*", @@ -17247,12 +17073,12 @@ } }, "@types/react-dom": { - "version": "17.0.18", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.18.tgz", - "integrity": "sha512-rLVtIfbwyur2iFKykP2w0pl/1unw26b5td16d5xMgp7/yjTHomkyxPYChFoCr/FtEX1lN9wY6lFj1qvKdS5kDw==", + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.9.tgz", + "integrity": "sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==", "dev": true, "requires": { - "@types/react": "^17" + "@types/react": "*" } }, "@types/scheduler": { @@ -17267,17 +17093,6 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, - "@types/styled-components": { - "version": "5.1.26", - "resolved": "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.26.tgz", - "integrity": "sha512-KuKJ9Z6xb93uJiIyxo/+ksS7yLjS1KzG6iv5i78dhVg/X3u5t1H7juRWqVmodIdz6wGVaIApo1u01kmFRdJHVw==", - "dev": true, - "requires": { - "@types/hoist-non-react-statics": "*", - "@types/react": "*", - "csstype": "^3.0.2" - } - }, "@types/uuid": { "version": "8.3.4", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", @@ -17608,9 +17423,9 @@ "dev": true }, "aria-hidden": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.1.tgz", - "integrity": "sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.2.tgz", + "integrity": "sha512-6y/ogyDTk/7YAe91T3E2PR1ALVKyM2QbTio5HwM+N1Q6CMlCKhvClyIjkckBswa0f2xJhjsfzIGa1yVSe1UMVA==", "requires": { "tslib": "^2.0.0" } @@ -17787,23 +17602,6 @@ "@babel/helper-define-polyfill-provider": "^0.3.3" } }, - "babel-plugin-styled-components": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", - "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.16.0", - "@babel/helper-module-imports": "^7.16.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "lodash": "^4.17.11", - "picomatch": "^2.3.0" - } - }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" - }, "babel-plugin-transform-react-remove-prop-types": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz", @@ -18010,11 +17808,6 @@ "quick-lru": "^4.0.1" } }, - "camelize": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", - "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" - }, "caniuse-lite": { "version": "1.0.30001426", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001426.tgz", @@ -18414,21 +18207,6 @@ "which": "^2.0.1" } }, - "css-color-keywords": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", - "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" - }, - "css-to-react-native": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", - "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", - "requires": { - "camelize": "^1.0.0", - "css-color-keywords": "^1.0.0", - "postcss-value-parser": "^4.0.2" - } - }, "csstype": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", @@ -19904,6 +19682,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "peer": true, "requires": { "react-is": "^16.7.0" }, @@ -19911,7 +19690,8 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "peer": true } } }, @@ -21691,7 +21471,8 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true }, "object-inspect": { "version": "1.12.2", @@ -22190,7 +21971,8 @@ "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true }, "pify": { "version": "5.0.0", @@ -22263,11 +22045,6 @@ "source-map-js": "^1.0.2" } }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -22486,22 +22263,20 @@ "dev": true }, "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "requires": { "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" + "scheduler": "^0.23.0" } }, "react-icons": { @@ -22510,12 +22285,6 @@ "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==", "requires": {} }, - "react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "peer": true - }, "react-refresh": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", @@ -23011,12 +22780,11 @@ "dev": true }, "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "loose-envify": "^1.1.0" } }, "semver": { @@ -23054,11 +22822,6 @@ "kind-of": "^6.0.2" } }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -23329,43 +23092,6 @@ "through": "^2.3.4" } }, - "styled-components": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", - "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/traverse": "^7.4.5", - "@emotion/is-prop-valid": "^1.1.0", - "@emotion/stylis": "^0.8.4", - "@emotion/unitless": "^0.7.4", - "babel-plugin-styled-components": ">= 1.12.0", - "css-to-react-native": "^3.0.0", - "hoist-non-react-statics": "^3.0.0", - "shallowequal": "^1.1.0", - "supports-color": "^5.5.0" - }, - "dependencies": { - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "stylis": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", diff --git a/package.json b/package.json index 5f1970b7f7..3b492f67fb 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "postpublish": "rm -rf packages/core/README.md" }, "overrides": { - "react": "^17.0.2", - "react-dom": "^17.0.2", + "react": "18.2.0", + "react-dom": "18.2.0", "prosemirror-view": "1.26.2" } } diff --git a/packages/core/package.json b/packages/core/package.json index 26f4cd9119..b0b301e1bb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -71,21 +71,17 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", "react-icons": "^4.3.1", - "styled-components": "^5.3.3", "uuid": "^8.3.2" }, - "overrides": { - "react-dom": "$react-dom", - "react": "$react" + "peerDependencies": { + "react": "^18", + "react-dom": "^18" }, "devDependencies": { "@types/lodash": "^4.14.179", - "@types/react": "^17.0.39", - "@types/react-dom": "^17.0.12", - "@types/styled-components": "^5.1.24", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx index 0ee37bcff1..3950de41b9 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx @@ -1,7 +1,7 @@ import { MantineProvider } from "@mantine/core"; import { Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { BlockNoteTheme } from "../../BlockNoteTheme"; import rootStyles from "../../root.module.css"; import { createBubbleMenuPlugin } from "./BubbleMenuPlugin"; @@ -16,11 +16,11 @@ export const BubbleMenuExtension = Extension.create<{}>({ addProseMirrorPlugins() { const element = document.createElement("div"); element.className = rootStyles.bnRoot; - ReactDOM.render( + const root = createRoot(element); + root.render( - , - element + ); return [ createBubbleMenuPlugin({ diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx index 6bc1be5508..a8e2a9b4c9 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx @@ -1,12 +1,12 @@ -import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state"; +import { MantineProvider } from "@mantine/core"; import { Node } from "prosemirror-model"; +import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state"; import * as pv from "prosemirror-view"; import { EditorView } from "prosemirror-view"; -import ReactDOM from "react-dom"; -import { DragHandle } from "./components/DragHandle"; -import { MantineProvider } from "@mantine/core"; +import { createRoot, Root } from "react-dom/client"; import { BlockNoteTheme } from "../../BlockNoteTheme"; import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection"; +import { DragHandle } from "./components/DragHandle"; const serializeForClipboard = (pv as any).__serializeForClipboard; // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799 @@ -91,10 +91,7 @@ function blockPositionFromCoords( return null; } -function blockPositionsFromSelection( - selection: Selection, - doc: Node -) { +function blockPositionsFromSelection(selection: Selection, doc: Node) { // Absolute positions just before the first block spanned by the selection, and just after the last block. Having the // selection start and end just before and just after the target blocks ensures no whitespace/line breaks are left // behind after dragging & dropping them. @@ -222,6 +219,7 @@ function dragStart(e: DragEvent, view: EditorView) { export const createDraggableBlocksPlugin = () => { let dropElement: HTMLElement | undefined; + let dropElementRoot: Root | undefined; const WIDTH = 48; @@ -255,7 +253,7 @@ export const createDraggableBlocksPlugin = () => { dragStart(e, editorView) ); dropElement.addEventListener("dragend", () => unsetDragImage()); - + dropElementRoot = createRoot(dropElement); return { // update(view, prevState) {}, destroy() { @@ -264,6 +262,7 @@ export const createDraggableBlocksPlugin = () => { } dropElement.parentNode!.removeChild(dropElement); dropElement = undefined; + dropElementRoot = undefined; }, }; }, @@ -282,12 +281,12 @@ export const createDraggableBlocksPlugin = () => { // return true; // }, handleKeyDown(_view, _event) { - if (!dropElement) { + if (!dropElementRoot) { throw new Error("unexpected"); } menuShown = false; addClicked = false; - ReactDOM.render(<>, dropElement); + dropElementRoot.render(<>); return false; }, handleDOMEvents: { @@ -304,16 +303,16 @@ export const createDraggableBlocksPlugin = () => { return true; }, mousedown(_view, _event: any) { - if (!dropElement) { + if (!dropElementRoot) { throw new Error("unexpected"); } menuShown = false; addClicked = false; - ReactDOM.render(<>, dropElement); + dropElementRoot.render(<>); return false; }, mousemove(view, event: any) { - if (!dropElement) { + if (!dropElementRoot || !dropElement) { throw new Error("unexpected"); } @@ -355,7 +354,7 @@ export const createDraggableBlocksPlugin = () => { dropElement.style.left = left + "px"; dropElement.style.top = rect.top + "px"; - ReactDOM.render( + dropElementRoot.render( { view={view} coords={coords} /> - , - dropElement + ); return true; }, diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index cd8a62e1ae..a64670d555 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -3,7 +3,7 @@ import Tippy from "@tippyjs/react"; import { getMarkRange } from "@tiptap/core"; import { Mark, ResolvedPos } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; -import ReactDOM from "react-dom"; +import { createRoot } from "react-dom/client"; import { BlockNoteTheme } from "../../BlockNoteTheme"; import { HyperlinkMenu } from "./menus/HyperlinkMenu"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); @@ -12,6 +12,7 @@ export const createHyperlinkMenuPlugin = () => { // as we always use Tippy appendTo(document.body), we can just create an element // that we use for ReactDOM, but it isn't used anywhere (except by React internally) const fakeRenderTarget = document.createElement("div"); + const fakeRenderTargetRoot = createRoot(fakeRenderTarget); let hoveredLink: HTMLAnchorElement | undefined; let menuState: "cursor-based" | "mouse-based" | "hidden" = "hidden"; @@ -27,7 +28,7 @@ export const createHyperlinkMenuPlugin = () => { // don't show menu when we have an active selection if (menuState !== "hidden") { menuState = "hidden"; - ReactDOM.render(<>, fakeRenderTarget); + fakeRenderTargetRoot.render(<>); } return; } @@ -62,7 +63,7 @@ export const createHyperlinkMenuPlugin = () => { // if the cursor moves way if (menuState === "cursor-based") { menuState = "hidden"; - ReactDOM.render(<>, fakeRenderTarget); + fakeRenderTargetRoot.render(<>); } return; } @@ -86,7 +87,7 @@ export const createHyperlinkMenuPlugin = () => { // A URL has to begin with http(s):// to be interpreted as an absolute path const editHandler = (href: string, text: string) => { menuState = "hidden"; - ReactDOM.render(<>, fakeRenderTarget); + fakeRenderTargetRoot.render(<>); // update the mark with new href (foundLinkMark as any).attrs = { ...foundLinkMark.attrs, href }; // TODO: invalid assign to attrs @@ -133,7 +134,7 @@ export const createHyperlinkMenuPlugin = () => { ); - ReactDOM.render(hyperlinkMenu, fakeRenderTarget); + fakeRenderTargetRoot.render(hyperlinkMenu); menuState = basedOnCursorPos ? "cursor-based" : "mouse-based"; }, }; From e6873385d78f5efdb4029ef3abc00b5320ecc2a5 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 8 Dec 2022 13:43:12 +0100 Subject: [PATCH 02/55] Maybe fixed React error in build --- package-lock.json | 4 ++++ packages/core/package.json | 2 ++ 2 files changed, 6 insertions(+) diff --git a/package-lock.json b/package-lock.json index a040621796..5d34b07529 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13668,6 +13668,8 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-icons": "^4.3.1", "uuid": "^8.3.2" }, @@ -15120,6 +15122,8 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-icons": "^4.3.1", "typescript": "^4.5.4", "uuid": "^8.3.2", diff --git a/packages/core/package.json b/packages/core/package.json index f91c510316..41142850f6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -72,6 +72,8 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-icons": "^4.3.1", "uuid": "^8.3.2" }, From a25032a192994ef5ac7d26505df56779cde0f48f Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 13 Dec 2022 12:48:52 +0100 Subject: [PATCH 03/55] Added early implementation of bubble menu & editor in separate react package --- examples/editor/src/App.tsx | 27 ---- examples/editor/src/main.tsx | 31 ++-- packages/core/src/BlockNoteEditor.ts | 60 +++++++ packages/core/src/MenuFunctions.ts | 130 +++++++++++++++ .../BubbleMenu/BubbleMenuExtension.tsx | 24 +-- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 143 +++++++---------- packages/react/package.json | 44 +++++ packages/react/src/BlockNoteTheme.ts | 150 ++++++++++++++++++ .../src/BubbleMenu/BubbleMenuFactory.tsx | 52 ++++++ .../src/BubbleMenu/components}/BubbleMenu.tsx | 147 +++++++++++------ .../components}/LinkToolbarButton.tsx | 4 +- .../components/EditHyperlinkMenu.tsx | 44 +++++ .../components/EditHyperlinkMenuItem.tsx | 34 ++++ .../components/EditHyperlinkMenuItemIcon.tsx | 31 ++++ .../components/EditHyperlinkMenuItemInput.tsx | 40 +++++ .../components/HoverHyperlinkMenu.tsx | 37 +++++ packages/react/src/editor/useEditor.ts | 51 ++++++ .../src/shared/components/toolbar/Toolbar.tsx | 10 ++ .../components/toolbar/ToolbarButton.tsx | 56 +++++++ .../components/toolbar/ToolbarDropdown.tsx | 35 ++++ .../toolbar/ToolbarDropdownItem.tsx | 35 ++++ .../toolbar/ToolbarDropdownTarget.tsx | 31 ++++ .../tooltip/TooltipContent.module.css | 15 ++ .../components/tooltip/TooltipContent.tsx | 23 +++ packages/react/src/tsconfig.json | 24 +++ packages/react/src/utils.ts | 43 +++++ 26 files changed, 1124 insertions(+), 197 deletions(-) delete mode 100644 examples/editor/src/App.tsx create mode 100644 packages/core/src/BlockNoteEditor.ts create mode 100644 packages/core/src/MenuFunctions.ts create mode 100644 packages/react/package.json create mode 100644 packages/react/src/BlockNoteTheme.ts create mode 100644 packages/react/src/BubbleMenu/BubbleMenuFactory.tsx rename packages/{core/src/extensions/BubbleMenu/component => react/src/BubbleMenu/components}/BubbleMenu.tsx (64%) rename packages/{core/src/extensions/BubbleMenu/component => react/src/BubbleMenu/components}/LinkToolbarButton.tsx (91%) create mode 100644 packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx create mode 100644 packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx create mode 100644 packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx create mode 100644 packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx create mode 100644 packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx create mode 100644 packages/react/src/editor/useEditor.ts create mode 100644 packages/react/src/shared/components/toolbar/Toolbar.tsx create mode 100644 packages/react/src/shared/components/toolbar/ToolbarButton.tsx create mode 100644 packages/react/src/shared/components/toolbar/ToolbarDropdown.tsx create mode 100644 packages/react/src/shared/components/toolbar/ToolbarDropdownItem.tsx create mode 100644 packages/react/src/shared/components/toolbar/ToolbarDropdownTarget.tsx create mode 100644 packages/react/src/shared/components/tooltip/TooltipContent.module.css create mode 100644 packages/react/src/shared/components/tooltip/TooltipContent.tsx create mode 100644 packages/react/src/tsconfig.json create mode 100644 packages/react/src/utils.ts diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx deleted file mode 100644 index 20a635af92..0000000000 --- a/examples/editor/src/App.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// import logo from './logo.svg' -import { EditorContent, useEditor } from "@blocknote/core"; -import "@blocknote/core/style.css"; -import { Editor } from "@tiptap/core"; -import styles from "./App.module.css"; - -type WindowWithProseMirror = Window & - typeof globalThis & { ProseMirror: Editor }; - -function App() { - const editor = useEditor({ - onUpdate: ({ editor }) => { - console.log(editor.getJSON()); - (window as WindowWithProseMirror).ProseMirror = editor; // Give tests a way to get editor instance - }, - editorProps: { - attributes: { - class: styles.editor, - "data-test": "editor", - }, - }, - }); - - return ; -} - -export default App; diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 0808f1a4d8..42ab5780e2 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,13 +1,24 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import App from "./App"; import "./index.css"; +import styles from "./App.module.css"; +import { mountBlockNoteEditor } from "../../../packages/core/src/BlockNoteEditor"; +import { BubbleMenuFactory } from "../../../packages/react/src/BubbleMenu/BubbleMenuFactory"; -window.React = React; +// type WindowWithProseMirror = Window & +// typeof globalThis & { ProseMirror: Editor }; -const root = createRoot(document.getElementById("root")!); -root.render( - - - -); +mountBlockNoteEditor({ + menus: { + bubbleMenuFactory: BubbleMenuFactory, + }, + element: document.getElementById("root")!, + onUpdate: ({ editor }) => { + console.log(editor.getJSON()); + (window as any).ProseMirror = editor; // Give tests a way to get editor instance + }, + editorProps: { + attributes: { + class: styles.editor, + "data-test": "editor", + }, + }, +}); diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts new file mode 100644 index 0000000000..e5f1f18ff7 --- /dev/null +++ b/packages/core/src/BlockNoteEditor.ts @@ -0,0 +1,60 @@ +import { Editor, EditorOptions } from "@tiptap/core"; + +import { getBlockNoteExtensions } from "./BlockNoteExtensions"; +import styles from "./editor.module.css"; +import rootStyles from "./root.module.css"; + +type BlockNoteEditorOptions = EditorOptions & { + enableBlockNoteExtensions: boolean; + disableHistoryExtension: boolean; + menus: { + bubbleMenuFactory: (editor: Editor) => HTMLElement; + }; +}; + +const blockNoteExtensions = getBlockNoteExtensions(); + +const blockNoteOptions = { + enableInputRules: true, + enablePasteRules: true, + enableCoreExtensions: false, +}; + +export const mountBlockNoteEditor = ( + options: Partial = {} +) => { + const extensions = options.disableHistoryExtension + ? blockNoteExtensions.filter((e) => e.name !== "history") + : blockNoteExtensions; + + extensions.map((extension) => { + if (extension.name === "BubbleMenuExtension") { + return extension.configure({ + bubbleMenuFactory: options.menus?.bubbleMenuFactory, + }); + } + + return extension; + }); + + const tiptapOptions = { + ...blockNoteOptions, + ...options, + extensions: + options.enableBlockNoteExtensions === false + ? options.extensions + : [...(options.extensions || []), ...extensions], + editorProps: { + attributes: { + ...(options.editorProps?.attributes || {}), + class: [ + styles.bnEditor, + rootStyles.bnRoot, + (options.editorProps?.attributes as any)?.class || "", + ].join(" "), + }, + }, + }; + + return new Editor(tiptapOptions); +}; diff --git a/packages/core/src/MenuFunctions.ts b/packages/core/src/MenuFunctions.ts new file mode 100644 index 0000000000..f188f91d05 --- /dev/null +++ b/packages/core/src/MenuFunctions.ts @@ -0,0 +1,130 @@ +import { Editor } from "@tiptap/core"; + +// Maybe useful later to not have to pass entire editor instance to menu factories? +export type BubbleMenuFunctionTypes = { + styles: { + boldActive: () => boolean; + toggleBold: () => void; + + italicActive: () => boolean; + toggleItalic: () => void; + + underlineActive: () => boolean; + toggleUnderline: () => void; + + strikeActive: () => boolean; + toggleStrike: () => void; + + hyperlinkActive: () => boolean; + hyperlinkUrl: () => string; + hyperlinkText: () => string; + addHyperlink: (url: string, text?: string) => void; + removeHyperlink: () => void; + }; + blockTypes: { + paragraphActive: () => boolean; + setParagraph: () => void; + + headingActive: () => boolean; + headingLevel: () => string; + setHeading: (level: string) => void; + + listItemActive: () => boolean; + listItemType: () => string; + setListItem: (type: string) => void; + }; +}; + +export function getBubbleMenuFunctions( + editor: Editor +): BubbleMenuFunctionTypes { + return { + styles: { + boldActive: () => editor.isActive("bold"), + toggleBold: () => { + editor.view.focus(); + editor.commands.toggleBold(); + }, + italicActive: () => editor.isActive("italic"), + toggleItalic: () => { + editor.view.focus(); + editor.commands.toggleItalic(); + }, + underlineActive: () => editor.isActive("underline"), + toggleUnderline: () => { + editor.view.focus(); + editor.commands.toggleUnderline(); + }, + strikeActive: () => editor.isActive("strike"), + toggleStrike: () => { + editor.view.focus(); + editor.commands.toggleStrike(); + }, + hyperlinkActive: () => editor.isActive("link"), + hyperlinkUrl: () => editor.getAttributes("link").href, + hyperlinkText: () => { + const { from, to } = editor.state.selection; + + return editor.state.doc.textBetween(from, to); + }, + addHyperlink: (url: string, text?: string) => { + if (url === "") { + return; + } + + let { from, to } = editor.state.selection; + + if (!text) { + text = editor.state.doc.textBetween(from, to); + } + + const mark = editor.schema.mark("link", { href: url }); + + editor.view.dispatch( + editor.view.state.tr + .insertText(text, from, to) + .addMark(from, from + text.length, mark) + ); + }, + }, + blockTypes: { + paragraphActive: () => + editor.state.selection.$from.node().type.name === "textContent", + setParagraph: () => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "textContent" + ); + }, + headingActive: () => + editor.state.selection.$from.node().type.name === "headingContent", + headingLevel: () => + editor.state.selection.$from.node().attrs["headingLevel"], + setHeading: (level: string = "1") => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "headingContent", + { + headingLevel: level, + } + ); + }, + listItemActive: () => + editor.state.selection.$from.node().type.name === "listItemContent", + listItemType: () => + editor.state.selection.$from.node().attrs["listItemType"], + setListItem: (type: string = "unordered") => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "listItemContent", + { + listItemType: type, + } + ); + }, + }, + }; +} diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx index 3950de41b9..7cc5b38483 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx @@ -1,35 +1,21 @@ -import { MantineProvider } from "@mantine/core"; -import { Extension } from "@tiptap/core"; +import { Editor, Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; -import { createRoot } from "react-dom/client"; -import { BlockNoteTheme } from "../../BlockNoteTheme"; -import rootStyles from "../../root.module.css"; import { createBubbleMenuPlugin } from "./BubbleMenuPlugin"; -import { BubbleMenu } from "./component/BubbleMenu"; /** * The menu that is displayed when selecting a piece of text. */ -export const BubbleMenuExtension = Extension.create<{}>({ +export const BubbleMenuExtension = Extension.create<{ + bubbleMenuFactory: (editor: Editor) => HTMLElement; +}>({ name: "BubbleMenuExtension", addProseMirrorPlugins() { - const element = document.createElement("div"); - element.className = rootStyles.bnRoot; - const root = createRoot(element); - root.render( - - - - ); return [ createBubbleMenuPlugin({ editor: this.editor, - element, + bubbleMenuFactory: this.editor.options.menus.bubbleMenuFactory, pluginKey: new PluginKey("BubbleMenuPlugin"), - tippyOptions: { - appendTo: document.body, - }, }), ]; }, diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index ed87cde122..3a3523a9fa 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -1,20 +1,13 @@ -import { - Editor, - isNodeSelection, - isTextSelection, - posToDOMRect, -} from "@tiptap/core"; +import { Editor, isTextSelection } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import tippy, { Instance, Props } from "tippy.js"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files export interface BubbleMenuPluginProps { pluginKey: PluginKey | string; editor: Editor; - element: HTMLElement; - tippyOptions?: Partial; + bubbleMenuFactory: (editor: Editor) => HTMLElement; shouldShow?: | ((props: { editor: Editor; @@ -34,7 +27,9 @@ export type BubbleMenuViewProps = BubbleMenuPluginProps & { export class BubbleMenuView { public editor: Editor; - public element: HTMLElement; + public bubbleMenuFactory: (editor: Editor) => HTMLElement; + + public bubbleMenuElement: HTMLElement | undefined; public view: EditorView; @@ -42,10 +37,6 @@ export class BubbleMenuView { public preventShow = false; - public tippy: Instance | undefined; - - public tippyOptions?: Partial; - public shouldShow: Exclude = ({ view, state, @@ -70,32 +61,19 @@ export class BubbleMenuView { constructor({ editor, - element, + bubbleMenuFactory, view, - tippyOptions = {}, shouldShow, }: BubbleMenuViewProps) { this.editor = editor; - this.element = element; + this.bubbleMenuFactory = bubbleMenuFactory; this.view = view; if (shouldShow) { this.shouldShow = shouldShow; } - this.element.addEventListener("mousedown", this.mousedownHandler, { - capture: true, - }); - this.view.dom.addEventListener("mousedown", this.viewMousedownHandler); - this.view.dom.addEventListener("mouseup", this.viewMouseupHandler); - this.view.dom.addEventListener("dragstart", this.dragstartHandler); - - this.editor.on("focus", this.focusHandler); - this.editor.on("blur", this.blurHandler); - this.tippyOptions = tippyOptions; - // Detaches menu content from its current parent - this.element.remove(); - this.element.style.visibility = "visible"; + this.addEditorListeners(); } mousedownHandler = () => { @@ -112,7 +90,7 @@ export class BubbleMenuView { }; dragstartHandler = () => { - this.hide(); + this.destroy(); }; focusHandler = () => { @@ -129,56 +107,26 @@ export class BubbleMenuView { if ( event?.relatedTarget && - this.element.parentNode?.contains(event.relatedTarget as Node) + this.bubbleMenuElement?.parentNode?.contains(event.relatedTarget as Node) ) { return; } - this.hide(); + this.destroy(); }; - createTooltip() { - const { element: editorElement } = this.editor.options; - const editorIsAttached = !!editorElement.parentElement; - - if (this.tippy || !editorIsAttached) { - return; - } - - this.tippy = tippy(editorElement, { - duration: 0, - getReferenceClientRect: null, - content: this.element, - interactive: true, - trigger: "manual", - placement: "top", - hideOnClick: "toggle", - ...this.tippyOptions, - }); - - // maybe we have to hide tippy on its own blur event as well - if (this.tippy.popper.firstChild) { - (this.tippy.popper.firstChild as HTMLElement).addEventListener( - "blur", - (event) => { - this.blurHandler({ event }); - } - ); - } - } - update(view: EditorView, oldState?: EditorState) { + console.log("UPDATING"); const { state, composing } = view; const { doc, selection } = state; const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); if (composing || isSame) { + console.log("NOT COMPOSING OR SAME"); return; } - this.createTooltip(); - // support for CellSelections const { ranges } = selection; const from = Math.min(...ranges.map((range) => range.$from.pos)); @@ -194,44 +142,63 @@ export class BubbleMenuView { }); if (!shouldShow || this.preventShow) { - this.hide(); + console.log("SHOULDN'T SHOW OR PREVENT SHOW"); + !shouldShow && console.log("SHOULDN'T SHOW"); + this.preventShow && console.log("PREVENT SHOW"); + + this.destroy(); return; } - this.tippy?.setProps({ - getReferenceClientRect: () => { - if (isNodeSelection(state.selection)) { - const node = view.nodeDOM(from) as HTMLElement; + console.log("SHOW"); + this.create(); + } - if (node) { - return node.getBoundingClientRect(); - } + create() { + if (!this.bubbleMenuElement) { + this.bubbleMenuElement = this.bubbleMenuFactory(this.editor); + this.bubbleMenuElement.style.visibility = "visible"; + this.bubbleMenuElement.addEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, } - - return posToDOMRect(view, from, to); - }, - }); - - this.show(); + ); + } } - show() { - this.tippy?.show(); + destroy() { + if (this.bubbleMenuElement) { + this.bubbleMenuElement.removeEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, + } + ); + this.bubbleMenuElement.remove(); + this.bubbleMenuElement = undefined; + } } - hide() { - this.tippy?.hide(); + addEditorListeners() { + this.view.dom.addEventListener("mousedown", this.viewMousedownHandler); + this.view.dom.addEventListener("mouseup", this.viewMouseupHandler); + this.view.dom.addEventListener("dragstart", this.dragstartHandler); + + this.editor.on("focus", this.focusHandler); + this.editor.on("blur", this.blurHandler); } - destroy() { - this.tippy?.destroy(); - this.element.removeEventListener("mousedown", this.mousedownHandler, { - capture: true, - }); + removeEditorListeners() { + this.destroy(); + this.view.dom.removeEventListener("mousedown", this.viewMousedownHandler); this.view.dom.removeEventListener("mouseup", this.viewMouseupHandler); this.view.dom.removeEventListener("dragstart", this.dragstartHandler); + this.editor.off("focus", this.focusHandler); this.editor.off("blur", this.blurHandler); } diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000000..f67e65a9c4 --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,44 @@ +{ + "name": "@blocknote/react", + "homepage": "https://github.com/yousefed/blocknote", + "private": false, + "version": "0.1.2", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release", + "preview": "vite preview", + "lint": "eslint ../menus/src --max-warnings 0" + }, + "dependencies": { + "@blocknote/core": "^0.1.2", + "@tippyjs/react": "^4.2.6", + "@tiptap/react": "^2.0.0-beta.207", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "eslint": "^8.10.0", + "eslint-config-react-app": "^7.0.0", + "prettier": "^2.7.1", + "typescript": "^4.5.4", + "vite": "^3.0.5", + "vite-plugin-eslint": "^1.7.0" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ], + "rules": { + "curly": 1 + } + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "gitHead": "37614ab348dcc7faa830a9a88437b37197a2162d" +} diff --git a/packages/react/src/BlockNoteTheme.ts b/packages/react/src/BlockNoteTheme.ts new file mode 100644 index 0000000000..61b69bd9fc --- /dev/null +++ b/packages/react/src/BlockNoteTheme.ts @@ -0,0 +1,150 @@ +import { MantineThemeOverride } from "@mantine/core"; + +export const BlockNoteTheme: MantineThemeOverride = { + activeStyles: { + // Removes button press effect. + transform: "none", + }, + colorScheme: "light", + colors: { + brandFinal: [ + "#F6F6F8", + "#ECEDF0", + "#DFE1E6", + "#C2C7D0", + "#A6ADBA", + "#8993A4", + "#6D798F", + "#505F79", + "#344563", + "#172B4D", + ], + }, + components: { + Menu: { + styles: (theme) => ({ + dropdown: { + backgroundColor: "white", + boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`, + border: `1px solid ${theme.colors.brandFinal[1]}`, + borderRadius: "6px", + padding: "2px", + }, + }), + }, + DragHandleMenu: { + styles: (theme) => ({ + root: { + ".mantine-Menu-item": { + color: theme.colors.brandFinal, + fontSize: "12px", + height: "34px", + }, + }, + }), + }, + EditHyperlinkMenu: { + styles: (theme) => ({ + root: { + backgroundColor: "white", + boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`, + border: `1px solid ${theme.colors.brandFinal[1]}`, + borderRadius: "6px", + gap: "4px", + minWidth: "145px", + padding: "2px", + // Row + ".mantine-Group-root": { + flexWrap: "nowrap", + gap: "8px", + paddingInline: "6px", + // Row icon + ".mantine-Container-root": { + color: theme.colors.brandFinal, + display: "flex", + justifyContent: "center", + padding: "0", + width: "fit-content", + }, + // Row input field + ".mantine-TextInput-root": { + background: "transparent", + width: "300px", + ".mantine-TextInput-wrapper": { + ".mantine-TextInput-input": { + fontSize: "12px", + border: 0, + padding: 0, + }, + }, + }, + }, + }, + }), + }, + Toolbar: { + styles: (theme) => ({ + root: { + backgroundColor: "white", + boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`, + border: `1px solid ${theme.colors.brandFinal[1]}`, + borderRadius: "6px", + flexWrap: "nowrap", + gap: "2px", + padding: "2px", + width: "fit-content", + // Button (including dropdown target) + ".mantine-UnstyledButton-root": { + borderRadius: "4px", + }, + // Dropdown + ".mantine-Menu-dropdown": { + // Dropdown item + ".mantine-Menu-item": { + color: theme.colors.brandFinal, + fontSize: "12px", + height: "34px", + ".mantine-Menu-itemRightSection": { + paddingLeft: "5px", + }, + }, + }, + }, + }), + }, + SuggestionList: { + styles: (theme) => ({ + root: { + // ...theme.other.defaultMenuStyles(theme), + ".mantine-Menu-item": { + // Icon + ".mantine-Menu-itemIcon": { + padding: "8px", + border: `1px solid ${theme.colors.brandFinal[2]}`, + backgroundColor: theme.colors.brandFinal[0], + borderRadius: "4px", + color: theme.colors.brandFinal, + }, + // Text + ".mantine-Menu-itemLabel": { + color: theme.colors.brandFinal, + paddingRight: "16px", + ".mantine-Stack-root": { + gap: "0", + }, + }, + // Badge (keyboard shortcut) + ".mantine-Menu-itemRightSection": { + ".mantine-Badge-root": { + border: `1px solid ${theme.colors.brandFinal[2]}`, + }, + }, + }, + }, + }), + }, + }, + fontFamily: "Inter", + primaryColor: "brandFinal", + primaryShade: 9, +}; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx new file mode 100644 index 0000000000..d8076fd23d --- /dev/null +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -0,0 +1,52 @@ +import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; +import { createRoot } from "react-dom/client"; +import { MantineProvider } from "@mantine/core"; +import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; +import { BubbleMenu } from "./components/BubbleMenu"; +import tippy from "tippy.js"; +import rootStyles from "../../../core/src/root.module.css"; + +export const BubbleMenuFactory = (editor: Editor) => { + const element = document.createElement("div"); + element.className = rootStyles.bnRoot; + const root = createRoot(element); + root.render( + + + + ); + + const { state } = editor.view; + const { selection } = state; + + // support for CellSelections + const { ranges } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const to = Math.max(...ranges.map((range) => range.$to.pos)); + + const getReferenceRect = () => { + if (isNodeSelection(state.selection)) { + const node = editor.view.nodeDOM(from) as HTMLElement; + + if (node) { + return node.getBoundingClientRect(); + } + } + + return posToDOMRect(editor.view, from, to); + }; + + const menu = tippy(document.body, { + duration: 0, + getReferenceClientRect: getReferenceRect, + content: element, + interactive: true, + trigger: "manual", + placement: "top", + hideOnClick: "toggle", + }); + + menu.show(); + + return element; +}; diff --git a/packages/core/src/extensions/BubbleMenu/component/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx similarity index 64% rename from packages/core/src/extensions/BubbleMenu/component/BubbleMenu.tsx rename to packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 315d6b967e..87e1e548e0 100644 --- a/packages/core/src/extensions/BubbleMenu/component/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -1,4 +1,5 @@ import { Editor } from "@tiptap/core"; +import { Node } from "prosemirror-model"; import { RiBold, RiH1, @@ -14,55 +15,87 @@ import { RiText, RiUnderline, } from "react-icons/ri"; -import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton"; -import { ToolbarDropdown } from "../../../shared/components/toolbar/ToolbarDropdown"; -import { Toolbar } from "../../../shared/components/toolbar/Toolbar"; -import { useEditorForceUpdate } from "../../../shared/hooks/useEditorForceUpdate"; -import { findBlock } from "../../Blocks/helpers/findBlock"; -import { formatKeyboardShortcut } from "../../../utils"; +import { ToolbarButton } from "../../shared/components/toolbar/ToolbarButton"; +import { ToolbarDropdown } from "../../shared/components/toolbar/ToolbarDropdown"; +import { Toolbar } from "../../shared/components/toolbar/Toolbar"; +import { useState } from "react"; +import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; -import { IconType } from "react-icons"; -import { Node } from "prosemirror-model"; -function getBlockName(blockContentNode: Node) { - if (blockContentNode.type.name === "textContent") { - return "Text"; - } +// TODO: add list options, indentation +export const BubbleMenu = (props: { editor: Editor }) => { + const getDropdownText = (node: Node) => { + if (node.type.name === "textContent") { + return "Text"; + } - if (blockContentNode.type.name === "headingContent") { - return "Heading " + blockContentNode.attrs["headingLevel"]; - } + if (node.type.name === "headingContent") { + return "Heading " + node.attrs["headingLevel"]; + } - if (blockContentNode.type.name === "listItemContent") { - return blockContentNode.attrs["listItemType"] === "unordered" - ? "Bullet List" - : "Numbered List"; - } + if (node.type.name === "listItemContent") { + if (node.attrs["listItemType"] === "unordered") { + return "Bullet List"; + } else { + return "Ordered List"; + } + } - return ""; -} + return undefined; + }; -// TODO: add list options, indentation -export const BubbleMenu = (props: { editor: Editor }) => { - useEditorForceUpdate(props.editor); + const getDropdownIcon = (node: Node) => { + if (node.type.name === "textContent") { + return RiText; + } + + if (node.type.name === "headingContent") { + if (node.attrs["headingLevel"] === "1") { + return RiH1; + } + + if (node.attrs["headingLevel"] === "2") { + return RiH2; + } - const selectedNode = props.editor.state.selection.$from.node(); - const currentBlockName = getBlockName(selectedNode); + if (node.attrs["headingLevel"] === "3") { + return RiH3; + } + } - const blockIconMap: Record = { - Text: RiText, - "Heading 1": RiH1, - "Heading 2": RiH2, - "Heading 3": RiH3, - "Bullet List": RiListUnordered, - "Numbered List": RiListOrdered, + if (node.type.name === "listItemContent") { + if (node.attrs["listItemType"] === "unordered") { + return RiListUnordered; + } else { + return RiListOrdered; + } + } + + return undefined; + }; + + const getActiveMarks = () => { + const activeMarks = new Set(); + + props.editor.isActive("bold") && activeMarks.add("bold"); + props.editor.isActive("italic") && activeMarks.add("italic"); + props.editor.isActive("underline") && activeMarks.add("underline"); + props.editor.isActive("strike") && activeMarks.add("strike"); + props.editor.isActive("link") && activeMarks.add("link"); + + return activeMarks; }; + const [selectedNode, setSelectedNode] = useState( + props.editor.state.selection.$from.node() + ); + const [selectedNodeMarks, setSelectedNodeMarks] = useState(getActiveMarks()); + return ( { @@ -72,6 +105,7 @@ export const BubbleMenu = (props: { editor: Editor }) => { props.editor.state.selection.from, "textContent" ); + setSelectedNode(props.editor.state.selection.$from.node()); }, text: "Text", icon: RiText, @@ -87,6 +121,7 @@ export const BubbleMenu = (props: { editor: Editor }) => { headingLevel: "1", } ); + setSelectedNode(props.editor.state.selection.$from.node()); }, text: "Heading 1", icon: RiH1, @@ -104,6 +139,7 @@ export const BubbleMenu = (props: { editor: Editor }) => { headingLevel: "2", } ); + setSelectedNode(props.editor.state.selection.$from.node()); }, text: "Heading 2", icon: RiH2, @@ -121,6 +157,7 @@ export const BubbleMenu = (props: { editor: Editor }) => { headingLevel: "3", } ); + setSelectedNode(props.editor.state.selection.$from.node()); }, text: "Heading 3", icon: RiH3, @@ -138,6 +175,7 @@ export const BubbleMenu = (props: { editor: Editor }) => { listItemType: "unordered", } ); + setSelectedNode(props.editor.state.selection.$from.node()); }, text: "Bullet List", icon: RiListUnordered, @@ -155,6 +193,7 @@ export const BubbleMenu = (props: { editor: Editor }) => { listItemType: "ordered", } ); + setSelectedNode(props.editor.state.selection.$from.node()); }, text: "Numbered List", icon: RiListOrdered, @@ -169,8 +208,9 @@ export const BubbleMenu = (props: { editor: Editor }) => { // Setting editor focus using a chained command instead causes bubble menu to flicker on click. props.editor.view.focus(); props.editor.commands.toggleBold(); + setSelectedNodeMarks(getActiveMarks()); }} - isSelected={props.editor.isActive("bold")} + isSelected={selectedNodeMarks.has("bold")} mainTooltip="Bold" secondaryTooltip={formatKeyboardShortcut("Mod+B")} icon={RiBold} @@ -179,8 +219,9 @@ export const BubbleMenu = (props: { editor: Editor }) => { onClick={() => { props.editor.view.focus(); props.editor.commands.toggleItalic(); + setSelectedNodeMarks(getActiveMarks()); }} - isSelected={props.editor.isActive("italic")} + isSelected={selectedNodeMarks.has("italic")} mainTooltip="Italic" secondaryTooltip={formatKeyboardShortcut("Mod+I")} icon={RiItalic} @@ -189,8 +230,9 @@ export const BubbleMenu = (props: { editor: Editor }) => { onClick={() => { props.editor.view.focus(); props.editor.commands.toggleUnderline(); + setSelectedNodeMarks(getActiveMarks()); }} - isSelected={props.editor.isActive("underline")} + isSelected={selectedNodeMarks.has("underline")} mainTooltip="Underline" secondaryTooltip={formatKeyboardShortcut("Mod+U")} icon={RiUnderline} @@ -199,8 +241,9 @@ export const BubbleMenu = (props: { editor: Editor }) => { onClick={() => { props.editor.view.focus(); props.editor.commands.toggleStrike(); + setSelectedNodeMarks(getActiveMarks()); }} - isSelected={props.editor.isActive("strike")} + isSelected={selectedNodeMarks.has("strike")} mainTooltip="Strike-through" secondaryTooltip={formatKeyboardShortcut("Mod+Shift+X")} icon={RiStrikethrough} @@ -209,6 +252,7 @@ export const BubbleMenu = (props: { editor: Editor }) => { onClick={() => { props.editor.view.focus(); props.editor.commands.sinkListItem("block"); + setSelectedNodeMarks(getActiveMarks()); }} isDisabled={!props.editor.can().sinkListItem("block")} mainTooltip="Indent" @@ -220,16 +264,18 @@ export const BubbleMenu = (props: { editor: Editor }) => { onClick={() => { props.editor.view.focus(); props.editor.commands.liftListItem("block"); + setSelectedNodeMarks(getActiveMarks()); }} isDisabled={ - !props.editor.can().command(({ state }) => { - const block = findBlock(state.selection); - if (!block) { - return false; - } - // If the depth is greater than 2 you can lift - return block.depth > 2; - }) + // !props.editor.can().command(({ state }) => { + // const block = findBlock(state.selection); + // if (!block) { + // return false; + // } + // // If the depth is greater than 2 you can lift + // return block.depth > 2; + // }) + true } mainTooltip="Decrease Indent" secondaryTooltip={formatKeyboardShortcut("Shift+Tab")} @@ -237,12 +283,11 @@ export const BubbleMenu = (props: { editor: Editor }) => { /> {/* void; +}; + +/** + * Menu which opens when editing an existing hyperlink or creating a new one. + * Provides input fields for setting the hyperlink URL and title. + */ +export const EditHyperlinkMenu = (props: EditHyperlinkMenuProps) => { + const [url, setUrl] = useState(props.url); + const [title, setTitle] = useState(props.text); + const { classes } = createStyles({ root: {} })(undefined, { + name: "EditHyperlinkMenu", + }); + + return ( + + setUrl(value)} + onSubmit={() => props.update(url, title)} + /> + setTitle(value)} + onSubmit={() => props.update(url, title)} + /> + + ); +}; diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx new file mode 100644 index 0000000000..ab6e3c16d0 --- /dev/null +++ b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx @@ -0,0 +1,34 @@ +import { IconType } from "react-icons"; +import { EditHyperlinkMenuItemIcon } from "./EditHyperlinkMenuItemIcon"; +import { EditHyperlinkMenuItemInput } from "./EditHyperlinkMenuItemInput"; +import { Group } from "@mantine/core"; + +export type EditHyperlinkMenuItemProps = { + icon: IconType; + mainIconTooltip: string; + secondaryIconTooltip?: string; + autofocus?: boolean; + placeholder?: string; + value?: string; + onChange: (value: string) => void; + onSubmit: () => void; +}; + +export function EditHyperlinkMenuItem(props: EditHyperlinkMenuItemProps) { + return ( + + + + + ); +} diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx new file mode 100644 index 0000000000..9d4936307d --- /dev/null +++ b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx @@ -0,0 +1,31 @@ +import { IconType } from "react-icons"; +import Tippy from "@tippyjs/react"; +import { TooltipContent } from "../../../shared/components/tooltip/TooltipContent"; +import { Container } from "@mantine/core"; + +export type EditHyperlinkMenuItemIconProps = { + icon: IconType; + mainTooltip: string; + secondaryTooltip?: string; +}; + +export function EditHyperlinkMenuItemIcon( + props: EditHyperlinkMenuItemIconProps +) { + const Icon = props.icon; + + return ( + + } + placement="left"> + + + + + ); +} diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx new file mode 100644 index 0000000000..961da494f0 --- /dev/null +++ b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx @@ -0,0 +1,40 @@ +import { KeyboardEvent, useEffect, useRef } from "react"; +import { TextInput } from "@mantine/core"; + +export type EditHyperlinkMenuItemInputProps = { + autofocus?: boolean; + placeholder?: string; + value?: string; + onChange: (value: string) => void; + onSubmit: () => void; +}; + +export function EditHyperlinkMenuItemInput( + props: EditHyperlinkMenuItemInputProps +) { + const inputRef = useRef(null); + + useEffect(() => { + setTimeout(() => { + props.autofocus && inputRef.current?.focus(); + }); + }, [props.autofocus]); + + function handleEnter(event: KeyboardEvent) { + if (event.key === "Enter") { + event.preventDefault(); + props.onSubmit(); + } + } + + return ( + props.onChange(event.currentTarget.value)} + onKeyDown={handleEnter} + placeholder={props.placeholder} + ref={inputRef} + /> + ); +} diff --git a/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx b/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx new file mode 100644 index 0000000000..acdaf8c9dc --- /dev/null +++ b/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx @@ -0,0 +1,37 @@ +import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; +import { Toolbar } from "../../../shared/components/toolbar/Toolbar"; +import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton"; + +type HoverHyperlinkMenuProps = { + url: string; + edit: () => void; + remove: () => void; +}; + +/** + * Menu which opens when hovering an existing hyperlink. + * Provides buttons for editing, opening, and removing the hyperlink. + */ +export const HoverHyperlinkMenu = (props: HoverHyperlinkMenuProps) => { + return ( + + + Edit Link + + { + window.open(props.url, "_blank"); + }} + icon={RiExternalLinkFill} + /> + + + ); +}; diff --git a/packages/react/src/editor/useEditor.ts b/packages/react/src/editor/useEditor.ts new file mode 100644 index 0000000000..6e5ed441a1 --- /dev/null +++ b/packages/react/src/editor/useEditor.ts @@ -0,0 +1,51 @@ +import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react"; + +import { DependencyList } from "react"; +import { getBlockNoteExtensions } from "../../../core/src/BlockNoteExtensions"; +import styles from "../../../core/src/editor.module.css"; +import rootStyles from "../../../core/src/root.module.css"; + +type BlockNoteEditorOptions = EditorOptions & { + enableBlockNoteExtensions: boolean; + disableHistoryExtension: boolean; +}; + +const blockNoteExtensions = getBlockNoteExtensions(); + +const blockNoteOptions = { + enableInputRules: true, + enablePasteRules: true, + enableCoreExtensions: false, +}; + +/** + * Main hook for importing a BlockNote editor into a react project + */ +export const useEditor = ( + options: Partial = {}, + deps: DependencyList = [] +) => { + const extensions = options.disableHistoryExtension + ? blockNoteExtensions.filter((e) => e.name !== "history") + : blockNoteExtensions; + + const tiptapOptions = { + ...blockNoteOptions, + ...options, + extensions: + options.enableBlockNoteExtensions === false + ? options.extensions + : [...(options.extensions || []), ...extensions], + editorProps: { + attributes: { + ...(options.editorProps?.attributes || {}), + class: [ + styles.bnEditor, + rootStyles.bnRoot, + (options.editorProps?.attributes as any)?.class || "", + ].join(" "), + }, + }, + }; + return useEditorTiptap(tiptapOptions, deps); +}; diff --git a/packages/react/src/shared/components/toolbar/Toolbar.tsx b/packages/react/src/shared/components/toolbar/Toolbar.tsx new file mode 100644 index 0000000000..df46517e30 --- /dev/null +++ b/packages/react/src/shared/components/toolbar/Toolbar.tsx @@ -0,0 +1,10 @@ +import { createStyles, Group } from "@mantine/core"; +import { ReactNode } from "react"; + +export const Toolbar = (props: { children: ReactNode }) => { + const { classes } = createStyles({ root: {} })(undefined, { + name: "Toolbar", + }); + + return {props.children}; +}; diff --git a/packages/react/src/shared/components/toolbar/ToolbarButton.tsx b/packages/react/src/shared/components/toolbar/ToolbarButton.tsx new file mode 100644 index 0000000000..3c12ec8f23 --- /dev/null +++ b/packages/react/src/shared/components/toolbar/ToolbarButton.tsx @@ -0,0 +1,56 @@ +import { ActionIcon, Button } from "@mantine/core"; +import Tippy from "@tippyjs/react"; +import { forwardRef } from "react"; +import { TooltipContent } from "../tooltip/TooltipContent"; +import { IconType } from "react-icons"; + +export type ToolbarButtonProps = { + onClick?: (e: React.MouseEvent) => void; + icon?: IconType; + mainTooltip: string; + secondaryTooltip?: string; + isSelected?: boolean; + children?: any; + isDisabled?: boolean; +}; + +/** + * Helper for basic buttons that show in the inline bubble menu. + */ +export const ToolbarButton = forwardRef((props: ToolbarButtonProps, ref) => { + const ButtonIcon = props.icon; + return ( + + } + trigger={"mouseenter"}> + {/*Creates an ActionIcon instead of a Button if only an icon is provided as content.*/} + {props.children ? ( + + ) : ( + + {ButtonIcon && } + + )} + + ); +}); diff --git a/packages/react/src/shared/components/toolbar/ToolbarDropdown.tsx b/packages/react/src/shared/components/toolbar/ToolbarDropdown.tsx new file mode 100644 index 0000000000..a97a6539a4 --- /dev/null +++ b/packages/react/src/shared/components/toolbar/ToolbarDropdown.tsx @@ -0,0 +1,35 @@ +import { Menu } from "@mantine/core"; +import { IconType } from "react-icons"; +import { ToolbarDropdownTarget } from "./ToolbarDropdownTarget"; +import { MouseEvent } from "react"; +import { ToolbarDropdownItem } from "./ToolbarDropdownItem"; + +export type ToolbarDropdownProps = { + text: string; + icon?: IconType; + items: Array<{ + onClick?: (e: MouseEvent) => void; + text: string; + icon?: IconType; + isSelected?: boolean; + children?: any; + isDisabled?: boolean; + }>; + children?: any; + isDisabled?: boolean; +}; + +export function ToolbarDropdown(props: ToolbarDropdownProps) { + return ( + + + + + + {props.items.map((item) => ( + + ))} + + + ); +} diff --git a/packages/react/src/shared/components/toolbar/ToolbarDropdownItem.tsx b/packages/react/src/shared/components/toolbar/ToolbarDropdownItem.tsx new file mode 100644 index 0000000000..bf60c106a8 --- /dev/null +++ b/packages/react/src/shared/components/toolbar/ToolbarDropdownItem.tsx @@ -0,0 +1,35 @@ +import { Menu } from "@mantine/core"; +import { IconType } from "react-icons"; +import { TiTick } from "react-icons/ti"; +import { MouseEvent } from "react"; + +export type ToolbarDropdownItemProps = { + onClick?: (e: MouseEvent) => void; + text: string; + icon?: IconType; + isSelected?: boolean; + children?: any; + isDisabled?: boolean; +}; + +export function ToolbarDropdownItem(props: ToolbarDropdownItemProps) { + const ItemIcon = props.icon; + + return ( + } + rightSection={ + props.isSelected ? ( + + ) : ( + // Ensures space for tick even if item isn't currently selected. +
+ ) + } + disabled={props.isDisabled}> + {props.text} + + ); +} diff --git a/packages/react/src/shared/components/toolbar/ToolbarDropdownTarget.tsx b/packages/react/src/shared/components/toolbar/ToolbarDropdownTarget.tsx new file mode 100644 index 0000000000..19b4c516c7 --- /dev/null +++ b/packages/react/src/shared/components/toolbar/ToolbarDropdownTarget.tsx @@ -0,0 +1,31 @@ +import { Button } from "@mantine/core"; +import { HiChevronDown } from "react-icons/hi"; +import { IconType } from "react-icons"; +import { forwardRef } from "react"; + +export type ToolbarDropdownTargetProps = { + text: string; + icon?: IconType; + isDisabled?: boolean; +}; + +export const ToolbarDropdownTarget = forwardRef< + HTMLButtonElement, + ToolbarDropdownTargetProps +>((props: ToolbarDropdownTargetProps, ref) => { + const { text, icon, isDisabled, ...others } = props; + + const TargetIcon = props.icon; + return ( + + ); +}); diff --git a/packages/react/src/shared/components/tooltip/TooltipContent.module.css b/packages/react/src/shared/components/tooltip/TooltipContent.module.css new file mode 100644 index 0000000000..c687d30ccf --- /dev/null +++ b/packages/react/src/shared/components/tooltip/TooltipContent.module.css @@ -0,0 +1,15 @@ +.tooltip { + color: var(--N40); + background-color: var(--N800); + box-shadow: 0 0 10px rgba(253, 254, 255, 0.8), + 0 0 3px rgba(253, 254, 255, 0.4); + border-radius: 2px; + font-size: smaller; + text-align: center; + padding: 4px; +} + +.secondaryText { + font-weight: 400; + opacity: 0.6; +} diff --git a/packages/react/src/shared/components/tooltip/TooltipContent.tsx b/packages/react/src/shared/components/tooltip/TooltipContent.tsx new file mode 100644 index 0000000000..2a57e841b8 --- /dev/null +++ b/packages/react/src/shared/components/tooltip/TooltipContent.tsx @@ -0,0 +1,23 @@ +import styles from "./TooltipContent.module.css"; + +/** + * Helper for the tooltip for inline bubble menu buttons. + * + * Often used to display a tooltip showing the command name + keyboard shortcut, e.g.: + * + * Bold + * Ctrl+B + * + * TODO: maybe use default Tippy styles instead? + */ +export const TooltipContent = (props: { + mainTooltip: string; + secondaryTooltip?: string; +}) => ( +
+
{props.mainTooltip}
+ {props.secondaryTooltip && ( +
{props.secondaryTooltip}
+ )} +
+); diff --git a/packages/react/src/tsconfig.json b/packages/react/src/tsconfig.json new file mode 100644 index 0000000000..8841d64b1c --- /dev/null +++ b/packages/react/src/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "jsx": "react-jsx", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "dist", + "declaration": true, + "declarationDir": "types", + "composite": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts new file mode 100644 index 0000000000..f43e3d30fe --- /dev/null +++ b/packages/react/src/utils.ts @@ -0,0 +1,43 @@ +import { useEffect, useState } from "react"; +import { Editor } from "@tiptap/core"; + +export const isAppleOS = () => + /Mac/.test(navigator.platform) || + (/AppleWebKit/.test(navigator.userAgent) && + /Mobile\/\w+/.test(navigator.userAgent)); + +export function formatKeyboardShortcut(shortcut: string) { + if (isAppleOS()) { + return shortcut.replace("Mod", "⌘"); + } else { + return shortcut.replace("Mod", "Ctrl"); + } +} + +function useForceUpdate() { + const [, setValue] = useState(0); + + return () => setValue((value) => value + 1); +} + +// This is a component that is similar to https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/useEditor.ts +// Use it to rerender a component whenever a transaction happens in the editor +export const useEditorForceUpdate = (editor: Editor) => { + const forceUpdate = useForceUpdate(); + + useEffect(() => { + const callback = () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + forceUpdate(); + }); + }); + }; + + editor.on("transaction", callback); + return () => { + editor.off("transaction", callback); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [editor]); +}; From 81867eecd68face3531916c047a044fb2440b64e Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 13 Dec 2022 19:53:58 +0100 Subject: [PATCH 04/55] Moved all hyperlink menus to react package --- examples/editor/src/main.tsx | 30 +++--- packages/core/src/BlockNoteEditor.ts | 27 ++++-- .../BubbleMenu/BubbleMenuExtension.tsx | 2 +- .../extensions/Hyperlinks/HyperlinkMark.tsx | 14 ++- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 92 +++++++++---------- .../Hyperlinks/menus/EditHyperlinkMenu.tsx | 44 --------- .../menus/EditHyperlinkMenuItem.tsx | 34 ------- .../menus/EditHyperlinkMenuItemIcon.tsx | 31 ------- .../menus/EditHyperlinkMenuItemInput.tsx | 40 -------- .../Hyperlinks/menus/HoverHyperlinkMenu.tsx | 37 -------- .../src/BubbleMenu/BubbleMenuFactory.tsx | 6 +- .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 51 ++++++++++ .../components}/HyperlinkMenu.tsx | 30 ++---- packages/react/src/editor/useEditor.ts | 8 +- 14 files changed, 157 insertions(+), 289 deletions(-) delete mode 100644 packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx delete mode 100644 packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx delete mode 100644 packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx delete mode 100644 packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx delete mode 100644 packages/core/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx create mode 100644 packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx rename packages/{core/src/extensions/Hyperlinks/menus => react/src/HyperlinkMenus/components}/HyperlinkMenu.tsx (55%) diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 42ab5780e2..16da024d18 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -2,23 +2,27 @@ import "./index.css"; import styles from "./App.module.css"; import { mountBlockNoteEditor } from "../../../packages/core/src/BlockNoteEditor"; import { BubbleMenuFactory } from "../../../packages/react/src/BubbleMenu/BubbleMenuFactory"; +import { HyperlinkMenuFactory } from "../../../packages/react/src/HyperlinkMenus/HyperlinkMenuFactory"; // type WindowWithProseMirror = Window & // typeof globalThis & { ProseMirror: Editor }; -mountBlockNoteEditor({ - menus: { +mountBlockNoteEditor( + { bubbleMenuFactory: BubbleMenuFactory, + hyperlinkMenuFactory: HyperlinkMenuFactory, }, - element: document.getElementById("root")!, - onUpdate: ({ editor }) => { - console.log(editor.getJSON()); - (window as any).ProseMirror = editor; // Give tests a way to get editor instance - }, - editorProps: { - attributes: { - class: styles.editor, - "data-test": "editor", + { + element: document.getElementById("root")!, + onUpdate: ({ editor }) => { + console.log(editor.getJSON()); + (window as any).ProseMirror = editor; // Give tests a way to get editor instance }, - }, -}); + editorProps: { + attributes: { + class: styles.editor, + "data-test": "editor", + }, + }, + } +); diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index e5f1f18ff7..88128b8ce0 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -7,9 +7,17 @@ import rootStyles from "./root.module.css"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; disableHistoryExtension: boolean; - menus: { - bubbleMenuFactory: (editor: Editor) => HTMLElement; - }; +}; + +export type menuFactoriesType = { + bubbleMenuFactory: (editor: Editor) => HTMLElement; + hyperlinkMenuFactory: ( + editor: Editor, + url: string, + text: string, + update: (url: string, text: string) => void, + remove: () => void + ) => HTMLElement; }; const blockNoteExtensions = getBlockNoteExtensions(); @@ -21,16 +29,23 @@ const blockNoteOptions = { }; export const mountBlockNoteEditor = ( + menuFactories: menuFactoriesType, options: Partial = {} ) => { - const extensions = options.disableHistoryExtension + let extensions = options.disableHistoryExtension ? blockNoteExtensions.filter((e) => e.name !== "history") : blockNoteExtensions; - extensions.map((extension) => { + extensions = extensions.map((extension) => { if (extension.name === "BubbleMenuExtension") { return extension.configure({ - bubbleMenuFactory: options.menus?.bubbleMenuFactory, + bubbleMenuFactory: menuFactories.bubbleMenuFactory, + }); + } + + if (extension.name === "link") { + return extension.configure({ + hyperlinkMenuFactory: menuFactories.hyperlinkMenuFactory, }); } diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx index 7cc5b38483..cda65f6c9e 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx @@ -14,7 +14,7 @@ export const BubbleMenuExtension = Extension.create<{ return [ createBubbleMenuPlugin({ editor: this.editor, - bubbleMenuFactory: this.editor.options.menus.bubbleMenuFactory, + bubbleMenuFactory: this.options.bubbleMenuFactory, pluginKey: new PluginKey("BubbleMenuPlugin"), }), ]; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx index 4431c493d5..f63199100d 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx @@ -1,15 +1,23 @@ import { Link } from "@tiptap/extension-link"; -import { createHyperlinkMenuPlugin } from "./HyperlinkMenuPlugin"; +import { + createHyperlinkMenuPlugin, + HyperlinkMenuPluginProps, +} from "./HyperlinkMenuPlugin"; /** * This custom link includes a special menu for editing/deleting/opening the link. * The menu will be triggered by hovering over the link with the mouse, * or by moving the cursor inside the link text */ -const Hyperlink = Link.extend({ +const Hyperlink = Link.extend({ priority: 500, addProseMirrorPlugins() { - return [...(this.parent?.() || []), createHyperlinkMenuPlugin()]; + return [ + ...(this.parent?.() || []), + createHyperlinkMenuPlugin(this.editor, { + hyperlinkMenuFactory: this.options.hyperlinkMenuFactory, + }), + ]; }, }); diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index a64670d555..a2ea0bbc47 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -1,22 +1,27 @@ -import { MantineProvider } from "@mantine/core"; -import Tippy from "@tippyjs/react"; -import { getMarkRange } from "@tiptap/core"; +import { Editor, getMarkRange } from "@tiptap/core"; import { Mark, ResolvedPos } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; -import { createRoot } from "react-dom/client"; -import { BlockNoteTheme } from "../../BlockNoteTheme"; -import { HyperlinkMenu } from "./menus/HyperlinkMenu"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); -export const createHyperlinkMenuPlugin = () => { - // as we always use Tippy appendTo(document.body), we can just create an element - // that we use for ReactDOM, but it isn't used anywhere (except by React internally) - const fakeRenderTarget = document.createElement("div"); - const fakeRenderTargetRoot = createRoot(fakeRenderTarget); +export type HyperlinkMenuPluginProps = { + hyperlinkMenuFactory: ( + editor: Editor, + url: string, + text: string, + update: (url: string, text: string) => void, + remove: () => void + ) => HTMLElement; +}; + +export const createHyperlinkMenuPlugin = ( + editor: Editor, + options: HyperlinkMenuPluginProps +) => { + const hyperlinkMenuFactory = options.hyperlinkMenuFactory; + let hyperlinkMenuElement: HTMLElement | undefined; let hoveredLink: HTMLAnchorElement | undefined; let menuState: "cursor-based" | "mouse-based" | "hidden" = "hidden"; - let nextTippyKey = 0; return new Plugin({ key: PLUGIN_KEY, @@ -28,7 +33,10 @@ export const createHyperlinkMenuPlugin = () => { // don't show menu when we have an active selection if (menuState !== "hidden") { menuState = "hidden"; - fakeRenderTargetRoot.render(<>); + if (hyperlinkMenuElement) { + hyperlinkMenuElement.remove(); + hyperlinkMenuElement = undefined; + } } return; } @@ -63,7 +71,10 @@ export const createHyperlinkMenuPlugin = () => { // if the cursor moves way if (menuState === "cursor-based") { menuState = "hidden"; - fakeRenderTargetRoot.render(<>); + if (hyperlinkMenuElement) { + hyperlinkMenuElement.remove(); + hyperlinkMenuElement = undefined; + } } return; } @@ -75,19 +86,17 @@ export const createHyperlinkMenuPlugin = () => { const text = view.state.doc.textBetween(range.from, range.to); const url = linkMark.attrs.href; - const anchorPos = { - // use the 'median' position of the range - ...view.coordsAtPos(Math.round((range.from + range.to) / 2)), - height: 0, // needed to satisfy types - width: 0, - }; - const foundLinkMark = linkMark; // typescript workaround for event handlers // A URL has to begin with http(s):// to be interpreted as an absolute path const editHandler = (href: string, text: string) => { menuState = "hidden"; - fakeRenderTargetRoot.render(<>); + + // hide menu + if (hyperlinkMenuElement) { + hyperlinkMenuElement.remove(); + hyperlinkMenuElement = undefined; + } // update the mark with new href (foundLinkMark as any).attrs = { ...foundLinkMark.attrs, href }; // TODO: invalid assign to attrs @@ -106,35 +115,16 @@ export const createHyperlinkMenuPlugin = () => { ); }; - const hyperlinkMenu = ( - - anchorPos as any} - content={ - - } - onHide={() => { - nextTippyKey++; - menuState = "hidden"; - }} - aria={{ expanded: false }} - interactive={true} - interactiveBorder={30} - triggerTarget={hoveredLink} - showOnCreate={basedOnCursorPos} - appendTo={document.body}> -
-
-
- ); - fakeRenderTargetRoot.render(hyperlinkMenu); + if (!hyperlinkMenuElement) { + hyperlinkMenuElement = hyperlinkMenuFactory( + editor, + url, + text, + editHandler, + removeHandler + ); + } + menuState = basedOnCursorPos ? "cursor-based" : "mouse-based"; }, }; diff --git a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx b/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx deleted file mode 100644 index aa931c57e5..0000000000 --- a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenu.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { createStyles, Stack } from "@mantine/core"; -import { useState } from "react"; -import { RiLink, RiText } from "react-icons/ri"; -import { EditHyperlinkMenuItem } from "./EditHyperlinkMenuItem"; - -export type EditHyperlinkMenuProps = { - url: string; - text: string; - update: (url: string, text: string) => void; -}; - -/** - * Menu which opens when editing an existing hyperlink or creating a new one. - * Provides input fields for setting the hyperlink URL and title. - */ -export const EditHyperlinkMenu = (props: EditHyperlinkMenuProps) => { - const [url, setUrl] = useState(props.url); - const [title, setTitle] = useState(props.text); - const { classes } = createStyles({ root: {} })(undefined, { - name: "EditHyperlinkMenu", - }); - - return ( - - setUrl(value)} - onSubmit={() => props.update(url, title)} - /> - setTitle(value)} - onSubmit={() => props.update(url, title)} - /> - - ); -}; diff --git a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx b/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx deleted file mode 100644 index ab6e3c16d0..0000000000 --- a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItem.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { IconType } from "react-icons"; -import { EditHyperlinkMenuItemIcon } from "./EditHyperlinkMenuItemIcon"; -import { EditHyperlinkMenuItemInput } from "./EditHyperlinkMenuItemInput"; -import { Group } from "@mantine/core"; - -export type EditHyperlinkMenuItemProps = { - icon: IconType; - mainIconTooltip: string; - secondaryIconTooltip?: string; - autofocus?: boolean; - placeholder?: string; - value?: string; - onChange: (value: string) => void; - onSubmit: () => void; -}; - -export function EditHyperlinkMenuItem(props: EditHyperlinkMenuItemProps) { - return ( - - - - - ); -} diff --git a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx b/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx deleted file mode 100644 index 9d4936307d..0000000000 --- a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemIcon.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { IconType } from "react-icons"; -import Tippy from "@tippyjs/react"; -import { TooltipContent } from "../../../shared/components/tooltip/TooltipContent"; -import { Container } from "@mantine/core"; - -export type EditHyperlinkMenuItemIconProps = { - icon: IconType; - mainTooltip: string; - secondaryTooltip?: string; -}; - -export function EditHyperlinkMenuItemIcon( - props: EditHyperlinkMenuItemIconProps -) { - const Icon = props.icon; - - return ( - - } - placement="left"> - - - - - ); -} diff --git a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx b/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx deleted file mode 100644 index 961da494f0..0000000000 --- a/packages/core/src/extensions/Hyperlinks/menus/EditHyperlinkMenuItemInput.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { KeyboardEvent, useEffect, useRef } from "react"; -import { TextInput } from "@mantine/core"; - -export type EditHyperlinkMenuItemInputProps = { - autofocus?: boolean; - placeholder?: string; - value?: string; - onChange: (value: string) => void; - onSubmit: () => void; -}; - -export function EditHyperlinkMenuItemInput( - props: EditHyperlinkMenuItemInputProps -) { - const inputRef = useRef(null); - - useEffect(() => { - setTimeout(() => { - props.autofocus && inputRef.current?.focus(); - }); - }, [props.autofocus]); - - function handleEnter(event: KeyboardEvent) { - if (event.key === "Enter") { - event.preventDefault(); - props.onSubmit(); - } - } - - return ( - props.onChange(event.currentTarget.value)} - onKeyDown={handleEnter} - placeholder={props.placeholder} - ref={inputRef} - /> - ); -} diff --git a/packages/core/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx b/packages/core/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx deleted file mode 100644 index acdaf8c9dc..0000000000 --- a/packages/core/src/extensions/Hyperlinks/menus/HoverHyperlinkMenu.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { Toolbar } from "../../../shared/components/toolbar/Toolbar"; -import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton"; - -type HoverHyperlinkMenuProps = { - url: string; - edit: () => void; - remove: () => void; -}; - -/** - * Menu which opens when hovering an existing hyperlink. - * Provides buttons for editing, opening, and removing the hyperlink. - */ -export const HoverHyperlinkMenu = (props: HoverHyperlinkMenuProps) => { - return ( - - - Edit Link - - { - window.open(props.url, "_blank"); - }} - icon={RiExternalLinkFill} - /> - - - ); -}; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index d8076fd23d..f7438358da 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -4,11 +4,11 @@ import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; import { BubbleMenu } from "./components/BubbleMenu"; import tippy from "tippy.js"; -import rootStyles from "../../../core/src/root.module.css"; +// import rootStyles from "../../../core/src/root.module.css"; export const BubbleMenuFactory = (editor: Editor) => { const element = document.createElement("div"); - element.className = rootStyles.bnRoot; + // element.className = rootStyles.bnRoot; const root = createRoot(element); root.render( @@ -48,5 +48,5 @@ export const BubbleMenuFactory = (editor: Editor) => { menu.show(); - return element; + return element as HTMLElement; }; diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx new file mode 100644 index 0000000000..32b0a419fa --- /dev/null +++ b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx @@ -0,0 +1,51 @@ +import { Editor } from "@tiptap/core"; +import { createRoot } from "react-dom/client"; +import { HyperlinkMenu } from "./components/HyperlinkMenu"; +import tippy from "tippy.js"; +import { MantineProvider } from "@mantine/core"; +import { BlockNoteTheme } from "../BlockNoteTheme"; + +export const HyperlinkMenuFactory = ( + editor: Editor, + url: string, + text: string, + update: (url: string, text: string) => void, + remove: () => void +) => { + const element = document.createElement("div"); + const root = createRoot(element); + + const getReferenceClientRect = () => { + const { left, top } = editor.view.coordsAtPos( + editor.state.selection.anchor + ); + + return new DOMRect(left, top); + }; + + root.render( + + + + ); + + const menu = tippy(document.body, { + duration: 0, + getReferenceClientRect: getReferenceClientRect, + content: element, + interactive: true, + trigger: "manual", + placement: "top", + hideOnClick: "toggle", + }); + + menu.show(); + + return element as HTMLElement; +}; diff --git a/packages/core/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx b/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx similarity index 55% rename from packages/core/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx rename to packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx index 4748334296..b8032182a3 100644 --- a/packages/core/src/extensions/Hyperlinks/menus/HyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx @@ -1,8 +1,7 @@ import { useState } from "react"; -import Tippy from "@tippyjs/react"; -import { EditHyperlinkMenu } from "./EditHyperlinkMenu"; -import { HoverHyperlinkMenu } from "./HoverHyperlinkMenu"; -import rootStyles from "../../../root.module.css"; +import { EditHyperlinkMenu } from "../EditHyperlinkMenu/components/EditHyperlinkMenu"; +import { HoverHyperlinkMenu } from "../HoverHyperlinkMenu/components/HoverHyperlinkMenu"; +// import rootStyles from "../../../root.module.css"; type HyperlinkMenuProps = { url: string; @@ -27,24 +26,11 @@ export const HyperlinkMenu = (props: HyperlinkMenuProps) => { const [isEditing, setIsEditing] = useState(false); const editHyperlinkMenu = ( - props.pos as any} - content={ - - } - interactive={true} - interactiveBorder={30} - showOnCreate={true} - trigger={"click"} // so that we don't hide on mouse out - hideOnClick - className={rootStyles.bnRoot} - appendTo={document.body}> -
-
+ ); const hoverHyperlinkMenu = ( diff --git a/packages/react/src/editor/useEditor.ts b/packages/react/src/editor/useEditor.ts index 6e5ed441a1..3d6c7a2d68 100644 --- a/packages/react/src/editor/useEditor.ts +++ b/packages/react/src/editor/useEditor.ts @@ -2,8 +2,8 @@ import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react"; import { DependencyList } from "react"; import { getBlockNoteExtensions } from "../../../core/src/BlockNoteExtensions"; -import styles from "../../../core/src/editor.module.css"; -import rootStyles from "../../../core/src/root.module.css"; +// import styles from "../../../core/src/editor.module.css"; +// import rootStyles from "../../../core/src/root.module.css"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; @@ -40,8 +40,8 @@ export const useEditor = ( attributes: { ...(options.editorProps?.attributes || {}), class: [ - styles.bnEditor, - rootStyles.bnRoot, + // styles.bnEditor, + // rootStyles.bnRoot, (options.editorProps?.attributes as any)?.class || "", ].join(" "), }, From 3a87e4b61361b3531a91dea032d5559e1b1534c4 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 14 Dec 2022 21:54:08 +0100 Subject: [PATCH 05/55] Major structural overhaul to menu factories --- examples/editor/src/main.tsx | 8 +- packages/core/src/BlockNoteEditor.ts | 16 +- packages/core/src/MenuFunctions.ts | 130 ---------- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 78 +++--- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 180 ++++++------- .../getBubbleMenuFactoryFunctions.ts | 134 ++++++++++ .../core/src/menu-tools/BubbleMenu/types.ts | 51 ++++ .../getHyperlinkHoverMenuFactoryFunctions.ts | 175 +++++++++++++ .../menu-tools/HyperlinkHoverMenu/types.ts | 17 ++ packages/core/src/menu-tools/types.ts | 6 + .../src/shared/components/toolbar/Toolbar.tsx | 10 - .../components/toolbar/ToolbarButton.tsx | 57 ----- .../components/toolbar/ToolbarDropdown.tsx | 35 --- .../toolbar/ToolbarDropdownItem.tsx | 35 --- .../toolbar/ToolbarDropdownTarget.tsx | 31 --- .../tooltip/TooltipContent.module.css | 15 -- .../components/tooltip/TooltipContent.tsx | 23 -- .../src/BubbleMenu/BubbleMenuFactory.tsx | 49 ++-- .../src/BubbleMenu/components/BubbleMenu.tsx | 239 ++++++++---------- .../components/LinkToolbarButton.tsx | 41 +-- .../react/src/{editor => Editor}/useEditor.ts | 0 .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 42 ++- .../components/HyperlinkMenu.tsx | 8 - 23 files changed, 657 insertions(+), 723 deletions(-) delete mode 100644 packages/core/src/MenuFunctions.ts create mode 100644 packages/core/src/menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions.ts create mode 100644 packages/core/src/menu-tools/BubbleMenu/types.ts create mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts create mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts create mode 100644 packages/core/src/menu-tools/types.ts delete mode 100644 packages/core/src/shared/components/toolbar/Toolbar.tsx delete mode 100644 packages/core/src/shared/components/toolbar/ToolbarButton.tsx delete mode 100644 packages/core/src/shared/components/toolbar/ToolbarDropdown.tsx delete mode 100644 packages/core/src/shared/components/toolbar/ToolbarDropdownItem.tsx delete mode 100644 packages/core/src/shared/components/toolbar/ToolbarDropdownTarget.tsx delete mode 100644 packages/core/src/shared/components/tooltip/TooltipContent.module.css delete mode 100644 packages/core/src/shared/components/tooltip/TooltipContent.tsx rename packages/react/src/{editor => Editor}/useEditor.ts (100%) diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 16da024d18..af60e30ea9 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,16 +1,16 @@ import "./index.css"; import styles from "./App.module.css"; import { mountBlockNoteEditor } from "../../../packages/core/src/BlockNoteEditor"; -import { BubbleMenuFactory } from "../../../packages/react/src/BubbleMenu/BubbleMenuFactory"; -import { HyperlinkMenuFactory } from "../../../packages/react/src/HyperlinkMenus/HyperlinkMenuFactory"; +import { ReactBubbleMenuFactory } from "../../../packages/react/src/BubbleMenu/BubbleMenuFactory"; +import { ReactHyperlinkMenuFactory } from "../../../packages/react/src/HyperlinkMenus/HyperlinkMenuFactory"; // type WindowWithProseMirror = Window & // typeof globalThis & { ProseMirror: Editor }; mountBlockNoteEditor( { - bubbleMenuFactory: BubbleMenuFactory, - hyperlinkMenuFactory: HyperlinkMenuFactory, + bubbleMenuFactory: ReactBubbleMenuFactory, + hyperlinkMenuFactory: ReactHyperlinkMenuFactory, }, { element: document.getElementById("root")!, diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 88128b8ce0..7b0b1a9aca 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -3,21 +3,17 @@ import { Editor, EditorOptions } from "@tiptap/core"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import rootStyles from "./root.module.css"; +import { BubbleMenuFactory } from "./menu-tools/BubbleMenu/types"; +import { HyperlinkHoverMenuFactory } from "./menu-tools/HyperlinkHoverMenu/types"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; disableHistoryExtension: boolean; }; -export type menuFactoriesType = { - bubbleMenuFactory: (editor: Editor) => HTMLElement; - hyperlinkMenuFactory: ( - editor: Editor, - url: string, - text: string, - update: (url: string, text: string) => void, - remove: () => void - ) => HTMLElement; +export type MenuFactories = { + bubbleMenuFactory: BubbleMenuFactory; + hyperlinkMenuFactory: HyperlinkHoverMenuFactory; }; const blockNoteExtensions = getBlockNoteExtensions(); @@ -29,7 +25,7 @@ const blockNoteOptions = { }; export const mountBlockNoteEditor = ( - menuFactories: menuFactoriesType, + menuFactories: MenuFactories, options: Partial = {} ) => { let extensions = options.disableHistoryExtension diff --git a/packages/core/src/MenuFunctions.ts b/packages/core/src/MenuFunctions.ts deleted file mode 100644 index f188f91d05..0000000000 --- a/packages/core/src/MenuFunctions.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Editor } from "@tiptap/core"; - -// Maybe useful later to not have to pass entire editor instance to menu factories? -export type BubbleMenuFunctionTypes = { - styles: { - boldActive: () => boolean; - toggleBold: () => void; - - italicActive: () => boolean; - toggleItalic: () => void; - - underlineActive: () => boolean; - toggleUnderline: () => void; - - strikeActive: () => boolean; - toggleStrike: () => void; - - hyperlinkActive: () => boolean; - hyperlinkUrl: () => string; - hyperlinkText: () => string; - addHyperlink: (url: string, text?: string) => void; - removeHyperlink: () => void; - }; - blockTypes: { - paragraphActive: () => boolean; - setParagraph: () => void; - - headingActive: () => boolean; - headingLevel: () => string; - setHeading: (level: string) => void; - - listItemActive: () => boolean; - listItemType: () => string; - setListItem: (type: string) => void; - }; -}; - -export function getBubbleMenuFunctions( - editor: Editor -): BubbleMenuFunctionTypes { - return { - styles: { - boldActive: () => editor.isActive("bold"), - toggleBold: () => { - editor.view.focus(); - editor.commands.toggleBold(); - }, - italicActive: () => editor.isActive("italic"), - toggleItalic: () => { - editor.view.focus(); - editor.commands.toggleItalic(); - }, - underlineActive: () => editor.isActive("underline"), - toggleUnderline: () => { - editor.view.focus(); - editor.commands.toggleUnderline(); - }, - strikeActive: () => editor.isActive("strike"), - toggleStrike: () => { - editor.view.focus(); - editor.commands.toggleStrike(); - }, - hyperlinkActive: () => editor.isActive("link"), - hyperlinkUrl: () => editor.getAttributes("link").href, - hyperlinkText: () => { - const { from, to } = editor.state.selection; - - return editor.state.doc.textBetween(from, to); - }, - addHyperlink: (url: string, text?: string) => { - if (url === "") { - return; - } - - let { from, to } = editor.state.selection; - - if (!text) { - text = editor.state.doc.textBetween(from, to); - } - - const mark = editor.schema.mark("link", { href: url }); - - editor.view.dispatch( - editor.view.state.tr - .insertText(text, from, to) - .addMark(from, from + text.length, mark) - ); - }, - }, - blockTypes: { - paragraphActive: () => - editor.state.selection.$from.node().type.name === "textContent", - setParagraph: () => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "textContent" - ); - }, - headingActive: () => - editor.state.selection.$from.node().type.name === "headingContent", - headingLevel: () => - editor.state.selection.$from.node().attrs["headingLevel"], - setHeading: (level: string = "1") => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "headingContent", - { - headingLevel: level, - } - ); - }, - listItemActive: () => - editor.state.selection.$from.node().type.name === "listItemContent", - listItemType: () => - editor.state.selection.$from.node().attrs["listItemType"], - setListItem: (type: string = "unordered") => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "listItemContent", - { - listItemType: type, - } - ); - }, - }, - }; -} diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 3a3523a9fa..bb5073b503 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -1,13 +1,16 @@ import { Editor, isTextSelection } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; +import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; +import { Menu } from "../../menu-tools/types"; +import { getBubbleMenuFactoryFunctions } from "../../menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files export interface BubbleMenuPluginProps { pluginKey: PluginKey | string; editor: Editor; - bubbleMenuFactory: (editor: Editor) => HTMLElement; + bubbleMenuFactory: BubbleMenuFactory; shouldShow?: | ((props: { editor: Editor; @@ -27,9 +30,9 @@ export type BubbleMenuViewProps = BubbleMenuPluginProps & { export class BubbleMenuView { public editor: Editor; - public bubbleMenuFactory: (editor: Editor) => HTMLElement; + public bubbleMenuFactory: BubbleMenuFactory; - public bubbleMenuElement: HTMLElement | undefined; + public bubbleMenu: Menu; public view: EditorView; @@ -67,6 +70,9 @@ export class BubbleMenuView { }: BubbleMenuViewProps) { this.editor = editor; this.bubbleMenuFactory = bubbleMenuFactory; + this.bubbleMenu = this.bubbleMenuFactory( + getBubbleMenuFactoryFunctions(editor) + ); this.view = view; if (shouldShow) { @@ -90,7 +96,7 @@ export class BubbleMenuView { }; dragstartHandler = () => { - this.destroy(); + this.hideMenu(); }; focusHandler = () => { @@ -107,23 +113,21 @@ export class BubbleMenuView { if ( event?.relatedTarget && - this.bubbleMenuElement?.parentNode?.contains(event.relatedTarget as Node) + this.bubbleMenu.element?.parentNode?.contains(event.relatedTarget as Node) ) { return; } - this.destroy(); + this.hideMenu(); }; update(view: EditorView, oldState?: EditorState) { - console.log("UPDATING"); const { state, composing } = view; const { doc, selection } = state; const isSame = oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); if (composing || isSame) { - console.log("NOT COMPOSING OR SAME"); return; } @@ -142,45 +146,37 @@ export class BubbleMenuView { }); if (!shouldShow || this.preventShow) { - console.log("SHOULDN'T SHOW OR PREVENT SHOW"); - !shouldShow && console.log("SHOULDN'T SHOW"); - this.preventShow && console.log("PREVENT SHOW"); - - this.destroy(); - + this.hideMenu(); return; } - console.log("SHOW"); - this.create(); + this.showMenu(); + this.bubbleMenu.update(); } - create() { - if (!this.bubbleMenuElement) { - this.bubbleMenuElement = this.bubbleMenuFactory(this.editor); - this.bubbleMenuElement.style.visibility = "visible"; - this.bubbleMenuElement.addEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); - } + showMenu() { + this.bubbleMenu.show(); + + this.bubbleMenu.element!.style.visibility = "visible"; + this.bubbleMenu.element!.addEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, + } + ); } - destroy() { - if (this.bubbleMenuElement) { - this.bubbleMenuElement.removeEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); - this.bubbleMenuElement.remove(); - this.bubbleMenuElement = undefined; - } + hideMenu() { + this.bubbleMenu.hide(); + + this.bubbleMenu.element!.removeEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, + } + ); } addEditorListeners() { @@ -193,7 +189,7 @@ export class BubbleMenuView { } removeEditorListeners() { - this.destroy(); + this.hideMenu(); this.view.dom.removeEventListener("mousedown", this.viewMousedownHandler); this.view.dom.removeEventListener("mouseup", this.viewMouseupHandler); diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index a2ea0bbc47..18ea661704 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -1,131 +1,69 @@ -import { Editor, getMarkRange } from "@tiptap/core"; -import { Mark, ResolvedPos } from "prosemirror-model"; +import { Editor } from "@tiptap/core"; +import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; +import { HyperlinkHoverMenuFactory } from "../../menu-tools/HyperlinkHoverMenu/types"; +import { getHyperlinkHoverMenuFactoryFunctions } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); export type HyperlinkMenuPluginProps = { - hyperlinkMenuFactory: ( - editor: Editor, - url: string, - text: string, - update: (url: string, text: string) => void, - remove: () => void - ) => HTMLElement; + hyperlinkMenuFactory: HyperlinkHoverMenuFactory; }; export const createHyperlinkMenuPlugin = ( editor: Editor, options: HyperlinkMenuPluginProps ) => { - const hyperlinkMenuFactory = options.hyperlinkMenuFactory; - let hyperlinkMenuElement: HTMLElement | undefined; + let mouseHoveredHyperlinkMark: Mark | undefined; + let keyboardHoveredHyperlinkMark: Mark | undefined; + let hyperlinkMark: Mark | undefined; - let hoveredLink: HTMLAnchorElement | undefined; - let menuState: "cursor-based" | "mouse-based" | "hidden" = "hidden"; + const hyperlinkMenuFactory = options.hyperlinkMenuFactory; + let hyperlinkMenu = hyperlinkMenuFactory( + getHyperlinkHoverMenuFactoryFunctions(editor) + ); return new Plugin({ key: PLUGIN_KEY, view() { return { - update: async (view, _prevState) => { - const selection = view.state.selection; - if (selection.from !== selection.to) { - // don't show menu when we have an active selection - if (menuState !== "hidden") { - menuState = "hidden"; - if (hyperlinkMenuElement) { - hyperlinkMenuElement.remove(); - hyperlinkMenuElement = undefined; - } - } - return; - } + update: async (_view, _prevState) => { + if (editor.state.selection.empty) { + const marksAtPos = editor.state.selection.$from.marks(); - let pos: number | undefined; - let resPos: ResolvedPos | undefined; - let linkMark: Mark | undefined; - let basedOnCursorPos = false; - if (hoveredLink) { - pos = view.posAtDOM(hoveredLink.firstChild!, 0); - resPos = view.state.doc.resolve(pos); - // based on https://github.com/ueberdosis/tiptap/blob/main/packages/core/src/helpers/getMarkRange.ts - const start = resPos.parent.childAfter(resPos.parentOffset).node; - linkMark = start?.marks.find((m) => m.type.name.startsWith("link")); - } + let foundHyperlinkMark = false; - if ( - !linkMark && - (view.hasFocus() || menuState === "cursor-based") // prevents re-opening menu after submission. Only open cursor-based menu if editor has focus - ) { - // no hovered link mark, try get from cursor position - pos = selection.from; - resPos = view.state.doc.resolve(pos); - const start = resPos.parent.childAfter(resPos.parentOffset).node; - linkMark = start?.marks.find((m) => m.type.name.startsWith("link")); - basedOnCursorPos = true; - } + for (const mark of marksAtPos) { + if (mark.type.name === editor.schema.mark("link").type.name) { + keyboardHoveredHyperlinkMark = mark; + foundHyperlinkMark = true; - if (!linkMark || !pos || !resPos) { - // The mouse-based popup takes care of hiding itself (tippy) - // Because the cursor-based popup is has "showOnCreate", we want to hide it manually - // if the cursor moves way - if (menuState === "cursor-based") { - menuState = "hidden"; - if (hyperlinkMenuElement) { - hyperlinkMenuElement.remove(); - hyperlinkMenuElement = undefined; + break; } } - return; - } - const range = getMarkRange(resPos, linkMark.type, linkMark.attrs); - if (!range) { - return; + if (!foundHyperlinkMark) { + keyboardHoveredHyperlinkMark = undefined; + } } - const text = view.state.doc.textBetween(range.from, range.to); - const url = linkMark.attrs.href; - - const foundLinkMark = linkMark; // typescript workaround for event handlers - // A URL has to begin with http(s):// to be interpreted as an absolute path - const editHandler = (href: string, text: string) => { - menuState = "hidden"; - - // hide menu - if (hyperlinkMenuElement) { - hyperlinkMenuElement.remove(); - hyperlinkMenuElement = undefined; - } + if (keyboardHoveredHyperlinkMark) { + hyperlinkMark = keyboardHoveredHyperlinkMark; + } - // update the mark with new href - (foundLinkMark as any).attrs = { ...foundLinkMark.attrs, href }; // TODO: invalid assign to attrs - // insertText actually replaces the range with text - const tr = view.state.tr.insertText(text, range.from, range.to); - // the former range.to is no longer in use - tr.addMark(range.from, range.from + text.length, foundLinkMark); - view.dispatch(tr); - }; + if (mouseHoveredHyperlinkMark) { + hyperlinkMark = mouseHoveredHyperlinkMark; + } - const removeHandler = () => { - view.dispatch( - view.state.tr - .removeMark(range.from, range.to, foundLinkMark.type) - .setMeta("preventAutolink", true) - ); - }; - - if (!hyperlinkMenuElement) { - hyperlinkMenuElement = hyperlinkMenuFactory( - editor, - url, - text, - editHandler, - removeHandler - ); + if (!mouseHoveredHyperlinkMark && !keyboardHoveredHyperlinkMark) { + hyperlinkMark = undefined; } - menuState = basedOnCursorPos ? "cursor-based" : "mouse-based"; + if (!hyperlinkMark) { + hyperlinkMenu.hide(); + } else { + hyperlinkMenu.update(); + hyperlinkMenu.show(); + } }, }; }, @@ -134,20 +72,46 @@ export const createHyperlinkMenuPlugin = ( handleDOMEvents: { // update view when an is hovered over mouseover(view, event: any) { - const newHoveredLink = + if ( event.target instanceof HTMLAnchorElement && event.target.nodeName === "A" - ? event.target - : undefined; + ) { + const hoveredHyperlinkElement = event.target; + const posInHoveredHyperlinkMark = + editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1; + const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve( + posInHoveredHyperlinkMark + ); + const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks(); + + let foundHyperlinkMark = false; - if (newHoveredLink !== hoveredLink) { - // dispatch a meta transaction to make sure the view gets updated - hoveredLink = newHoveredLink; + for (const mark of marksAtPos) { + if (mark.type.name === editor.schema.mark("link").type.name) { + mouseHoveredHyperlinkMark = mark; + foundHyperlinkMark = true; + break; + } + } + + if (!foundHyperlinkMark) { + mouseHoveredHyperlinkMark = undefined; + } + } else { + mouseHoveredHyperlinkMark = undefined; + } + + // Using setTimeout ensures all other listeners of this event are executed before a new transaction is + // dispatched. + setTimeout(() => { view.dispatch( - view.state.tr.setMeta(PLUGIN_KEY, { hoveredLinkChanged: true }) + view.state.tr.setMeta(PLUGIN_KEY, { + hoveredLinkChanged: true, + }) ); - } + }); + return false; }, }, diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions.ts new file mode 100644 index 0000000000..96ea572785 --- /dev/null +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions.ts @@ -0,0 +1,134 @@ +import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; +import { BubbleMenuFactoryFunctions } from "./types"; + +export function getBubbleMenuFactoryFunctions( + editor: Editor +): BubbleMenuFactoryFunctions { + return { + marks: { + bold: { + isActive: () => editor.isActive("bold"), + toggle: () => { + editor.view.focus(); + editor.commands.toggleBold(); + }, + }, + italic: { + isActive: () => editor.isActive("italic"), + toggle: () => { + editor.view.focus(); + editor.commands.toggleItalic(); + }, + }, + underline: { + isActive: () => editor.isActive("underline"), + toggle: () => { + editor.view.focus(); + editor.commands.toggleUnderline(); + }, + }, + strike: { + isActive: () => editor.isActive("strike"), + toggle: () => { + editor.view.focus(); + editor.commands.toggleStrike(); + }, + }, + hyperlink: { + isActive: () => editor.isActive("link"), + getUrl: () => editor.getAttributes("link").href, + getText: () => { + const { from, to } = editor.state.selection; + + return editor.state.doc.textBetween(from, to); + }, + set: (url: string, text?: string) => { + if (url === "") { + return; + } + + let { from, to } = editor.state.selection; + + if (!text) { + text = editor.state.doc.textBetween(from, to); + } + + const mark = editor.schema.mark("link", { href: url }); + + editor.view.dispatch( + editor.view.state.tr + .insertText(text, from, to) + .addMark(from, from + text.length, mark) + ); + }, + }, + }, + blocks: { + paragraph: { + isActive: () => + editor.state.selection.$from.node().type.name === "textContent", + set: () => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "textContent" + ); + }, + }, + heading: { + isActive: () => + editor.state.selection.$from.node().type.name === "headingContent", + getLevel: () => + editor.state.selection.$from.node().attrs["headingLevel"], + set: (level: string = "1") => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "headingContent", + { + headingLevel: level, + } + ); + }, + }, + listItem: { + isActive: () => + editor.state.selection.$from.node().type.name === "listItemContent", + getType: () => + editor.state.selection.$from.node().attrs["listItemType"], + set: (type: string = "unordered") => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "listItemContent", + { + listItemType: type, + } + ); + }, + }, + }, + view: { + getSelectionBoundingBox: () => { + const { state } = editor.view; + const { selection } = state; + + // support for CellSelections + const { ranges } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const to = Math.max(...ranges.map((range) => range.$to.pos)); + + if (isNodeSelection(selection)) { + const node = editor.view.nodeDOM(from) as HTMLElement; + + if (node) { + return node.getBoundingClientRect(); + } + } + + return posToDOMRect(editor.view, from, to); + }, + getEditorElement: () => editor.options.element, + }, + }; +} diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts new file mode 100644 index 0000000000..347a3c6fce --- /dev/null +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -0,0 +1,51 @@ +import { Menu } from "../types"; + +export type BasicMarkFunctions = { + isActive: () => boolean; + toggle: () => void; +}; + +export type HyperlinkMarkFunctions = { + isActive: () => boolean; + getUrl: () => string; + getText: () => string; + set: (url: string, text?: string) => void; +}; + +export type ParagraphBlockFunctions = { + isActive: () => boolean; + set: () => void; +}; + +export type HeadingBlockFunctions = { + isActive: () => boolean; + getLevel: () => string; + set: (level: string) => void; +}; + +export type ListItemBlockFunctions = { + isActive: () => boolean; + getType: () => string; + set: (type: string) => void; +}; + +export type BubbleMenuFactoryFunctions = { + marks: { + bold: BasicMarkFunctions; + italic: BasicMarkFunctions; + underline: BasicMarkFunctions; + strike: BasicMarkFunctions; + hyperlink: HyperlinkMarkFunctions; + }; + blocks: { + paragraph: ParagraphBlockFunctions; + heading: HeadingBlockFunctions; + listItem: ListItemBlockFunctions; + }; + view: { + getSelectionBoundingBox: () => DOMRect; + getEditorElement: () => Element; + }; +}; + +export type BubbleMenuFactory = (functions: BubbleMenuFactoryFunctions) => Menu; diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts new file mode 100644 index 0000000000..7f66139ba3 --- /dev/null +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts @@ -0,0 +1,175 @@ +import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; +import { HyperlinkHoverMenuFactoryFunctions } from "./types"; +import { Mark } from "prosemirror-model"; + +let mouseHoveredHyperlinkMark: Mark | undefined; +let mouseHoveredHyperlinkMarkRange: Range | undefined; + +let keyboardHoveredHyperlinkMark: Mark | undefined; +let keyboardHoveredHyperlinkMarkRange: Range | undefined; + +let hyperlinkMark: Mark | undefined; +let hyperlinkMarkRange: Range | undefined; + +function getHyperlinkMark(editor: Editor) { + if (editor.state.selection.empty) { + const marksAtPos = editor.state.selection.$from.marks(); + + let foundHyperlinkMark = false; + + for (const mark of marksAtPos) { + if (mark.type.name === editor.schema.mark("link").type.name) { + keyboardHoveredHyperlinkMark = mark; + keyboardHoveredHyperlinkMarkRange = + getMarkRange(editor.state.selection.$from, mark.type, mark.attrs) || + undefined; + foundHyperlinkMark = true; + + break; + } + } + + if (!foundHyperlinkMark) { + keyboardHoveredHyperlinkMark = undefined; + keyboardHoveredHyperlinkMarkRange = undefined; + } + } + + if (keyboardHoveredHyperlinkMark) { + hyperlinkMark = keyboardHoveredHyperlinkMark; + hyperlinkMarkRange = keyboardHoveredHyperlinkMarkRange; + } + + // console.log(mouseHoveredHyperlinkMark); + if (mouseHoveredHyperlinkMark) { + hyperlinkMark = mouseHoveredHyperlinkMark; + hyperlinkMarkRange = mouseHoveredHyperlinkMarkRange; + } + + if (!mouseHoveredHyperlinkMark && !keyboardHoveredHyperlinkMark) { + hyperlinkMark = undefined; + hyperlinkMarkRange = undefined; + } + + return { + hyperlinkMark, + hyperlinkMarkRange, + }; +} + +export function getHyperlinkHoverMenuFactoryFunctions( + editor: Editor +): HyperlinkHoverMenuFactoryFunctions { + const editorElement = editor.options.element; + editorElement.addEventListener("mouseover", (event) => { + if ( + event.target instanceof HTMLAnchorElement && + event.target.nodeName === "A" + ) { + const hoveredHyperlinkElement = event.target; + const posInHoveredHyperlinkMark = + editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1; + const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve( + posInHoveredHyperlinkMark + ); + const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks(); + + let foundHyperlinkMark = false; + + for (const mark of marksAtPos) { + if (mark.type.name === editor.schema.mark("link").type.name) { + mouseHoveredHyperlinkMark = mark; + mouseHoveredHyperlinkMarkRange = + getMarkRange( + resolvedPosInHoveredHyperlinkMark, + mark.type, + mark.attrs + ) || undefined; + foundHyperlinkMark = true; + + break; + } + } + + if (!foundHyperlinkMark) { + mouseHoveredHyperlinkMark = undefined; + mouseHoveredHyperlinkMarkRange = undefined; + } + } else { + mouseHoveredHyperlinkMark = undefined; + mouseHoveredHyperlinkMarkRange = undefined; + } + }); + + return { + hyperlink: { + getUrl: () => { + const { hyperlinkMark } = getHyperlinkMark(editor); + return hyperlinkMark?.attrs.href; + }, + getText: () => { + const { hyperlinkMarkRange } = getHyperlinkMark(editor); + + if (!hyperlinkMarkRange) { + return ""; + } + + return editor.view.state.doc.textBetween( + hyperlinkMarkRange.from, + hyperlinkMarkRange.to + ); + }, + edit: (url: string, text: string) => { + const { hyperlinkMarkRange } = getHyperlinkMark(editor); + + if (!hyperlinkMarkRange) { + return; + } + + const tr = editor.view.state.tr.insertText( + text, + hyperlinkMarkRange.from, + hyperlinkMarkRange.to + ); + tr.addMark( + hyperlinkMarkRange.from, + hyperlinkMarkRange.from + text.length, + editor.schema.mark("link", { href: url }) + ); + editor.view.dispatch(tr); + }, + delete: () => { + const { hyperlinkMark, hyperlinkMarkRange } = getHyperlinkMark(editor); + + if (!hyperlinkMark || !hyperlinkMarkRange) { + return; + } + + editor.view.dispatch( + editor.view.state.tr + .removeMark( + hyperlinkMarkRange.from, + hyperlinkMarkRange.to, + hyperlinkMark.type + ) + .setMeta("preventAutolink", true) + ); + }, + }, + view: { + getHyperlinkBoundingBox: () => { + const { hyperlinkMarkRange } = getHyperlinkMark(editor); + + if (!hyperlinkMarkRange) { + return; + } + + return posToDOMRect( + editor.view, + hyperlinkMarkRange.from, + hyperlinkMarkRange.to + ); + }, + }, + }; +} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts new file mode 100644 index 0000000000..8f44364d56 --- /dev/null +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts @@ -0,0 +1,17 @@ +import { Menu } from "../types"; + +export type HyperlinkHoverMenuFactoryFunctions = { + hyperlink: { + getUrl: () => string; + getText: () => string; + edit: (url: string, text: string) => void; + delete: () => void; + }; + view: { + getHyperlinkBoundingBox: () => DOMRect | undefined; + }; +}; + +export type HyperlinkHoverMenuFactory = ( + functions: HyperlinkHoverMenuFactoryFunctions +) => Menu; diff --git a/packages/core/src/menu-tools/types.ts b/packages/core/src/menu-tools/types.ts new file mode 100644 index 0000000000..8a8fb4d924 --- /dev/null +++ b/packages/core/src/menu-tools/types.ts @@ -0,0 +1,6 @@ +export type Menu = { + element: HTMLElement | undefined; + show: () => void; + hide: () => void; + update: () => void; +}; diff --git a/packages/core/src/shared/components/toolbar/Toolbar.tsx b/packages/core/src/shared/components/toolbar/Toolbar.tsx deleted file mode 100644 index df46517e30..0000000000 --- a/packages/core/src/shared/components/toolbar/Toolbar.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createStyles, Group } from "@mantine/core"; -import { ReactNode } from "react"; - -export const Toolbar = (props: { children: ReactNode }) => { - const { classes } = createStyles({ root: {} })(undefined, { - name: "Toolbar", - }); - - return {props.children}; -}; diff --git a/packages/core/src/shared/components/toolbar/ToolbarButton.tsx b/packages/core/src/shared/components/toolbar/ToolbarButton.tsx deleted file mode 100644 index fe457e8253..0000000000 --- a/packages/core/src/shared/components/toolbar/ToolbarButton.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ActionIcon, Button } from "@mantine/core"; -import Tippy from "@tippyjs/react"; -import { forwardRef } from "react"; -import { TooltipContent } from "../tooltip/TooltipContent"; -import React from "react"; -import { IconType } from "react-icons"; - -export type ToolbarButtonProps = { - onClick?: (e: React.MouseEvent) => void; - icon?: IconType; - mainTooltip: string; - secondaryTooltip?: string; - isSelected?: boolean; - children?: any; - isDisabled?: boolean; -}; - -/** - * Helper for basic buttons that show in the inline bubble menu. - */ -export const ToolbarButton = forwardRef((props: ToolbarButtonProps, ref) => { - const ButtonIcon = props.icon; - return ( - - } - trigger={"mouseenter"}> - {/*Creates an ActionIcon instead of a Button if only an icon is provided as content.*/} - {props.children ? ( - - ) : ( - - {ButtonIcon && } - - )} - - ); -}); diff --git a/packages/core/src/shared/components/toolbar/ToolbarDropdown.tsx b/packages/core/src/shared/components/toolbar/ToolbarDropdown.tsx deleted file mode 100644 index a97a6539a4..0000000000 --- a/packages/core/src/shared/components/toolbar/ToolbarDropdown.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Menu } from "@mantine/core"; -import { IconType } from "react-icons"; -import { ToolbarDropdownTarget } from "./ToolbarDropdownTarget"; -import { MouseEvent } from "react"; -import { ToolbarDropdownItem } from "./ToolbarDropdownItem"; - -export type ToolbarDropdownProps = { - text: string; - icon?: IconType; - items: Array<{ - onClick?: (e: MouseEvent) => void; - text: string; - icon?: IconType; - isSelected?: boolean; - children?: any; - isDisabled?: boolean; - }>; - children?: any; - isDisabled?: boolean; -}; - -export function ToolbarDropdown(props: ToolbarDropdownProps) { - return ( - - - - - - {props.items.map((item) => ( - - ))} - - - ); -} diff --git a/packages/core/src/shared/components/toolbar/ToolbarDropdownItem.tsx b/packages/core/src/shared/components/toolbar/ToolbarDropdownItem.tsx deleted file mode 100644 index bf60c106a8..0000000000 --- a/packages/core/src/shared/components/toolbar/ToolbarDropdownItem.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Menu } from "@mantine/core"; -import { IconType } from "react-icons"; -import { TiTick } from "react-icons/ti"; -import { MouseEvent } from "react"; - -export type ToolbarDropdownItemProps = { - onClick?: (e: MouseEvent) => void; - text: string; - icon?: IconType; - isSelected?: boolean; - children?: any; - isDisabled?: boolean; -}; - -export function ToolbarDropdownItem(props: ToolbarDropdownItemProps) { - const ItemIcon = props.icon; - - return ( - } - rightSection={ - props.isSelected ? ( - - ) : ( - // Ensures space for tick even if item isn't currently selected. -
- ) - } - disabled={props.isDisabled}> - {props.text} - - ); -} diff --git a/packages/core/src/shared/components/toolbar/ToolbarDropdownTarget.tsx b/packages/core/src/shared/components/toolbar/ToolbarDropdownTarget.tsx deleted file mode 100644 index 19b4c516c7..0000000000 --- a/packages/core/src/shared/components/toolbar/ToolbarDropdownTarget.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button } from "@mantine/core"; -import { HiChevronDown } from "react-icons/hi"; -import { IconType } from "react-icons"; -import { forwardRef } from "react"; - -export type ToolbarDropdownTargetProps = { - text: string; - icon?: IconType; - isDisabled?: boolean; -}; - -export const ToolbarDropdownTarget = forwardRef< - HTMLButtonElement, - ToolbarDropdownTargetProps ->((props: ToolbarDropdownTargetProps, ref) => { - const { text, icon, isDisabled, ...others } = props; - - const TargetIcon = props.icon; - return ( - - ); -}); diff --git a/packages/core/src/shared/components/tooltip/TooltipContent.module.css b/packages/core/src/shared/components/tooltip/TooltipContent.module.css deleted file mode 100644 index c687d30ccf..0000000000 --- a/packages/core/src/shared/components/tooltip/TooltipContent.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.tooltip { - color: var(--N40); - background-color: var(--N800); - box-shadow: 0 0 10px rgba(253, 254, 255, 0.8), - 0 0 3px rgba(253, 254, 255, 0.4); - border-radius: 2px; - font-size: smaller; - text-align: center; - padding: 4px; -} - -.secondaryText { - font-weight: 400; - opacity: 0.6; -} diff --git a/packages/core/src/shared/components/tooltip/TooltipContent.tsx b/packages/core/src/shared/components/tooltip/TooltipContent.tsx deleted file mode 100644 index 2a57e841b8..0000000000 --- a/packages/core/src/shared/components/tooltip/TooltipContent.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import styles from "./TooltipContent.module.css"; - -/** - * Helper for the tooltip for inline bubble menu buttons. - * - * Often used to display a tooltip showing the command name + keyboard shortcut, e.g.: - * - * Bold - * Ctrl+B - * - * TODO: maybe use default Tippy styles instead? - */ -export const TooltipContent = (props: { - mainTooltip: string; - secondaryTooltip?: string; -}) => ( -
-
{props.mainTooltip}
- {props.secondaryTooltip && ( -
{props.secondaryTooltip}
- )} -
-); diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index f7438358da..db4a1c358b 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -1,44 +1,30 @@ -import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; import { createRoot } from "react-dom/client"; import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; import { BubbleMenu } from "./components/BubbleMenu"; import tippy from "tippy.js"; +import { + BubbleMenuFactory, + BubbleMenuFactoryFunctions, +} from "../../../core/src/menu-tools/BubbleMenu/types"; // import rootStyles from "../../../core/src/root.module.css"; -export const BubbleMenuFactory = (editor: Editor) => { +export const ReactBubbleMenuFactory: BubbleMenuFactory = ( + bubbleMenuFactoryFunctions: BubbleMenuFactoryFunctions +) => { const element = document.createElement("div"); // element.className = rootStyles.bnRoot; const root = createRoot(element); root.render( - + ); - const { state } = editor.view; - const { selection } = state; - - // support for CellSelections - const { ranges } = selection; - const from = Math.min(...ranges.map((range) => range.$from.pos)); - const to = Math.max(...ranges.map((range) => range.$to.pos)); - - const getReferenceRect = () => { - if (isNodeSelection(state.selection)) { - const node = editor.view.nodeDOM(from) as HTMLElement; - - if (node) { - return node.getBoundingClientRect(); - } - } - - return posToDOMRect(editor.view, from, to); - }; - - const menu = tippy(document.body, { + let menu = tippy(bubbleMenuFactoryFunctions.view.getEditorElement(), { duration: 0, - getReferenceClientRect: getReferenceRect, + getReferenceClientRect: + bubbleMenuFactoryFunctions.view.getSelectionBoundingBox, content: element, interactive: true, trigger: "manual", @@ -46,7 +32,14 @@ export const BubbleMenuFactory = (editor: Editor) => { hideOnClick: "toggle", }); - menu.show(); - - return element as HTMLElement; + return { + element: element as HTMLElement, + show: menu.show, + hide: menu.hide, + // BubbleMenu React component updates its UI elements automatically with useState hooks, so we only need to ensure + // the tippy menu updates its position. + update: () => { + menu.popperInstance?.forceUpdate(); + }, + }; }; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 87e1e548e0..138ea78c0d 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -1,5 +1,3 @@ -import { Editor } from "@tiptap/core"; -import { Node } from "prosemirror-model"; import { RiBold, RiH1, @@ -21,240 +19,203 @@ import { Toolbar } from "../../shared/components/toolbar/Toolbar"; import { useState } from "react"; import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; +import { BubbleMenuFactoryFunctions } from "../../../../core/src/menu-tools/BubbleMenu/types"; // TODO: add list options, indentation -export const BubbleMenu = (props: { editor: Editor }) => { - const getDropdownText = (node: Node) => { - if (node.type.name === "textContent") { - return "Text"; - } +export const BubbleMenu = (props: { + bubbleMenuFunctions: BubbleMenuFactoryFunctions; +}) => { + const getActiveMarks = () => { + const activeMarks = new Set(); - if (node.type.name === "headingContent") { - return "Heading " + node.attrs["headingLevel"]; - } + props.bubbleMenuFunctions.marks.bold.isActive() && activeMarks.add("bold"); + props.bubbleMenuFunctions.marks.italic.isActive() && + activeMarks.add("italic"); + props.bubbleMenuFunctions.marks.underline.isActive() && + activeMarks.add("underline"); + props.bubbleMenuFunctions.marks.strike.isActive() && + activeMarks.add("strike"); + props.bubbleMenuFunctions.marks.hyperlink.isActive() && + activeMarks.add("link"); - if (node.type.name === "listItemContent") { - if (node.attrs["listItemType"] === "unordered") { - return "Bullet List"; - } else { - return "Ordered List"; - } - } - - return undefined; + return activeMarks; }; - const getDropdownIcon = (node: Node) => { - if (node.type.name === "textContent") { - return RiText; + const getActiveBlock = () => { + if (props.bubbleMenuFunctions.blocks.paragraph.isActive()) { + return { + text: "Text", + icon: RiText, + }; } - if (node.type.name === "headingContent") { - if (node.attrs["headingLevel"] === "1") { - return RiH1; + if (props.bubbleMenuFunctions.blocks.heading.isActive()) { + if (props.bubbleMenuFunctions.blocks.heading.getLevel() === "1") { + return { + text: "Heading 1", + icon: RiH1, + }; } - if (node.attrs["headingLevel"] === "2") { - return RiH2; + if (props.bubbleMenuFunctions.blocks.heading.getLevel() === "2") { + return { + text: "Heading 2", + icon: RiH2, + }; } - if (node.attrs["headingLevel"] === "3") { - return RiH3; + if (props.bubbleMenuFunctions.blocks.heading.getLevel() === "3") { + return { + text: "Heading 3", + icon: RiH3, + }; } } - if (node.type.name === "listItemContent") { - if (node.attrs["listItemType"] === "unordered") { - return RiListUnordered; + if (props.bubbleMenuFunctions.blocks.listItem.isActive()) { + if (props.bubbleMenuFunctions.blocks.listItem.getType() === "unordered") { + return { + text: "Bullet List", + icon: RiListUnordered, + }; } else { - return RiListOrdered; + return { + text: "Ordered List", + icon: RiListOrdered, + }; } } return undefined; }; - const getActiveMarks = () => { - const activeMarks = new Set(); - - props.editor.isActive("bold") && activeMarks.add("bold"); - props.editor.isActive("italic") && activeMarks.add("italic"); - props.editor.isActive("underline") && activeMarks.add("underline"); - props.editor.isActive("strike") && activeMarks.add("strike"); - props.editor.isActive("link") && activeMarks.add("link"); - - return activeMarks; - }; - - const [selectedNode, setSelectedNode] = useState( - props.editor.state.selection.$from.node() - ); - const [selectedNodeMarks, setSelectedNodeMarks] = useState(getActiveMarks()); + const [activeMarks, setActiveMarks] = useState(getActiveMarks()); + const [activeBlock, setActiveBlock] = useState(getActiveBlock()); return ( { // Setting editor focus using a chained command instead causes bubble menu to flicker on click. - props.editor.view.focus(); - props.editor.commands.BNSetContentType( - props.editor.state.selection.from, - "textContent" - ); - setSelectedNode(props.editor.state.selection.$from.node()); + props.bubbleMenuFunctions.blocks.paragraph.set(); + setActiveBlock(getActiveBlock()); }, text: "Text", icon: RiText, - isSelected: selectedNode.type.name === "textContent", + isSelected: props.bubbleMenuFunctions.blocks.paragraph.isActive(), }, { onClick: () => { - props.editor.view.focus(); - props.editor.commands.BNSetContentType( - props.editor.state.selection.from, - "headingContent", - { - headingLevel: "1", - } - ); - setSelectedNode(props.editor.state.selection.$from.node()); + props.bubbleMenuFunctions.blocks.heading.set("1"); + setActiveBlock(getActiveBlock()); }, text: "Heading 1", icon: RiH1, isSelected: - selectedNode.type.name === "headingContent" && - selectedNode.attrs["headingLevel"] === "1", + props.bubbleMenuFunctions.blocks.heading.isActive() && + props.bubbleMenuFunctions.blocks.heading.getLevel() === "1", }, { onClick: () => { - props.editor.view.focus(); - props.editor.commands.BNSetContentType( - props.editor.state.selection.from, - "headingContent", - { - headingLevel: "2", - } - ); - setSelectedNode(props.editor.state.selection.$from.node()); + props.bubbleMenuFunctions.blocks.heading.set("2"); + setActiveBlock(getActiveBlock()); }, text: "Heading 2", icon: RiH2, isSelected: - selectedNode.type.name === "headingContent" && - selectedNode.attrs["headingLevel"] === "2", + props.bubbleMenuFunctions.blocks.heading.isActive() && + props.bubbleMenuFunctions.blocks.heading.getLevel() === "2", }, { onClick: () => { - props.editor.view.focus(); - props.editor.commands.BNSetContentType( - props.editor.state.selection.from, - "headingContent", - { - headingLevel: "3", - } - ); - setSelectedNode(props.editor.state.selection.$from.node()); + props.bubbleMenuFunctions.blocks.heading.set("3"); + setActiveBlock(getActiveBlock()); }, text: "Heading 3", icon: RiH3, isSelected: - selectedNode.type.name === "headingContent" && - selectedNode.attrs["headingLevel"] === "3", + props.bubbleMenuFunctions.blocks.heading.isActive() && + props.bubbleMenuFunctions.blocks.heading.getLevel() === "3", }, { onClick: () => { - props.editor.view.focus(); - props.editor.commands.BNSetContentType( - props.editor.state.selection.from, - "listItemContent", - { - listItemType: "unordered", - } - ); - setSelectedNode(props.editor.state.selection.$from.node()); + props.bubbleMenuFunctions.blocks.listItem.set("unordered"); + setActiveBlock(getActiveBlock()); }, text: "Bullet List", icon: RiListUnordered, isSelected: - selectedNode.type.name === "listItemContent" && - selectedNode.attrs["listItemType"] === "unordered", + props.bubbleMenuFunctions.blocks.listItem.isActive() && + props.bubbleMenuFunctions.blocks.listItem.getType() === + "unordered", }, { onClick: () => { - props.editor.view.focus(); - props.editor.commands.BNSetContentType( - props.editor.state.selection.from, - "listItemContent", - { - listItemType: "ordered", - } - ); - setSelectedNode(props.editor.state.selection.$from.node()); + props.bubbleMenuFunctions.blocks.listItem.set("ordered"); + setActiveBlock(getActiveBlock()); }, text: "Numbered List", icon: RiListOrdered, isSelected: - selectedNode.type.name === "listItemContent" && - selectedNode.attrs["listItemType"] === "ordered", + props.bubbleMenuFunctions.blocks.listItem.isActive() && + props.bubbleMenuFunctions.blocks.listItem.getType() === "ordered", }, ]} /> { - // Setting editor focus using a chained command instead causes bubble menu to flicker on click. - props.editor.view.focus(); - props.editor.commands.toggleBold(); - setSelectedNodeMarks(getActiveMarks()); + props.bubbleMenuFunctions.marks.bold.toggle(); + setActiveMarks(getActiveMarks()); }} - isSelected={selectedNodeMarks.has("bold")} + isSelected={activeMarks.has("bold")} mainTooltip="Bold" secondaryTooltip={formatKeyboardShortcut("Mod+B")} icon={RiBold} /> { - props.editor.view.focus(); - props.editor.commands.toggleItalic(); - setSelectedNodeMarks(getActiveMarks()); + props.bubbleMenuFunctions.marks.italic.toggle(); + setActiveMarks(getActiveMarks()); }} - isSelected={selectedNodeMarks.has("italic")} + isSelected={activeMarks.has("italic")} mainTooltip="Italic" secondaryTooltip={formatKeyboardShortcut("Mod+I")} icon={RiItalic} /> { - props.editor.view.focus(); - props.editor.commands.toggleUnderline(); - setSelectedNodeMarks(getActiveMarks()); + props.bubbleMenuFunctions.marks.underline.toggle(); + setActiveMarks(getActiveMarks()); }} - isSelected={selectedNodeMarks.has("underline")} + isSelected={activeMarks.has("underline")} mainTooltip="Underline" secondaryTooltip={formatKeyboardShortcut("Mod+U")} icon={RiUnderline} /> { - props.editor.view.focus(); - props.editor.commands.toggleStrike(); - setSelectedNodeMarks(getActiveMarks()); + props.bubbleMenuFunctions.marks.strike.toggle(); + setActiveMarks(getActiveMarks()); }} - isSelected={selectedNodeMarks.has("strike")} + isSelected={activeMarks.has("strike")} mainTooltip="Strike-through" secondaryTooltip={formatKeyboardShortcut("Mod+Shift+X")} icon={RiStrikethrough} /> { - props.editor.view.focus(); - props.editor.commands.sinkListItem("block"); - setSelectedNodeMarks(getActiveMarks()); + // props.editor.view.focus(); + // props.editor.commands.sinkListItem("block"); + setActiveMarks(getActiveMarks()); }} - isDisabled={!props.editor.can().sinkListItem("block")} + isDisabled={ + // !props.editor.can().sinkListItem("block") + true + } mainTooltip="Indent" secondaryTooltip={formatKeyboardShortcut("Tab")} icon={RiIndentIncrease} @@ -262,9 +223,9 @@ export const BubbleMenu = (props: { editor: Editor }) => { { - props.editor.view.focus(); - props.editor.commands.liftListItem("block"); - setSelectedNodeMarks(getActiveMarks()); + // props.editor.view.focus(); + // props.editor.commands.liftListItem("block"); + setActiveMarks(getActiveMarks()); }} isDisabled={ // !props.editor.can().command(({ state }) => { @@ -283,8 +244,8 @@ export const BubbleMenu = (props: { editor: Editor }) => { /> { // TODO: review code; does this pattern still make sense? const updateCreationMenu = useCallback(() => { - const onSubmit = (url: string, text: string) => { - if (url === "") { - return; - } - const mark = props.editor.schema.mark("link", { href: url }); - let { from, to } = props.editor.state.selection; - props.editor.view.dispatch( - props.editor.view.state.tr - .insertText(text, from, to) - .addMark(from, from + text.length, mark) - ); - }; - - // get the currently selected text and url from the document, and use it to - // create a new creation menu - const { from, to } = props.editor.state.selection; - const selectedText = props.editor.state.doc.textBetween(from, to); - const activeUrl = props.editor.isActive("link") - ? props.editor.getAttributes("link").href || "" - : ""; - setCreationMenu( ); - }, [props.editor]); + }, [props.hyperlinkMarkFunctions]); return ( void, - remove: () => void +export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( + hyperlinkHoverMenuFactoryFunctions: HyperlinkHoverMenuFactoryFunctions ) => { const element = document.createElement("div"); const root = createRoot(element); - const getReferenceClientRect = () => { - const { left, top } = editor.view.coordsAtPos( - editor.state.selection.anchor - ); - - return new DOMRect(left, top); - }; - root.render( ); const menu = tippy(document.body, { duration: 0, - getReferenceClientRect: getReferenceClientRect, + getReferenceClientRect: + hyperlinkHoverMenuFactoryFunctions.view.getHyperlinkBoundingBox, content: element, interactive: true, trigger: "manual", @@ -47,5 +38,12 @@ export const HyperlinkMenuFactory = ( menu.show(); - return element as HTMLElement; + return { + element: element, + show: menu.show, + hide: menu.hide, + update: () => { + menu.popperInstance?.forceUpdate(); + }, + }; }; diff --git a/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx b/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx index b8032182a3..cdf5534e57 100644 --- a/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx @@ -6,14 +6,6 @@ import { HoverHyperlinkMenu } from "../HoverHyperlinkMenu/components/HoverHyperl type HyperlinkMenuProps = { url: string; text: string; - pos: { - height: number; - width: number; - left: number; - right: number; - top: number; - bottom: number; - }; update: (url: string, text: string) => void; remove: () => void; }; From 1a2fd177e098c54ee79196b3d4c410665496f1b6 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Sat, 17 Dec 2022 17:27:48 +0100 Subject: [PATCH 06/55] Added suggestions menu factory with new (hopefully final) factory structure --- examples/editor/src/main.tsx | 2 + packages/core/src/BlockNoteEditor.ts | 9 + .../SlashMenu/SlashMenuExtension.ts | 1 + .../getSuggestionsMenuFactoryFunctions.ts | 22 ++ .../src/menu-tools/SuggestionsMenu/types.ts | 22 ++ packages/core/src/menu-tools/types.ts | 16 +- .../SuggestionListReactRenderer.tsx | 236 ------------------ .../plugins/suggestion/SuggestionPlugin.ts | 193 ++++++++++---- .../suggestion/components/SuggestionGroup.tsx | 47 ---- .../suggestion/components/SuggestionList.tsx | 92 ------- .../components/suggestion/SuggestionList.tsx | 111 ++++++++ .../suggestion/SuggestionListItem.tsx} | 36 +-- .../suggestion/SuggestionsMenuFactory.tsx | 58 +++++ 13 files changed, 401 insertions(+), 444 deletions(-) create mode 100644 packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions.ts create mode 100644 packages/core/src/menu-tools/SuggestionsMenu/types.ts delete mode 100644 packages/core/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx delete mode 100644 packages/core/src/shared/plugins/suggestion/components/SuggestionGroup.tsx delete mode 100644 packages/core/src/shared/plugins/suggestion/components/SuggestionList.tsx create mode 100644 packages/react/src/shared/components/suggestion/SuggestionList.tsx rename packages/{core/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx => react/src/shared/components/suggestion/SuggestionListItem.tsx} (74%) create mode 100644 packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index af60e30ea9..57573de8e4 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -3,6 +3,7 @@ import styles from "./App.module.css"; import { mountBlockNoteEditor } from "../../../packages/core/src/BlockNoteEditor"; import { ReactBubbleMenuFactory } from "../../../packages/react/src/BubbleMenu/BubbleMenuFactory"; import { ReactHyperlinkMenuFactory } from "../../../packages/react/src/HyperlinkMenus/HyperlinkMenuFactory"; +import { ReactSuggestionsMenuFactory } from "../../../packages/react/src/shared/components/suggestion/SuggestionsMenuFactory"; // type WindowWithProseMirror = Window & // typeof globalThis & { ProseMirror: Editor }; @@ -11,6 +12,7 @@ mountBlockNoteEditor( { bubbleMenuFactory: ReactBubbleMenuFactory, hyperlinkMenuFactory: ReactHyperlinkMenuFactory, + suggestionsMenuFactory: ReactSuggestionsMenuFactory, }, { element: document.getElementById("root")!, diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 7b0b1a9aca..a876086e3e 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -5,6 +5,8 @@ import styles from "./editor.module.css"; import rootStyles from "./root.module.css"; import { BubbleMenuFactory } from "./menu-tools/BubbleMenu/types"; import { HyperlinkHoverMenuFactory } from "./menu-tools/HyperlinkHoverMenu/types"; +import { SuggestionsMenuFactory } from "./menu-tools/SuggestionsMenu/types"; +import SuggestionItem from "./shared/plugins/suggestion/SuggestionItem"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; @@ -14,6 +16,7 @@ type BlockNoteEditorOptions = EditorOptions & { export type MenuFactories = { bubbleMenuFactory: BubbleMenuFactory; hyperlinkMenuFactory: HyperlinkHoverMenuFactory; + suggestionsMenuFactory: SuggestionsMenuFactory; }; const blockNoteExtensions = getBlockNoteExtensions(); @@ -45,6 +48,12 @@ export const mountBlockNoteEditor = ( }); } + if (extension.name === "slash-command") { + return extension.configure({ + suggestionsMenuFactory: menuFactories.suggestionsMenuFactory, + }); + } + return extension; }); diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts index e2c0c4bafa..ca51d42415 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts @@ -25,6 +25,7 @@ export const SlashMenuExtension = Extension.create({ pluginKey: SlashMenuPluginKey, editor: this.editor, char: "/", + suggestionsMenuFactory: this.options.suggestionsMenuFactory, items: (query) => { const commands = []; diff --git a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions.ts b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions.ts new file mode 100644 index 0000000000..c53fc7e7d8 --- /dev/null +++ b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions.ts @@ -0,0 +1,22 @@ +import { SuggestionsMenuFactoryFunctions } from "./types"; +import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; + +// TODO: Only need either getQuery or matchesQuery, not both. Depends if we want to allow users the ability to define +// their own block type aliases/matching algorithm. +export function getSuggestionsMenuFactoryFunctions( + items: T[], + selectedItemIndex: number, + itemCallback: (item: T) => void, + selectedBlockBoundingBox: DOMRect +): SuggestionsMenuFactoryFunctions { + return { + menuItems: { + items: items, + selectedItemIndex: selectedItemIndex, + itemCallback: itemCallback, + }, + view: { + selectedBlockBoundingBox: selectedBlockBoundingBox, + }, + }; +} diff --git a/packages/core/src/menu-tools/SuggestionsMenu/types.ts b/packages/core/src/menu-tools/SuggestionsMenu/types.ts new file mode 100644 index 0000000000..8333ccf237 --- /dev/null +++ b/packages/core/src/menu-tools/SuggestionsMenu/types.ts @@ -0,0 +1,22 @@ +import { Menu } from "../types"; +import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; + +export type SuggestionsMenuItem = { + name: string; + set: () => void; +}; + +export type SuggestionsMenuFactoryFunctions = { + menuItems: { + items: T[]; + selectedItemIndex: number; + itemCallback: (item: T) => void; + }; + view: { + selectedBlockBoundingBox: DOMRect; + }; +}; + +export type SuggestionsMenuFactory = ( + functions: SuggestionsMenuFactoryFunctions +) => Menu; diff --git a/packages/core/src/menu-tools/types.ts b/packages/core/src/menu-tools/types.ts index 8a8fb4d924..79606905e4 100644 --- a/packages/core/src/menu-tools/types.ts +++ b/packages/core/src/menu-tools/types.ts @@ -1,6 +1,16 @@ -export type Menu = { +import { BubbleMenuFactoryFunctions } from "./BubbleMenu/types"; +import { HyperlinkHoverMenuFactoryFunctions } from "./HyperlinkHoverMenu/types"; +import { SuggestionsMenuFactoryFunctions } from "./SuggestionsMenu/types"; +import SuggestionItem from "../shared/plugins/suggestion/SuggestionItem"; + +export type MenuUpdateProps = + | BubbleMenuFactoryFunctions + | HyperlinkHoverMenuFactoryFunctions + | SuggestionsMenuFactoryFunctions; + +export type Menu = { element: HTMLElement | undefined; - show: () => void; + show: (props: MenuUpdateProps) => void; hide: () => void; - update: () => void; + update: (newProps: MenuUpdateProps) => void; }; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx b/packages/core/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx deleted file mode 100644 index 7e712ff48e..0000000000 --- a/packages/core/src/shared/plugins/suggestion/SuggestionListReactRenderer.tsx +++ /dev/null @@ -1,236 +0,0 @@ -import { Editor as ReactEditor, ReactRenderer } from "@tiptap/react"; -import { Editor } from "@tiptap/core"; -import tippy, { Instance } from "tippy.js"; -import SuggestionItem from "./SuggestionItem"; -import { - SuggestionList, - SuggestionListProps, -} from "./components/SuggestionList"; -import { BlockNoteTheme } from "../../../BlockNoteTheme"; -import { MantineProvider } from "@mantine/core"; - -/** - * The interface that each suggestion renderer should conform to. - */ -export interface SuggestionRenderer { - /** - * Disposes of the suggestion menu. - */ - onExit?: (props: SuggestionRendererProps) => void; - - /** - * Updates the suggestion menu. - * - * This function should be called when the renderer's `props` change, - * after `onStart` has been called. - */ - onUpdate?: (props: SuggestionRendererProps) => void; - - /** - * Creates and displays a new suggestion menu popup. - */ - onStart?: (props: SuggestionRendererProps) => void; - - /** - * Function for handling key events - */ - onKeyDown?: (event: KeyboardEvent) => boolean; - - /** - * The DOM Element representing the suggestion menu - */ - getComponent: () => Element | undefined; -} - -export type SuggestionRendererProps = { - /** - * Object containing all suggestion items, grouped by their `groupName`. - */ - groups: { - [groupName: string]: T[]; - }; - - /** - * The total number of suggestion-items. - */ - count: number; - - /** - * This callback is executed whenever the user selects an item. - * - * @param item the selected item - */ - onSelectItem: (item: T) => void; - - /** - * A function returning the client rect to use as reference for positioning the suggestion menu popup. - */ - clientRect: (() => DOMRect) | null; - - /** - * This callback is executed when the suggestion menu needs to be closed, - * e.g. when the user presses escape. - */ - onClose: () => void; -}; - -/** - * This function creates a SuggestionRenderer based on TipTap's ReactRenderer utility. - * - * The resulting renderer can be used to display a suggestion menu containing (grouped) suggestion items. - * - * This renderer also takes care of the following key events: - * - Key up/down, for navigating the suggestion menu (selecting different items) - * - Enter for picking the currently selected item and closing the menu - * - Escape to close the menu, without taking action - * - * @param editor the TipTap editor - * @returns the newly constructed SuggestionRenderer - */ -export default function createRenderer( - editor: Editor -): SuggestionRenderer { - let component: ReactRenderer; - let popup: Instance[]; - let componentsDisposedOrDisposing = true; - let selectedIndex = 0; - let props: SuggestionRendererProps | undefined; - - /** - * Helper function to find out what item corresponds to a certain index. - * - * This function might throw an error if the index is invalid, - * or when this function is not called in the proper environment. - * - * @param index the index - * @returns the item that corresponds to the index - */ - const itemByIndex = (index: number): T => { - if (!props) { - throw new Error("props not set"); - } - let currentIndex = 0; - for (const groupName in props.groups) { - const items = props.groups[groupName]; - const groupSize = items.length; - // Check if index lies within this group - if (index < currentIndex + groupSize) { - return items[index - currentIndex]; - } - currentIndex += groupSize; - } - throw Error("item not found"); - }; - - return { - getComponent: () => { - if (!popup || !popup[0]) { - return undefined; - } - // return the tippy container element, this is used to ensure - // that click events inside the menu are handled properly. - return popup[0].reference; - }, - onStart: (newProps) => { - props = newProps; - componentsDisposedOrDisposing = false; - selectedIndex = 0; - const componentProps: SuggestionListProps = { - groups: newProps.groups, - count: newProps.count, - onSelectItem: newProps.onSelectItem, - selectedIndex, - }; - - component = new ReactRenderer( - (props: SuggestionListProps) => ( - - - - ), - { - editor: editor as ReactEditor, - props: componentProps, - } - ); - - popup = tippy("body", { - getReferenceClientRect: newProps.clientRect, - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); - }, - - onUpdate: (newProps) => { - props = newProps; - if (props.groups !== component.props.groups) { - // if the set of items is different (e.g.: by typing / searching), reset the selectedIndex to 0 - selectedIndex = 0; - } - const componentProps: SuggestionListProps = { - groups: props.groups, - count: props.count, - onSelectItem: props.onSelectItem, - selectedIndex, - }; - component.updateProps(componentProps); - - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); - }, - - onKeyDown: (event) => { - if (!props) { - return false; - } - if (event.key === "ArrowUp") { - selectedIndex = (selectedIndex + props.count - 1) % props.count; - component.updateProps({ - selectedIndex, - }); - return true; - } - - if (event.key === "ArrowDown") { - selectedIndex = (selectedIndex + 1) % props.count; - component.updateProps({ - selectedIndex, - }); - return true; - } - - if (event.key === "Enter") { - const item = itemByIndex(selectedIndex); - props.onSelectItem(item); - return true; - } - - if (event.key === "Escape") { - props.onClose(); - return true; - } - return false; - }, - - onExit: (_props) => { - if (componentsDisposedOrDisposing) { - return; - } - // onExit, first hide tippy popup so it shows fade-out - // then (after 1 second, actually destroy resources) - componentsDisposedOrDisposing = true; - const popupToDestroy = popup[0]; - const componentToDestroy = component; - popupToDestroy.hide(); - setTimeout(() => { - popupToDestroy.destroy(); - componentToDestroy.destroy(); - }, 1000); - }, - }; -} diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 67b9372c51..07ebb7a9be 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -1,13 +1,11 @@ import { Editor, Range } from "@tiptap/core"; -import { escapeRegExp, groupBy } from "lodash"; +import { escapeRegExp } from "lodash"; import { Plugin, PluginKey, Selection } from "prosemirror-state"; -import { Decoration, DecorationSet } from "prosemirror-view"; +import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; import SuggestionItem from "./SuggestionItem"; - -import createRenderer, { - SuggestionRendererProps, -} from "./SuggestionListReactRenderer"; +import { SuggestionsMenuFactory } from "../../../menu-tools/SuggestionsMenu/types"; +import { getSuggestionsMenuFactoryFunctions } from "../../../menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions"; export type SuggestionPluginOptions = { /** @@ -27,6 +25,8 @@ export type SuggestionPluginOptions = { */ char: string; + suggestionsMenuFactory: SuggestionsMenuFactory; + /** * The callback that gets executed when an item is selected by the user. * @@ -102,6 +102,7 @@ export function createSuggestionPlugin({ pluginKey, editor, char, + suggestionsMenuFactory, onSelectItem: selectItemCallback = () => {}, items = () => [], }: SuggestionPluginOptions) { @@ -110,7 +111,25 @@ export function createSuggestionPlugin({ throw new Error("'char' should be a single character"); } - const renderer = createRenderer(editor); + const deactivate = (view: EditorView) => { + view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true })); + }; + + const suggestionsMenu = suggestionsMenuFactory( + getSuggestionsMenuFactoryFunctions( + [], + 0, + (item) => { + deactivate(editor.view); + selectItemCallback({ + item: item, + editor: editor, + range: { from: 0, to: 0 }, + }); + }, + new DOMRect() + ) + ); // Plugin key is passed in parameter so it can be exported and used in draghandle return new Plugin({ @@ -120,8 +139,7 @@ export function createSuggestionPlugin({ // prevent blurring when clicking with the mouse inside the popup menu const blurMeta = transaction.getMeta("blur"); if (blurMeta?.event.relatedTarget) { - const c = renderer.getComponent(); - if (c?.contains(blurMeta.event.relatedTarget)) { + if (suggestionsMenu.element?.contains(blurMeta.event.relatedTarget)) { return false; } } @@ -149,49 +167,48 @@ export function createSuggestionPlugin({ `[data-decoration-id="${state.decorationId}"]` ); - const groups: { [groupName: string]: T[] } = groupBy( - state.items, - "groupName" - ); - - const deactivate = () => { - view.dispatch( - view.state.tr.setMeta(pluginKey, { deactivate: true }) - ); - }; - - const rendererProps: SuggestionRendererProps = { - groups: changed || started ? groups : {}, - count: state.items.length, - onSelectItem: (item: T) => { - deactivate(); - selectItemCallback({ - item, - editor, - range: state.range, - }); - }, - // virtual node for popper.js or tippy.js - // this can be used for building popups without a DOM node - clientRect: decorationNode - ? () => decorationNode.getBoundingClientRect() - : null, - onClose: () => { - deactivate(); - renderer.onExit?.(rendererProps); - }, - }; - if (stopped) { - renderer.onExit?.(rendererProps); + suggestionsMenu.hide(); } if (changed) { - renderer.onUpdate?.(rendererProps); + suggestionsMenu.update( + getSuggestionsMenuFactoryFunctions( + next.items, + 0, + (item) => { + deactivate(editor.view); + selectItemCallback({ + item: item, + editor: editor, + range: state.range, + }); + }, + decorationNode !== null + ? decorationNode.getBoundingClientRect() + : new DOMRect() + ) + ); } if (started) { - renderer.onStart?.(rendererProps); + suggestionsMenu.show( + getSuggestionsMenuFactoryFunctions( + next.items, + 0, + (item) => { + deactivate(editor.view); + selectItemCallback({ + item: item, + editor: editor, + range: state.range, + }); + }, + decorationNode !== null + ? decorationNode.getBoundingClientRect() + : new DOMRect() + ) + ); } }, }; @@ -206,6 +223,7 @@ export function createSuggestionPlugin({ query: null as string | null, notFoundCount: 0, items: [] as T[], + selectedItemIndex: 0, type: "slash", decorationId: null as string | null, }; @@ -221,6 +239,47 @@ export function createSuggestionPlugin({ return next; } + // Handles transactions created by navigating the menu using the up/down arrow keys. + if ( + transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined + ) { + let newIndex = + transaction.getMeta(pluginKey).selectedItemIndexChanged; + + if (newIndex < 0) { + newIndex = prev.items.length - 1; + } + + if (newIndex >= prev.items.length) { + newIndex = 0; + } + + next.selectedItemIndex = newIndex; + + const decorationNode = document.querySelector( + `[data-decoration-id="${next.decorationId}"]` + ); + + suggestionsMenu.update( + getSuggestionsMenuFactoryFunctions( + next.items, + next.selectedItemIndex, + (item) => { + selectItemCallback({ + item: item, + editor: editor, + range: next.range, + }); + }, + decorationNode !== null + ? decorationNode.getBoundingClientRect() + : new DOMRect() + ) + ); + + return next; + } + if ( // only show popup if selection is a blinking cursor selection.from === selection.to && @@ -247,6 +306,7 @@ export function createSuggestionPlugin({ next.query = ""; next.active = true; next.type = transaction.getMeta(pluginKey)?.type; + next.selectedItemIndex = 0; } else if (prev.active) { // Try to match against where our cursor currently is // if the type is slash we get the command after the character @@ -263,6 +323,7 @@ export function createSuggestionPlugin({ next.active = true; next.decorationId = prev.decorationId; next.query = match.query; + next.selectedItemIndex = 0; } } else { next.active = false; @@ -319,12 +380,46 @@ export function createSuggestionPlugin({ // return true to cancel the original event, as we insert / ourselves return true; } - return false; + } else { + const { items, range, selectedItemIndex } = pluginKey.getState( + view.state + ); + + if (event.key === "ArrowUp") { + view.dispatch( + view.state.tr.setMeta(pluginKey, { + selectedItemIndexChanged: selectedItemIndex - 1, + }) + ); + return true; + } + + if (event.key === "ArrowDown") { + view.dispatch( + view.state.tr.setMeta(pluginKey, { + selectedItemIndexChanged: selectedItemIndex + 1, + }) + ); + return true; + } + + if (event.key === "Enter") { + deactivate(view); + selectItemCallback({ + item: items[selectedItemIndex], + editor: editor, + range: range, + }); + return true; + } + + if (event.key === "Escape") { + deactivate(view); + return true; + } } - // pass the key event onto the renderer (to handle arrow keys, enter and escape) - // return true if the event got handled by the renderer or false otherwise - return renderer.onKeyDown?.(event) || false; + return false; }, // Setup decorator on the currently active suggestion. diff --git a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroup.tsx b/packages/core/src/shared/plugins/suggestion/components/SuggestionGroup.tsx deleted file mode 100644 index 716721fdcd..0000000000 --- a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroup.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Menu } from "@mantine/core"; -import SuggestionItem from "../SuggestionItem"; -import { SuggestionGroupItem } from "./SuggestionGroupItem"; - -type SuggestionGroupProps = { - /** - * Name of the group - */ - name: string; - - /** - * The list of items - */ - items: T[]; - - /** - * Index of the selected item in this group; relative to this item group (so 0 refers to the first item in this group) - * This should be 'undefined' if none of the items in this group are selected - */ - selectedIndex?: number; - - /** - * Callback that gets executed when an item is clicked on. - */ - clickItem: (item: T) => void; -}; - -export function SuggestionGroup( - props: SuggestionGroupProps -) { - return ( - <> - {props.name} - {props.items.map((item, index) => { - return ( - - ); - })} - - ); -} diff --git a/packages/core/src/shared/plugins/suggestion/components/SuggestionList.tsx b/packages/core/src/shared/plugins/suggestion/components/SuggestionList.tsx deleted file mode 100644 index 7b4a043126..0000000000 --- a/packages/core/src/shared/plugins/suggestion/components/SuggestionList.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { createStyles, Menu } from "@mantine/core"; -import { SuggestionGroup } from "./SuggestionGroup"; -import SuggestionItem from "../SuggestionItem"; - -export type SuggestionListProps = { - // Object containing all suggestion items, grouped by their `groupName`. - groups: { - [groupName: string]: T[]; - }; - - //The total number of suggestion-items - count: number; - - // This callback gets executed whenever an item is clicked on - onSelectItem: (item: T) => void; - - // The index of the item that is currently selected (but not yet clicked on) - selectedIndex: number; -}; - -// Stateless component that renders the suggestion list -export function SuggestionList( - props: SuggestionListProps -) { - const { classes } = createStyles({ root: {} })(undefined, { - name: "SuggestionList", - }); - - const renderedGroups = []; - - let currentGroupIndex = 0; - - for (const groupName in props.groups) { - const items = props.groups[groupName]; - - renderedGroups.push( - = currentGroupIndex - ? props.selectedIndex - currentGroupIndex - : undefined - } - clickItem={props.onSelectItem}> - ); - - currentGroupIndex += items.length; - } - - return ( - - - {renderedGroups.length > 0 ? ( - renderedGroups - ) : ( - No match found - )} - - - - // doesn't work well yet, maybe https://github.com/atomiks/tippyjs-react/issues/173 - // We now render the tippy element manually in SuggestionListReactRenderer - // - // - // {renderedGroups.length > 0 ? ( - // renderedGroups - // ) : ( - //
- // )} - //
- //
- // } - // interactive={false}> - //
- // - ); -} diff --git a/packages/react/src/shared/components/suggestion/SuggestionList.tsx b/packages/react/src/shared/components/suggestion/SuggestionList.tsx new file mode 100644 index 0000000000..b1ae8a8088 --- /dev/null +++ b/packages/react/src/shared/components/suggestion/SuggestionList.tsx @@ -0,0 +1,111 @@ +import { createStyles, Menu } from "@mantine/core"; +import { SuggestionListItem } from "./SuggestionListItem"; +import { SuggestionsMenuFactoryFunctions } from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; +import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; + +export type SuggestionListProps = + SuggestionsMenuFactoryFunctions; + +export function SuggestionList( + props: SuggestionListProps +) { + const { classes } = createStyles({ root: {} })(undefined, { + name: "SuggestionList", + }); + console.log(props.menuItems.items); + + const headingGroup = []; + const basicBlockGroup = []; + + for (const item of props.menuItems.items) { + console.log(item.name); + + if (item.name === "Heading") { + headingGroup.push(item); + } + + if (item.name === "Heading 2") { + headingGroup.push(item); + } + + if (item.name === "Heading 3") { + headingGroup.push(item); + } + + if (item.name === "Numbered List") { + basicBlockGroup.push(item); + } + + if (item.name === "Bullet List") { + basicBlockGroup.push(item); + } + + if (item.name === "Paragraph") { + basicBlockGroup.push(item); + } + } + + const renderedItems = []; + let index = 0; + + if (headingGroup.length > 0) { + renderedItems.push( + {"Headings"} + ); + + for (const item of headingGroup) { + renderedItems.push( + props.menuItems.itemCallback(item)} + /> + ); + index++; + } + } + + if (basicBlockGroup.length > 0) { + renderedItems.push( + {"Basic Blocks"} + ); + + for (const item of basicBlockGroup) { + renderedItems.push( + props.menuItems.itemCallback(item)} + /> + ); + index++; + } + } + + return ( + + + {renderedItems.length > 0 ? ( + renderedItems + ) : ( + No match found + )} + + + ); +} diff --git a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx b/packages/react/src/shared/components/suggestion/SuggestionListItem.tsx similarity index 74% rename from packages/core/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx rename to packages/react/src/shared/components/suggestion/SuggestionListItem.tsx index 11967985a0..b89ae8c442 100644 --- a/packages/core/src/shared/plugins/suggestion/components/SuggestionGroupItem.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionListItem.tsx @@ -1,27 +1,28 @@ -import SuggestionItem from "../SuggestionItem"; +// import SuggestionItem from "../SuggestionItem"; import { useEffect, useRef } from "react"; import { Badge, createStyles, Menu, Stack, Text } from "@mantine/core"; +import { IconType } from "react-icons"; const MIN_LEFT_MARGIN = 5; -export type SuggestionGroupItemProps = { - item: T; - index: number; - selectedIndex?: number; - clickItem: (item: T) => void; +export type SuggestionGroupItemProps = { + name: string; + hint: string; + icon: IconType; + shortcut?: string; + isSelected: boolean; + set: () => void; }; -export function SuggestionGroupItem( - props: SuggestionGroupItemProps -) { +export function SuggestionListItem(props: SuggestionGroupItemProps) { const itemRef = useRef(null); const { classes } = createStyles({ root: {} })(undefined, { name: "SuggestionListItem", }); function isSelected() { - const isKeyboardSelected = - props.selectedIndex !== undefined && props.selectedIndex === props.index; + const isKeyboardSelected = props.isSelected; + // props.selectedIndex !== undefined && props.selectedIndex === props.index; const isMouseSelected = itemRef.current?.matches(":hover"); return isKeyboardSelected || isMouseSelected; @@ -53,29 +54,30 @@ export function SuggestionGroupItem( } }); - const Icon = props.item.icon; + const Icon = props.icon; return ( } - onClick={() => props.clickItem(props.item)} + onClick={props.set} + closeMenuOnClick={false} // Ensures an item selected with both mouse & keyboard doesn't get deselected on mouse leave. onMouseLeave={() => { setTimeout(() => { updateSelection(); - }); + }, 1); }} ref={itemRef} rightSection={ - props.item.shortcut && {props.item.shortcut} + props.shortcut && {props.shortcut} }> {/*Might need separate classes.*/} - {props.item.name} + {props.name} - {props.item.hint} + {props.hint} ); diff --git a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx new file mode 100644 index 0000000000..b50b41354d --- /dev/null +++ b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx @@ -0,0 +1,58 @@ +import { createRoot } from "react-dom/client"; +import { MantineProvider } from "@mantine/core"; +import { BlockNoteTheme } from "../../../BlockNoteTheme"; +import tippy from "tippy.js"; +import { + SuggestionsMenuFactory, + SuggestionsMenuFactoryFunctions, +} from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; +import { SuggestionList } from "./SuggestionList"; +import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; +// import rootStyles from "../../../core/src/root.module.css"; + +export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< + SuggestionItem +> = () => { + const element = document.createElement("div"); + // element.className = rootStyles.bnRoot; + const root = createRoot(element); + + const menu = tippy(document.body, { + duration: 0, + getReferenceClientRect: () => new DOMRect(), + content: element, + interactive: true, + trigger: "manual", + placement: "bottom-start", + hideOnClick: "toggle", + }); + + return { + element: element as HTMLElement, + show: (props: SuggestionsMenuFactoryFunctions) => { + root.render( + + + + ); + + menu.props.getReferenceClientRect = () => + props.view.selectedBlockBoundingBox; + + menu.show(); + }, + update: (newProps: SuggestionsMenuFactoryFunctions) => { + root.render( + + + + ); + + menu.props.getReferenceClientRect = () => + newProps.view.selectedBlockBoundingBox; + }, + hide: () => { + menu.hide(); + }, + }; +}; From b4c5dcc3a38bfcc5bd6284e00ae25988a91e8bf7 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 19 Dec 2022 17:29:25 +0100 Subject: [PATCH 07/55] Refactored bubble menu code to use improved factory structure --- .../BubbleMenu/BubbleMenuExtension.tsx | 5 +- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 301 +++++++++--------- ...toryFunctions.ts => getBubbleMenuProps.ts} | 40 ++- .../core/src/menu-tools/BubbleMenu/types.ts | 51 +-- packages/core/src/menu-tools/types.ts | 18 +- .../src/BubbleMenu/BubbleMenuFactory.tsx | 38 ++- .../src/BubbleMenu/components/BubbleMenu.tsx | 96 +++--- .../components/LinkToolbarButton.tsx | 16 +- 8 files changed, 262 insertions(+), 303 deletions(-) rename packages/core/src/menu-tools/BubbleMenu/{getBubbleMenuFactoryFunctions.ts => getBubbleMenuProps.ts} (77%) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx index cda65f6c9e..7b72b47788 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx @@ -1,12 +1,13 @@ -import { Editor, Extension } from "@tiptap/core"; +import { Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; import { createBubbleMenuPlugin } from "./BubbleMenuPlugin"; +import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; /** * The menu that is displayed when selecting a piece of text. */ export const BubbleMenuExtension = Extension.create<{ - bubbleMenuFactory: (editor: Editor) => HTMLElement; + bubbleMenuFactory: BubbleMenuFactory; }>({ name: "BubbleMenuExtension", diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index bb5073b503..be2cee69d4 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -2,13 +2,12 @@ import { Editor, isTextSelection } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; -import { Menu } from "../../menu-tools/types"; -import { getBubbleMenuFactoryFunctions } from "../../menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions"; +import { getBubbleMenuProps } from "../../menu-tools/BubbleMenu/getBubbleMenuProps"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files export interface BubbleMenuPluginProps { - pluginKey: PluginKey | string; + pluginKey: PluginKey; editor: Editor; bubbleMenuFactory: BubbleMenuFactory; shouldShow?: @@ -23,186 +22,170 @@ export interface BubbleMenuPluginProps { | null; } -export type BubbleMenuViewProps = BubbleMenuPluginProps & { - view: EditorView; -}; - -export class BubbleMenuView { - public editor: Editor; - - public bubbleMenuFactory: BubbleMenuFactory; - - public bubbleMenu: Menu; - - public view: EditorView; - - public preventHide = false; - - public preventShow = false; - - public shouldShow: Exclude = ({ - view, - state, - from, - to, - }) => { - const { doc, selection } = state; - const { empty } = selection; - - // Sometime check for `empty` is not enough. - // Doubleclick an empty paragraph returns a node size of 2. - // So we check also for an empty text size. - const isEmptyTextBlock = - !doc.textBetween(from, to).length && isTextSelection(state.selection); - - if (!view.hasFocus() || empty || isEmptyTextBlock) { - return false; - } - - return true; +export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { + const bubbleMenu = options.bubbleMenuFactory( + getBubbleMenuProps(options.editor) + ); + + // TODO: Is this callback needed? + const mousedownHandler = (_view: EditorView) => { + // view.dispatch( + // view.state.tr.setMeta(options.pluginKey, { + // preventHide: true, + // }) + // ); }; - constructor({ - editor, - bubbleMenuFactory, - view, - shouldShow, - }: BubbleMenuViewProps) { - this.editor = editor; - this.bubbleMenuFactory = bubbleMenuFactory; - this.bubbleMenu = this.bubbleMenuFactory( - getBubbleMenuFactoryFunctions(editor) + const viewMousedownHandler = (view: EditorView) => { + view.dispatch( + view.state.tr.setMeta(options.pluginKey, { + preventShow: true, + }) ); - this.view = view; - - if (shouldShow) { - this.shouldShow = shouldShow; - } - - this.addEditorListeners(); - } - - mousedownHandler = () => { - this.preventHide = true; - }; - - viewMousedownHandler = () => { - this.preventShow = true; }; - viewMouseupHandler = () => { - this.preventShow = false; - setTimeout(() => this.update(this.editor.view)); + const viewMouseupHandler = (view: EditorView) => { + view.dispatch( + view.state.tr.setMeta(options.pluginKey, { + preventShow: false, + }) + ); }; - dragstartHandler = () => { - this.hideMenu(); + const dragstartHandler = () => { + bubbleMenu.hide(); }; - focusHandler = () => { + // TODO: Is this callback needed? + const focusHandler = () => { // we use `setTimeout` to make sure `selection` is already updated - setTimeout(() => this.update(this.editor.view)); + // setTimeout(() => this.update(this.editor.view)); }; - blurHandler = ({ event }: { event: FocusEvent }) => { - if (this.preventHide) { - this.preventHide = false; + const blurHandler = ({ event }: { event: FocusEvent }, view: EditorView) => { + const pluginState = options.pluginKey.getState(view.state); + + if (pluginState.preventHide) { + pluginState.preventHide = false; return; } if ( event?.relatedTarget && - this.bubbleMenu.element?.parentNode?.contains(event.relatedTarget as Node) + bubbleMenu.element?.parentNode?.contains(event.relatedTarget as Node) ) { return; } - this.hideMenu(); + bubbleMenu.hide(); }; - update(view: EditorView, oldState?: EditorState) { - const { state, composing } = view; - const { doc, selection } = state; - const isSame = - oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); - - if (composing || isSame) { - return; - } - - // support for CellSelections - const { ranges } = selection; - const from = Math.min(...ranges.map((range) => range.$from.pos)); - const to = Math.max(...ranges.map((range) => range.$to.pos)); - - const shouldShow = this.shouldShow?.({ - editor: this.editor, - view, - state, - oldState, - from, - to, - }); - - if (!shouldShow || this.preventShow) { - this.hideMenu(); - return; - } - - this.showMenu(); - this.bubbleMenu.update(); - } - - showMenu() { - this.bubbleMenu.show(); - - this.bubbleMenu.element!.style.visibility = "visible"; - this.bubbleMenu.element!.addEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); - } - - hideMenu() { - this.bubbleMenu.hide(); - - this.bubbleMenu.element!.removeEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); - } - - addEditorListeners() { - this.view.dom.addEventListener("mousedown", this.viewMousedownHandler); - this.view.dom.addEventListener("mouseup", this.viewMouseupHandler); - this.view.dom.addEventListener("dragstart", this.dragstartHandler); - - this.editor.on("focus", this.focusHandler); - this.editor.on("blur", this.blurHandler); - } - - removeEditorListeners() { - this.hideMenu(); - - this.view.dom.removeEventListener("mousedown", this.viewMousedownHandler); - this.view.dom.removeEventListener("mouseup", this.viewMouseupHandler); - this.view.dom.removeEventListener("dragstart", this.dragstartHandler); - - this.editor.off("focus", this.focusHandler); - this.editor.off("blur", this.blurHandler); - } -} - -export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { return new Plugin({ - key: new PluginKey("BubbleMenuPlugin"), - view: (view) => new BubbleMenuView({ view, ...options }), + key: options.pluginKey, + view: (view) => { + view.dom.addEventListener("mousedown", () => viewMousedownHandler(view)); + view.dom.addEventListener("mouseup", () => viewMouseupHandler(view)); + view.dom.addEventListener("dragstart", dragstartHandler); + + options.editor.on("focus", focusHandler); + options.editor.on("blur", ({ event }: { event: FocusEvent }) => + blurHandler({ event }, view) + ); + + return { + update: (view, prevState) => { + const prev = options.pluginKey.getState(prevState); + const next = options.pluginKey.getState(view.state); + + if (!next.preventShow) { + if (!prev.show && next.show) { + bubbleMenu.show(getBubbleMenuProps(options.editor)); + + bubbleMenu.element!.addEventListener( + "mousedown", + () => mousedownHandler(view), + { + capture: true, + } + ); + } + + if (prev.show && next.show) { + console.log("UPDATE"); + console.log(options.editor.isActive("bold")); + bubbleMenu.update(getBubbleMenuProps(options.editor)); + } + } + + if (!next.preventHide) { + if (prev.show && !next.show) { + bubbleMenu.hide(); + + bubbleMenu.element!.removeEventListener( + "mousedown", + () => mousedownHandler(view), + { + capture: true, + } + ); + } + } + }, + }; + }, + state: { + init: () => { + return { + show: false, + preventShow: false, + preventHide: false, + }; + }, + apply: (tr, prev, _oldState, state) => { + const next = { ...prev }; + + if (tr.getMeta(options.pluginKey)?.preventShow !== undefined) { + next.preventShow = tr.getMeta(options.pluginKey).preventShow; + } + + if (tr.getMeta(options.pluginKey)?.preventHide !== undefined) { + next.preventHide = tr.getMeta(options.pluginKey).preventHide; + } + + const { doc, selection } = state; + + // const isSame = + // oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); + // + // if (isSame && !prev.preventHide) { + // next.show = false; + // return next; + // } + + // Support for CellSelections + const { ranges, empty } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const to = Math.max(...ranges.map((range) => range.$to.pos)); + + // Sometime check for `empty` is not enough. + // Doubleclick an empty paragraph returns a node size of 2. + // So we check also for an empty text size. + const isEmptyTextBlock = + !doc.textBetween(from, to).length && isTextSelection(state.selection); + + if ((empty || isEmptyTextBlock) && !next.preventHide) { + next.show = false; + return next; + } + + if (!next.preventShow) { + next.show = true; + return next; + } + + return next; + }, + }, }); }; diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts similarity index 77% rename from packages/core/src/menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions.ts rename to packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts index 96ea572785..8e690c2f8d 100644 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuFactoryFunctions.ts +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts @@ -1,47 +1,44 @@ import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; -import { BubbleMenuFactoryFunctions } from "./types"; +import { BubbleMenuProps } from "./types"; -export function getBubbleMenuFactoryFunctions( - editor: Editor -): BubbleMenuFactoryFunctions { +export function getBubbleMenuProps(editor: Editor): BubbleMenuProps { return { marks: { bold: { - isActive: () => editor.isActive("bold"), + isActive: editor.isActive("bold"), toggle: () => { editor.view.focus(); editor.commands.toggleBold(); }, }, italic: { - isActive: () => editor.isActive("italic"), + isActive: editor.isActive("italic"), toggle: () => { editor.view.focus(); editor.commands.toggleItalic(); }, }, underline: { - isActive: () => editor.isActive("underline"), + isActive: editor.isActive("underline"), toggle: () => { editor.view.focus(); editor.commands.toggleUnderline(); }, }, strike: { - isActive: () => editor.isActive("strike"), + isActive: editor.isActive("strike"), toggle: () => { editor.view.focus(); editor.commands.toggleStrike(); }, }, hyperlink: { - isActive: () => editor.isActive("link"), - getUrl: () => editor.getAttributes("link").href, - getText: () => { - const { from, to } = editor.state.selection; - - return editor.state.doc.textBetween(from, to); - }, + isActive: editor.isActive("link"), + url: editor.getAttributes("link").href, + text: editor.state.doc.textBetween( + editor.state.selection.from, + editor.state.selection.to + ), set: (url: string, text?: string) => { if (url === "") { return; @@ -65,7 +62,7 @@ export function getBubbleMenuFactoryFunctions( }, blocks: { paragraph: { - isActive: () => + isActive: editor.state.selection.$from.node().type.name === "textContent", set: () => { editor.view.focus(); @@ -76,10 +73,9 @@ export function getBubbleMenuFactoryFunctions( }, }, heading: { - isActive: () => + isActive: editor.state.selection.$from.node().type.name === "headingContent", - getLevel: () => - editor.state.selection.$from.node().attrs["headingLevel"], + level: editor.state.selection.$from.node().attrs["headingLevel"], set: (level: string = "1") => { editor.view.focus(); editor.commands.BNSetContentType( @@ -92,10 +88,9 @@ export function getBubbleMenuFactoryFunctions( }, }, listItem: { - isActive: () => + isActive: editor.state.selection.$from.node().type.name === "listItemContent", - getType: () => - editor.state.selection.$from.node().attrs["listItemType"], + type: editor.state.selection.$from.node().attrs["listItemType"], set: (type: string = "unordered") => { editor.view.focus(); editor.commands.BNSetContentType( @@ -109,6 +104,7 @@ export function getBubbleMenuFactoryFunctions( }, }, view: { + // TODO: Define function in plugin instead and pass it as an argument? getSelectionBoundingBox: () => { const { state } = editor.view; const { selection } = state; diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 347a3c6fce..07372344c1 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -1,46 +1,46 @@ -import { Menu } from "../types"; +import { Menu, MenuFactory } from "../types"; -export type BasicMarkFunctions = { - isActive: () => boolean; +export type BasicMarkProps = { + isActive: boolean; toggle: () => void; }; -export type HyperlinkMarkFunctions = { - isActive: () => boolean; - getUrl: () => string; - getText: () => string; +export type HyperlinkMarkProps = { + isActive: boolean; + url: string; + text: string; set: (url: string, text?: string) => void; }; -export type ParagraphBlockFunctions = { - isActive: () => boolean; +export type ParagraphBlockProps = { + isActive: boolean; set: () => void; }; -export type HeadingBlockFunctions = { - isActive: () => boolean; - getLevel: () => string; +export type HeadingBlockProps = { + isActive: boolean; + level: string; set: (level: string) => void; }; -export type ListItemBlockFunctions = { - isActive: () => boolean; - getType: () => string; +export type ListItemBlockProps = { + isActive: boolean; + type: string; set: (type: string) => void; }; -export type BubbleMenuFactoryFunctions = { +export type BubbleMenuProps = { marks: { - bold: BasicMarkFunctions; - italic: BasicMarkFunctions; - underline: BasicMarkFunctions; - strike: BasicMarkFunctions; - hyperlink: HyperlinkMarkFunctions; + bold: BasicMarkProps; + italic: BasicMarkProps; + underline: BasicMarkProps; + strike: BasicMarkProps; + hyperlink: HyperlinkMarkProps; }; blocks: { - paragraph: ParagraphBlockFunctions; - heading: HeadingBlockFunctions; - listItem: ListItemBlockFunctions; + paragraph: ParagraphBlockProps; + heading: HeadingBlockProps; + listItem: ListItemBlockProps; }; view: { getSelectionBoundingBox: () => DOMRect; @@ -48,4 +48,5 @@ export type BubbleMenuFactoryFunctions = { }; }; -export type BubbleMenuFactory = (functions: BubbleMenuFactoryFunctions) => Menu; +export type BubbleMenu = Menu; +export type BubbleMenuFactory = MenuFactory; diff --git a/packages/core/src/menu-tools/types.ts b/packages/core/src/menu-tools/types.ts index 79606905e4..3b610b46e1 100644 --- a/packages/core/src/menu-tools/types.ts +++ b/packages/core/src/menu-tools/types.ts @@ -1,16 +1,8 @@ -import { BubbleMenuFactoryFunctions } from "./BubbleMenu/types"; -import { HyperlinkHoverMenuFactoryFunctions } from "./HyperlinkHoverMenu/types"; -import { SuggestionsMenuFactoryFunctions } from "./SuggestionsMenu/types"; -import SuggestionItem from "../shared/plugins/suggestion/SuggestionItem"; - -export type MenuUpdateProps = - | BubbleMenuFactoryFunctions - | HyperlinkHoverMenuFactoryFunctions - | SuggestionsMenuFactoryFunctions; - -export type Menu = { +export type Menu = { element: HTMLElement | undefined; - show: (props: MenuUpdateProps) => void; + show: (props: MenuProps) => void; hide: () => void; - update: (newProps: MenuUpdateProps) => void; + update: (newProps: MenuProps) => void; }; + +export type MenuFactory = (props: MenuProps) => Menu; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index db4a1c358b..27a5709d7d 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -1,30 +1,25 @@ import { createRoot } from "react-dom/client"; import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; -import { BubbleMenu } from "./components/BubbleMenu"; +import { BubbleMenu as ReactBubbleMenu } from "./components/BubbleMenu"; import tippy from "tippy.js"; import { + BubbleMenu, BubbleMenuFactory, - BubbleMenuFactoryFunctions, + BubbleMenuProps, } from "../../../core/src/menu-tools/BubbleMenu/types"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactBubbleMenuFactory: BubbleMenuFactory = ( - bubbleMenuFactoryFunctions: BubbleMenuFactoryFunctions -) => { + props: BubbleMenuProps +): BubbleMenu => { const element = document.createElement("div"); // element.className = rootStyles.bnRoot; const root = createRoot(element); - root.render( - - - - ); - let menu = tippy(bubbleMenuFactoryFunctions.view.getEditorElement(), { + let menu = tippy(props.view.getEditorElement(), { duration: 0, - getReferenceClientRect: - bubbleMenuFactoryFunctions.view.getSelectionBoundingBox, + getReferenceClientRect: props.view.getSelectionBoundingBox, content: element, interactive: true, trigger: "manual", @@ -34,12 +29,25 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( return { element: element as HTMLElement, - show: menu.show, + show: (props: BubbleMenuProps) => { + root.render( + + + + ); + menu.show(); + }, hide: menu.hide, // BubbleMenu React component updates its UI elements automatically with useState hooks, so we only need to ensure // the tippy menu updates its position. - update: () => { - menu.popperInstance?.forceUpdate(); + update: (newProps: BubbleMenuProps) => { + root.render( + + + + ); + // Waits one second for animation to complete. Can be a bit clunky. + setTimeout(() => menu.popperInstance?.forceUpdate(), 1000); }, }; }; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 138ea78c0d..0ada8331f9 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -16,55 +16,50 @@ import { import { ToolbarButton } from "../../shared/components/toolbar/ToolbarButton"; import { ToolbarDropdown } from "../../shared/components/toolbar/ToolbarDropdown"; import { Toolbar } from "../../shared/components/toolbar/Toolbar"; -import { useState } from "react"; import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; -import { BubbleMenuFactoryFunctions } from "../../../../core/src/menu-tools/BubbleMenu/types"; +import { BubbleMenuProps } from "../../../../core/src/menu-tools/BubbleMenu/types"; // TODO: add list options, indentation -export const BubbleMenu = (props: { - bubbleMenuFunctions: BubbleMenuFactoryFunctions; -}) => { +export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { const getActiveMarks = () => { const activeMarks = new Set(); + console.log(props.bubbleMenuProps.marks.bold.isActive); - props.bubbleMenuFunctions.marks.bold.isActive() && activeMarks.add("bold"); - props.bubbleMenuFunctions.marks.italic.isActive() && - activeMarks.add("italic"); - props.bubbleMenuFunctions.marks.underline.isActive() && + props.bubbleMenuProps.marks.bold.isActive && activeMarks.add("bold"); + props.bubbleMenuProps.marks.italic.isActive && activeMarks.add("italic"); + props.bubbleMenuProps.marks.underline.isActive && activeMarks.add("underline"); - props.bubbleMenuFunctions.marks.strike.isActive() && - activeMarks.add("strike"); - props.bubbleMenuFunctions.marks.hyperlink.isActive() && - activeMarks.add("link"); + props.bubbleMenuProps.marks.strike.isActive && activeMarks.add("strike"); + props.bubbleMenuProps.marks.hyperlink.isActive && activeMarks.add("link"); return activeMarks; }; const getActiveBlock = () => { - if (props.bubbleMenuFunctions.blocks.paragraph.isActive()) { + if (props.bubbleMenuProps.blocks.paragraph.isActive) { return { text: "Text", icon: RiText, }; } - if (props.bubbleMenuFunctions.blocks.heading.isActive()) { - if (props.bubbleMenuFunctions.blocks.heading.getLevel() === "1") { + if (props.bubbleMenuProps.blocks.heading.isActive) { + if (props.bubbleMenuProps.blocks.heading.level === "1") { return { text: "Heading 1", icon: RiH1, }; } - if (props.bubbleMenuFunctions.blocks.heading.getLevel() === "2") { + if (props.bubbleMenuProps.blocks.heading.level === "2") { return { text: "Heading 2", icon: RiH2, }; } - if (props.bubbleMenuFunctions.blocks.heading.getLevel() === "3") { + if (props.bubbleMenuProps.blocks.heading.level === "3") { return { text: "Heading 3", icon: RiH3, @@ -72,8 +67,8 @@ export const BubbleMenu = (props: { } } - if (props.bubbleMenuFunctions.blocks.listItem.isActive()) { - if (props.bubbleMenuFunctions.blocks.listItem.getType() === "unordered") { + if (props.bubbleMenuProps.blocks.listItem.isActive) { + if (props.bubbleMenuProps.blocks.listItem.type === "unordered") { return { text: "Bullet List", icon: RiListUnordered, @@ -89,8 +84,8 @@ export const BubbleMenu = (props: { return undefined; }; - const [activeMarks, setActiveMarks] = useState(getActiveMarks()); - const [activeBlock, setActiveBlock] = useState(getActiveBlock()); + const activeMarks = getActiveMarks(); + const activeBlock = getActiveBlock(); return ( @@ -101,75 +96,67 @@ export const BubbleMenu = (props: { { onClick: () => { // Setting editor focus using a chained command instead causes bubble menu to flicker on click. - props.bubbleMenuFunctions.blocks.paragraph.set(); - setActiveBlock(getActiveBlock()); + props.bubbleMenuProps.blocks.paragraph.set(); }, text: "Text", icon: RiText, - isSelected: props.bubbleMenuFunctions.blocks.paragraph.isActive(), + isSelected: props.bubbleMenuProps.blocks.paragraph.isActive, }, { onClick: () => { - props.bubbleMenuFunctions.blocks.heading.set("1"); - setActiveBlock(getActiveBlock()); + props.bubbleMenuProps.blocks.heading.set("1"); }, text: "Heading 1", icon: RiH1, isSelected: - props.bubbleMenuFunctions.blocks.heading.isActive() && - props.bubbleMenuFunctions.blocks.heading.getLevel() === "1", + props.bubbleMenuProps.blocks.heading.isActive && + props.bubbleMenuProps.blocks.heading.level === "1", }, { onClick: () => { - props.bubbleMenuFunctions.blocks.heading.set("2"); - setActiveBlock(getActiveBlock()); + props.bubbleMenuProps.blocks.heading.set("2"); }, text: "Heading 2", icon: RiH2, isSelected: - props.bubbleMenuFunctions.blocks.heading.isActive() && - props.bubbleMenuFunctions.blocks.heading.getLevel() === "2", + props.bubbleMenuProps.blocks.heading.isActive && + props.bubbleMenuProps.blocks.heading.level === "2", }, { onClick: () => { - props.bubbleMenuFunctions.blocks.heading.set("3"); - setActiveBlock(getActiveBlock()); + props.bubbleMenuProps.blocks.heading.set("3"); }, text: "Heading 3", icon: RiH3, isSelected: - props.bubbleMenuFunctions.blocks.heading.isActive() && - props.bubbleMenuFunctions.blocks.heading.getLevel() === "3", + props.bubbleMenuProps.blocks.heading.isActive && + props.bubbleMenuProps.blocks.heading.level === "3", }, { onClick: () => { - props.bubbleMenuFunctions.blocks.listItem.set("unordered"); - setActiveBlock(getActiveBlock()); + props.bubbleMenuProps.blocks.listItem.set("unordered"); }, text: "Bullet List", icon: RiListUnordered, isSelected: - props.bubbleMenuFunctions.blocks.listItem.isActive() && - props.bubbleMenuFunctions.blocks.listItem.getType() === - "unordered", + props.bubbleMenuProps.blocks.listItem.isActive && + props.bubbleMenuProps.blocks.listItem.type === "unordered", }, { onClick: () => { - props.bubbleMenuFunctions.blocks.listItem.set("ordered"); - setActiveBlock(getActiveBlock()); + props.bubbleMenuProps.blocks.listItem.set("ordered"); }, text: "Numbered List", icon: RiListOrdered, isSelected: - props.bubbleMenuFunctions.blocks.listItem.isActive() && - props.bubbleMenuFunctions.blocks.listItem.getType() === "ordered", + props.bubbleMenuProps.blocks.listItem.isActive && + props.bubbleMenuProps.blocks.listItem.type === "ordered", }, ]} /> { - props.bubbleMenuFunctions.marks.bold.toggle(); - setActiveMarks(getActiveMarks()); + props.bubbleMenuProps.marks.bold.toggle(); }} isSelected={activeMarks.has("bold")} mainTooltip="Bold" @@ -178,8 +165,7 @@ export const BubbleMenu = (props: { /> { - props.bubbleMenuFunctions.marks.italic.toggle(); - setActiveMarks(getActiveMarks()); + props.bubbleMenuProps.marks.italic.toggle(); }} isSelected={activeMarks.has("italic")} mainTooltip="Italic" @@ -188,8 +174,7 @@ export const BubbleMenu = (props: { /> { - props.bubbleMenuFunctions.marks.underline.toggle(); - setActiveMarks(getActiveMarks()); + props.bubbleMenuProps.marks.underline.toggle(); }} isSelected={activeMarks.has("underline")} mainTooltip="Underline" @@ -198,8 +183,7 @@ export const BubbleMenu = (props: { /> { - props.bubbleMenuFunctions.marks.strike.toggle(); - setActiveMarks(getActiveMarks()); + props.bubbleMenuProps.marks.strike.toggle(); }} isSelected={activeMarks.has("strike")} mainTooltip="Strike-through" @@ -210,7 +194,6 @@ export const BubbleMenu = (props: { onClick={() => { // props.editor.view.focus(); // props.editor.commands.sinkListItem("block"); - setActiveMarks(getActiveMarks()); }} isDisabled={ // !props.editor.can().sinkListItem("block") @@ -225,7 +208,6 @@ export const BubbleMenu = (props: { onClick={() => { // props.editor.view.focus(); // props.editor.commands.liftListItem("block"); - setActiveMarks(getActiveMarks()); }} isDisabled={ // !props.editor.can().command(({ state }) => { @@ -244,7 +226,7 @@ export const BubbleMenu = (props: { /> { ); - }, [props.hyperlinkMarkFunctions]); + }, [props.hyperlinkMarkProps]); return ( Date: Mon, 19 Dec 2022 18:35:29 +0100 Subject: [PATCH 08/55] Changes to bubble menu code and documentation --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 26 ++++++++----------- .../BubbleMenu/getBubbleMenuProps.ts | 2 +- .../core/src/menu-tools/BubbleMenu/types.ts | 5 +++- .../src/BubbleMenu/BubbleMenuFactory.tsx | 10 +++---- .../src/BubbleMenu/components/BubbleMenu.tsx | 1 - 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index be2cee69d4..a964f4a7d7 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -111,10 +111,11 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { ); } - if (prev.show && next.show) { - console.log("UPDATE"); - console.log(options.editor.isActive("bold")); - bubbleMenu.update(getBubbleMenuProps(options.editor)); + if (!next.preventUpdate) { + if (prev.show && next.show) { + console.log("UPDATE"); + bubbleMenu.update(getBubbleMenuProps(options.editor)); + } } } @@ -139,30 +140,25 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { return { show: false, preventShow: false, + preventUpdate: false, preventHide: false, }; }, - apply: (tr, prev, _oldState, state) => { + apply: (tr, prev, oldState, state) => { const next = { ...prev }; + const { doc, selection } = state; if (tr.getMeta(options.pluginKey)?.preventShow !== undefined) { next.preventShow = tr.getMeta(options.pluginKey).preventShow; } + next.preventUpdate = + oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); + if (tr.getMeta(options.pluginKey)?.preventHide !== undefined) { next.preventHide = tr.getMeta(options.pluginKey).preventHide; } - const { doc, selection } = state; - - // const isSame = - // oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); - // - // if (isSame && !prev.preventHide) { - // next.show = false; - // return next; - // } - // Support for CellSelections const { ranges, empty } = selection; const from = Math.min(...ranges.map((range) => range.$from.pos)); diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts index 8e690c2f8d..59eff940ba 100644 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts @@ -124,7 +124,7 @@ export function getBubbleMenuProps(editor: Editor): BubbleMenuProps { return posToDOMRect(editor.view, from, to); }, - getEditorElement: () => editor.options.element, + editorElement: editor.options.element, }, }; } diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 07372344c1..51b32362c2 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -43,8 +43,11 @@ export type BubbleMenuProps = { listItem: ListItemBlockProps; }; view: { + // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are + // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection + // bounding box used to create it is from an editor view that is out of date due to the animation. getSelectionBoundingBox: () => DOMRect; - getEditorElement: () => Element; + editorElement: Element; }; }; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 27a5709d7d..6ce1708275 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -17,7 +17,7 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( // element.className = rootStyles.bnRoot; const root = createRoot(element); - let menu = tippy(props.view.getEditorElement(), { + let menu = tippy(props.view.editorElement, { duration: 0, getReferenceClientRect: props.view.getSelectionBoundingBox, content: element, @@ -38,16 +38,16 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( menu.show(); }, hide: menu.hide, - // BubbleMenu React component updates its UI elements automatically with useState hooks, so we only need to ensure - // the tippy menu updates its position. update: (newProps: BubbleMenuProps) => { root.render( ); - // Waits one second for animation to complete. Can be a bit clunky. - setTimeout(() => menu.popperInstance?.forceUpdate(), 1000); + + // TODO: Waits 500ms for animations to complete, looks clunky. See TODO in getBubbleMenuProps for why this is + // necessary. + setTimeout(() => menu.popperInstance?.forceUpdate(), 350); }, }; }; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 0ada8331f9..261ae53bb1 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -24,7 +24,6 @@ import { BubbleMenuProps } from "../../../../core/src/menu-tools/BubbleMenu/type export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { const getActiveMarks = () => { const activeMarks = new Set(); - console.log(props.bubbleMenuProps.marks.bold.isActive); props.bubbleMenuProps.marks.bold.isActive && activeMarks.add("bold"); props.bubbleMenuProps.marks.italic.isActive && activeMarks.add("italic"); From d5d6515ac4a0fc6ad07ccc77e30f4cf62c9f1493 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 19 Dec 2022 18:35:29 +0100 Subject: [PATCH 09/55] Changes to bubble menu code and documentation --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 25 ++++++++----------- .../BubbleMenu/getBubbleMenuProps.ts | 2 +- .../core/src/menu-tools/BubbleMenu/types.ts | 5 +++- .../src/BubbleMenu/BubbleMenuFactory.tsx | 10 ++++---- .../src/BubbleMenu/components/BubbleMenu.tsx | 1 - 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index be2cee69d4..5f97ba6472 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -111,10 +111,10 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { ); } - if (prev.show && next.show) { - console.log("UPDATE"); - console.log(options.editor.isActive("bold")); - bubbleMenu.update(getBubbleMenuProps(options.editor)); + if (!next.preventUpdate) { + if (prev.show && next.show) { + bubbleMenu.update(getBubbleMenuProps(options.editor)); + } } } @@ -139,30 +139,25 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { return { show: false, preventShow: false, + preventUpdate: false, preventHide: false, }; }, - apply: (tr, prev, _oldState, state) => { + apply: (tr, prev, oldState, state) => { const next = { ...prev }; + const { doc, selection } = state; if (tr.getMeta(options.pluginKey)?.preventShow !== undefined) { next.preventShow = tr.getMeta(options.pluginKey).preventShow; } + next.preventUpdate = + oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); + if (tr.getMeta(options.pluginKey)?.preventHide !== undefined) { next.preventHide = tr.getMeta(options.pluginKey).preventHide; } - const { doc, selection } = state; - - // const isSame = - // oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); - // - // if (isSame && !prev.preventHide) { - // next.show = false; - // return next; - // } - // Support for CellSelections const { ranges, empty } = selection; const from = Math.min(...ranges.map((range) => range.$from.pos)); diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts index 8e690c2f8d..59eff940ba 100644 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts @@ -124,7 +124,7 @@ export function getBubbleMenuProps(editor: Editor): BubbleMenuProps { return posToDOMRect(editor.view, from, to); }, - getEditorElement: () => editor.options.element, + editorElement: editor.options.element, }, }; } diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 07372344c1..51b32362c2 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -43,8 +43,11 @@ export type BubbleMenuProps = { listItem: ListItemBlockProps; }; view: { + // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are + // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection + // bounding box used to create it is from an editor view that is out of date due to the animation. getSelectionBoundingBox: () => DOMRect; - getEditorElement: () => Element; + editorElement: Element; }; }; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 27a5709d7d..6ce1708275 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -17,7 +17,7 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( // element.className = rootStyles.bnRoot; const root = createRoot(element); - let menu = tippy(props.view.getEditorElement(), { + let menu = tippy(props.view.editorElement, { duration: 0, getReferenceClientRect: props.view.getSelectionBoundingBox, content: element, @@ -38,16 +38,16 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( menu.show(); }, hide: menu.hide, - // BubbleMenu React component updates its UI elements automatically with useState hooks, so we only need to ensure - // the tippy menu updates its position. update: (newProps: BubbleMenuProps) => { root.render( ); - // Waits one second for animation to complete. Can be a bit clunky. - setTimeout(() => menu.popperInstance?.forceUpdate(), 1000); + + // TODO: Waits 500ms for animations to complete, looks clunky. See TODO in getBubbleMenuProps for why this is + // necessary. + setTimeout(() => menu.popperInstance?.forceUpdate(), 350); }, }; }; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 0ada8331f9..261ae53bb1 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -24,7 +24,6 @@ import { BubbleMenuProps } from "../../../../core/src/menu-tools/BubbleMenu/type export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { const getActiveMarks = () => { const activeMarks = new Set(); - console.log(props.bubbleMenuProps.marks.bold.isActive); props.bubbleMenuProps.marks.bold.isActive && activeMarks.add("bold"); props.bubbleMenuProps.marks.italic.isActive && activeMarks.add("italic"); From bac3b6a8b57727b3f12ccc85f62c5e29cbf7860a Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 20 Dec 2022 17:08:41 +0100 Subject: [PATCH 10/55] General improvements, mostly to bubble and hyperlink menus --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 63 ++--- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 244 +++++++++++++++--- .../BubbleMenu/getBubbleMenuProps.ts | 4 +- .../core/src/menu-tools/BubbleMenu/types.ts | 3 - .../getHyperlinkHoverMenuFactoryFunctions.ts | 175 ------------- .../getHyperlinkHoverMenuProps.ts | 23 ++ .../menu-tools/HyperlinkHoverMenu/types.ts | 16 +- ...unctions.ts => getSuggestionsMenuProps.ts} | 10 +- .../src/menu-tools/SuggestionsMenu/types.ts | 14 +- .../plugins/suggestion/SuggestionPlugin.ts | 22 +- .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 66 +++-- .../components/suggestion/SuggestionList.tsx | 4 +- .../suggestion/SuggestionsMenuFactory.tsx | 25 +- 13 files changed, 358 insertions(+), 311 deletions(-) delete mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts create mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts rename packages/core/src/menu-tools/SuggestionsMenu/{getSuggestionsMenuFactoryFunctions.ts => getSuggestionsMenuProps.ts} (69%) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 5f97ba6472..b8f2fd978b 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -98,38 +98,43 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { const prev = options.pluginKey.getState(prevState); const next = options.pluginKey.getState(view.state); - if (!next.preventShow) { - if (!prev.show && next.show) { - bubbleMenu.show(getBubbleMenuProps(options.editor)); - - bubbleMenu.element!.addEventListener( - "mousedown", - () => mousedownHandler(view), - { - capture: true, - } - ); - } - - if (!next.preventUpdate) { - if (prev.show && next.show) { - bubbleMenu.update(getBubbleMenuProps(options.editor)); + if (!prev.show && next.show && !next.preventShow) { + bubbleMenu.show(getBubbleMenuProps(options.editor)); + + bubbleMenu.element!.addEventListener( + "mousedown", + () => mousedownHandler(view), + { + capture: true, } - } + ); + + return; + } + + if ( + prev.show && + next.show && + !next.preventShow && + !next.preventUpdate + ) { + bubbleMenu.update(getBubbleMenuProps(options.editor)); + + return; } - if (!next.preventHide) { - if (prev.show && !next.show) { - bubbleMenu.hide(); - - bubbleMenu.element!.removeEventListener( - "mousedown", - () => mousedownHandler(view), - { - capture: true, - } - ); - } + if (prev.show && !next.show && !next.preventHide) { + bubbleMenu.element!.removeEventListener( + "mousedown", + () => mousedownHandler(view), + { + capture: true, + } + ); + + bubbleMenu.hide(); + + return; } }, }; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index 18ea661704..28ac49d0a9 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -1,8 +1,8 @@ -import { Editor } from "@tiptap/core"; +import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; import { HyperlinkHoverMenuFactory } from "../../menu-tools/HyperlinkHoverMenu/types"; -import { getHyperlinkHoverMenuFactoryFunctions } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions"; +import { getHyperlinkHoverMenuProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); export type HyperlinkMenuPluginProps = { @@ -13,13 +13,43 @@ export const createHyperlinkMenuPlugin = ( editor: Editor, options: HyperlinkMenuPluginProps ) => { + let menuHideTimer: NodeJS.Timeout | undefined; + const startMenuHideTimer = () => { + menuHideTimer = setTimeout(() => { + editor.view.dispatch( + editor.view.state.tr.setMeta(PLUGIN_KEY, { + hoveredLinkChanged: true, + }) + ); + }, 250); + }; + const stopMenuHideTimer = () => { + if (menuHideTimer) { + clearTimeout(menuHideTimer); + menuHideTimer = undefined; + } + + return false; + }; + let mouseHoveredHyperlinkMark: Mark | undefined; + let mouseHoveredHyperlinkMarkRange: Range | undefined; + let keyboardHoveredHyperlinkMark: Mark | undefined; + let keyboardHoveredHyperlinkMarkRange: Range | undefined; + let hyperlinkMark: Mark | undefined; + let hyperlinkMarkRange: Range | undefined; - const hyperlinkMenuFactory = options.hyperlinkMenuFactory; - let hyperlinkMenu = hyperlinkMenuFactory( - getHyperlinkHoverMenuFactoryFunctions(editor) + let hyperlinkMenu = options.hyperlinkMenuFactory( + getHyperlinkHoverMenuProps( + "", + "", + () => {}, + () => {}, + new DOMRect(), + editor.options.element + ) ); return new Plugin({ @@ -35,6 +65,12 @@ export const createHyperlinkMenuPlugin = ( for (const mark of marksAtPos) { if (mark.type.name === editor.schema.mark("link").type.name) { keyboardHoveredHyperlinkMark = mark; + keyboardHoveredHyperlinkMarkRange = + getMarkRange( + editor.state.selection.$from, + mark.type, + mark.attrs + ) || undefined; foundHyperlinkMark = true; break; @@ -43,26 +79,118 @@ export const createHyperlinkMenuPlugin = ( if (!foundHyperlinkMark) { keyboardHoveredHyperlinkMark = undefined; + keyboardHoveredHyperlinkMarkRange = undefined; } } - if (keyboardHoveredHyperlinkMark) { - hyperlinkMark = keyboardHoveredHyperlinkMark; - } + const prevHyperlinkMark = hyperlinkMark; + // Keyboard cursor position overrides mouse cursor position. if (mouseHoveredHyperlinkMark) { hyperlinkMark = mouseHoveredHyperlinkMark; + hyperlinkMarkRange = mouseHoveredHyperlinkMarkRange; + } + + if (keyboardHoveredHyperlinkMark) { + hyperlinkMark = keyboardHoveredHyperlinkMark; + hyperlinkMarkRange = keyboardHoveredHyperlinkMarkRange; } if (!mouseHoveredHyperlinkMark && !keyboardHoveredHyperlinkMark) { hyperlinkMark = undefined; + hyperlinkMarkRange = undefined; } - if (!hyperlinkMark) { + // Hides menu. + if (prevHyperlinkMark && !hyperlinkMark) { + hyperlinkMenu.element?.removeEventListener( + "mouseleave", + startMenuHideTimer + ); + hyperlinkMenu.element?.removeEventListener( + "mouseenter", + stopMenuHideTimer + ); + hyperlinkMenu.hide(); - } else { - hyperlinkMenu.update(); - hyperlinkMenu.show(); + + return; + } + + if (hyperlinkMark) { + // Gets all variables/functions needed to render menu. + const url = hyperlinkMark.attrs.href; + const text = editor.view.state.doc.textBetween( + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.to + ); + const editHyperlink = (url: string, text: string) => { + const tr = editor.view.state.tr.insertText( + text, + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.to + ); + tr.addMark( + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.from + text.length, + editor.schema.mark("link", { href: url }) + ); + editor.view.dispatch(tr); + }; + const deleteHyperlink = () => { + editor.view.dispatch( + editor.view.state.tr + .removeMark( + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.to, + hyperlinkMark!.type + ) + .setMeta("preventAutolink", true) + ); + }; + const hyperlinkBoundingBox = posToDOMRect( + editor.view, + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.to + ); + const editorElement = editor.view.dom; + + // Shows menu. + if (!prevHyperlinkMark) { + hyperlinkMenu.show( + getHyperlinkHoverMenuProps( + url, + text, + editHyperlink, + deleteHyperlink, + hyperlinkBoundingBox, + editorElement + ) + ); + + hyperlinkMenu.element?.addEventListener( + "mouseleave", + startMenuHideTimer + ); + hyperlinkMenu.element?.addEventListener( + "mouseenter", + stopMenuHideTimer + ); + + return; + } + + // Updates menu. + hyperlinkMenu.update( + getHyperlinkHoverMenuProps( + url, + text, + editHyperlink, + deleteHyperlink, + hyperlinkBoundingBox, + editorElement + ) + ); } }, }; @@ -70,47 +198,77 @@ export const createHyperlinkMenuPlugin = ( props: { handleDOMEvents: { - // update view when an
is hovered over - mouseover(view, event: any) { + // Updates view when an anchor () element is hovered. + mouseover: (view, event: Event) => { + console.log(event.target); + + // Checks if target element is an anchor () element. if ( - event.target instanceof HTMLAnchorElement && - event.target.nodeName === "A" + !(event.target instanceof HTMLAnchorElement) || + event.target.nodeName !== "A" ) { - const hoveredHyperlinkElement = event.target; - const posInHoveredHyperlinkMark = - editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1; - const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve( - posInHoveredHyperlinkMark - ); - const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks(); + mouseHoveredHyperlinkMark = undefined; + mouseHoveredHyperlinkMarkRange = undefined; - let foundHyperlinkMark = false; + return false; + } - for (const mark of marksAtPos) { - if (mark.type.name === editor.schema.mark("link").type.name) { - mouseHoveredHyperlinkMark = mark; - foundHyperlinkMark = true; + stopMenuHideTimer(); - break; - } - } + // Finds link mark at the hovered element's position to update mouseHoveredHyperlinkMark and + // mouseHoveredHyperlinkMarkRange. + const hoveredHyperlinkElement = event.target; + const posInHoveredHyperlinkMark = + editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1; + const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve( + posInHoveredHyperlinkMark + ); + const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks(); - if (!foundHyperlinkMark) { - mouseHoveredHyperlinkMark = undefined; + let foundHyperlinkMark = false; + + for (const mark of marksAtPos) { + if (mark.type.name === editor.schema.mark("link").type.name) { + mouseHoveredHyperlinkMark = mark; + mouseHoveredHyperlinkMarkRange = + getMarkRange( + resolvedPosInHoveredHyperlinkMark, + mark.type, + mark.attrs + ) || undefined; + foundHyperlinkMark = true; + + break; } - } else { + } + + // Resets mouseHoveredHyperlinkMark and mouseHoveredHyperlinkMarkRange if no link mark was found. + if (!foundHyperlinkMark) { mouseHoveredHyperlinkMark = undefined; + mouseHoveredHyperlinkMarkRange = undefined; + + return false; } - // Using setTimeout ensures all other listeners of this event are executed before a new transaction is - // dispatched. - setTimeout(() => { - view.dispatch( - view.state.tr.setMeta(PLUGIN_KEY, { - hoveredLinkChanged: true, - }) - ); - }); + // Dispatches transaction to update the view. + view.dispatch( + view.state.tr.setMeta(PLUGIN_KEY, { + hoveredLinkChanged: true, + }) + ); + + return false; + }, + // Updates view half a second after the cursor leaves an anchor () element. This update is cancelled if + mouseout: (_view, event: Event) => { + if ( + !(event.target instanceof HTMLAnchorElement) || + event.target.nodeName !== "A" + ) { + return false; + } + + startMenuHideTimer(); return false; }, diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts index 59eff940ba..3b9fd98fbe 100644 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts @@ -104,7 +104,9 @@ export function getBubbleMenuProps(editor: Editor): BubbleMenuProps { }, }, view: { - // TODO: Define function in plugin instead and pass it as an argument? + // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are + // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection + // bounding box used to create it is from an editor view that is out of date due to the animation. getSelectionBoundingBox: () => { const { state } = editor.view; const { selection } = state; diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 51b32362c2..5ce97c411d 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -43,9 +43,6 @@ export type BubbleMenuProps = { listItem: ListItemBlockProps; }; view: { - // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are - // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection - // bounding box used to create it is from an editor view that is out of date due to the animation. getSelectionBoundingBox: () => DOMRect; editorElement: Element; }; diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts deleted file mode 100644 index 7f66139ba3..0000000000 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuFactoryFunctions.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; -import { HyperlinkHoverMenuFactoryFunctions } from "./types"; -import { Mark } from "prosemirror-model"; - -let mouseHoveredHyperlinkMark: Mark | undefined; -let mouseHoveredHyperlinkMarkRange: Range | undefined; - -let keyboardHoveredHyperlinkMark: Mark | undefined; -let keyboardHoveredHyperlinkMarkRange: Range | undefined; - -let hyperlinkMark: Mark | undefined; -let hyperlinkMarkRange: Range | undefined; - -function getHyperlinkMark(editor: Editor) { - if (editor.state.selection.empty) { - const marksAtPos = editor.state.selection.$from.marks(); - - let foundHyperlinkMark = false; - - for (const mark of marksAtPos) { - if (mark.type.name === editor.schema.mark("link").type.name) { - keyboardHoveredHyperlinkMark = mark; - keyboardHoveredHyperlinkMarkRange = - getMarkRange(editor.state.selection.$from, mark.type, mark.attrs) || - undefined; - foundHyperlinkMark = true; - - break; - } - } - - if (!foundHyperlinkMark) { - keyboardHoveredHyperlinkMark = undefined; - keyboardHoveredHyperlinkMarkRange = undefined; - } - } - - if (keyboardHoveredHyperlinkMark) { - hyperlinkMark = keyboardHoveredHyperlinkMark; - hyperlinkMarkRange = keyboardHoveredHyperlinkMarkRange; - } - - // console.log(mouseHoveredHyperlinkMark); - if (mouseHoveredHyperlinkMark) { - hyperlinkMark = mouseHoveredHyperlinkMark; - hyperlinkMarkRange = mouseHoveredHyperlinkMarkRange; - } - - if (!mouseHoveredHyperlinkMark && !keyboardHoveredHyperlinkMark) { - hyperlinkMark = undefined; - hyperlinkMarkRange = undefined; - } - - return { - hyperlinkMark, - hyperlinkMarkRange, - }; -} - -export function getHyperlinkHoverMenuFactoryFunctions( - editor: Editor -): HyperlinkHoverMenuFactoryFunctions { - const editorElement = editor.options.element; - editorElement.addEventListener("mouseover", (event) => { - if ( - event.target instanceof HTMLAnchorElement && - event.target.nodeName === "A" - ) { - const hoveredHyperlinkElement = event.target; - const posInHoveredHyperlinkMark = - editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1; - const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve( - posInHoveredHyperlinkMark - ); - const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks(); - - let foundHyperlinkMark = false; - - for (const mark of marksAtPos) { - if (mark.type.name === editor.schema.mark("link").type.name) { - mouseHoveredHyperlinkMark = mark; - mouseHoveredHyperlinkMarkRange = - getMarkRange( - resolvedPosInHoveredHyperlinkMark, - mark.type, - mark.attrs - ) || undefined; - foundHyperlinkMark = true; - - break; - } - } - - if (!foundHyperlinkMark) { - mouseHoveredHyperlinkMark = undefined; - mouseHoveredHyperlinkMarkRange = undefined; - } - } else { - mouseHoveredHyperlinkMark = undefined; - mouseHoveredHyperlinkMarkRange = undefined; - } - }); - - return { - hyperlink: { - getUrl: () => { - const { hyperlinkMark } = getHyperlinkMark(editor); - return hyperlinkMark?.attrs.href; - }, - getText: () => { - const { hyperlinkMarkRange } = getHyperlinkMark(editor); - - if (!hyperlinkMarkRange) { - return ""; - } - - return editor.view.state.doc.textBetween( - hyperlinkMarkRange.from, - hyperlinkMarkRange.to - ); - }, - edit: (url: string, text: string) => { - const { hyperlinkMarkRange } = getHyperlinkMark(editor); - - if (!hyperlinkMarkRange) { - return; - } - - const tr = editor.view.state.tr.insertText( - text, - hyperlinkMarkRange.from, - hyperlinkMarkRange.to - ); - tr.addMark( - hyperlinkMarkRange.from, - hyperlinkMarkRange.from + text.length, - editor.schema.mark("link", { href: url }) - ); - editor.view.dispatch(tr); - }, - delete: () => { - const { hyperlinkMark, hyperlinkMarkRange } = getHyperlinkMark(editor); - - if (!hyperlinkMark || !hyperlinkMarkRange) { - return; - } - - editor.view.dispatch( - editor.view.state.tr - .removeMark( - hyperlinkMarkRange.from, - hyperlinkMarkRange.to, - hyperlinkMark.type - ) - .setMeta("preventAutolink", true) - ); - }, - }, - view: { - getHyperlinkBoundingBox: () => { - const { hyperlinkMarkRange } = getHyperlinkMark(editor); - - if (!hyperlinkMarkRange) { - return; - } - - return posToDOMRect( - editor.view, - hyperlinkMarkRange.from, - hyperlinkMarkRange.to - ); - }, - }, - }; -} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts new file mode 100644 index 0000000000..83df9723ed --- /dev/null +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts @@ -0,0 +1,23 @@ +import { HyperlinkHoverMenuProps } from "./types"; + +export function getHyperlinkHoverMenuProps( + url: string, + text: string, + editHyperlink: (url: string, text: string) => void, + deleteHyperlink: () => void, + hyperlinkBoundingBox: DOMRect, + editorElement: Element +): HyperlinkHoverMenuProps { + return { + hyperlink: { + url: url, + text: text, + edit: editHyperlink, + delete: deleteHyperlink, + }, + view: { + hyperlinkBoundingBox: hyperlinkBoundingBox, + editorElement: editorElement, + }, + }; +} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts index 8f44364d56..6639bdb7ea 100644 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts @@ -1,17 +1,17 @@ -import { Menu } from "../types"; +import { Menu, MenuFactory } from "../types"; -export type HyperlinkHoverMenuFactoryFunctions = { +export type HyperlinkHoverMenuProps = { hyperlink: { - getUrl: () => string; - getText: () => string; + url: string; + text: string; edit: (url: string, text: string) => void; delete: () => void; }; view: { - getHyperlinkBoundingBox: () => DOMRect | undefined; + hyperlinkBoundingBox: DOMRect; + editorElement: Element; }; }; -export type HyperlinkHoverMenuFactory = ( - functions: HyperlinkHoverMenuFactoryFunctions -) => Menu; +export type HyperlinkHoverMenu = Menu; +export type HyperlinkHoverMenuFactory = MenuFactory; diff --git a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions.ts b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts similarity index 69% rename from packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions.ts rename to packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts index c53fc7e7d8..d28dc2fba3 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions.ts +++ b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts @@ -1,14 +1,15 @@ -import { SuggestionsMenuFactoryFunctions } from "./types"; +import { SuggestionsMenuProps } from "./types"; import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; // TODO: Only need either getQuery or matchesQuery, not both. Depends if we want to allow users the ability to define // their own block type aliases/matching algorithm. -export function getSuggestionsMenuFactoryFunctions( +export function getSuggestionsMenuProps( items: T[], selectedItemIndex: number, itemCallback: (item: T) => void, - selectedBlockBoundingBox: DOMRect -): SuggestionsMenuFactoryFunctions { + selectedBlockBoundingBox: DOMRect, + editorElement: Element +): SuggestionsMenuProps { return { menuItems: { items: items, @@ -17,6 +18,7 @@ export function getSuggestionsMenuFactoryFunctions( }, view: { selectedBlockBoundingBox: selectedBlockBoundingBox, + editorElement: editorElement, }, }; } diff --git a/packages/core/src/menu-tools/SuggestionsMenu/types.ts b/packages/core/src/menu-tools/SuggestionsMenu/types.ts index 8333ccf237..fc9c2b8611 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/types.ts +++ b/packages/core/src/menu-tools/SuggestionsMenu/types.ts @@ -1,4 +1,4 @@ -import { Menu } from "../types"; +import { Menu, MenuFactory } from "../types"; import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; export type SuggestionsMenuItem = { @@ -6,7 +6,7 @@ export type SuggestionsMenuItem = { set: () => void; }; -export type SuggestionsMenuFactoryFunctions = { +export type SuggestionsMenuProps = { menuItems: { items: T[]; selectedItemIndex: number; @@ -14,9 +14,13 @@ export type SuggestionsMenuFactoryFunctions = { }; view: { selectedBlockBoundingBox: DOMRect; + editorElement: Element; }; }; -export type SuggestionsMenuFactory = ( - functions: SuggestionsMenuFactoryFunctions -) => Menu; +export type SuggestionsMenu = Menu< + SuggestionsMenuProps +>; +export type SuggestionsMenuFactory = MenuFactory< + SuggestionsMenuProps +>; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 07ebb7a9be..f7ec74e286 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -5,7 +5,7 @@ import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; import SuggestionItem from "./SuggestionItem"; import { SuggestionsMenuFactory } from "../../../menu-tools/SuggestionsMenu/types"; -import { getSuggestionsMenuFactoryFunctions } from "../../../menu-tools/SuggestionsMenu/getSuggestionsMenuFactoryFunctions"; +import { getSuggestionsMenuProps } from "../../../menu-tools/SuggestionsMenu/getSuggestionsMenuProps"; export type SuggestionPluginOptions = { /** @@ -116,7 +116,7 @@ export function createSuggestionPlugin({ }; const suggestionsMenu = suggestionsMenuFactory( - getSuggestionsMenuFactoryFunctions( + getSuggestionsMenuProps( [], 0, (item) => { @@ -127,7 +127,8 @@ export function createSuggestionPlugin({ range: { from: 0, to: 0 }, }); }, - new DOMRect() + new DOMRect(), + editor.options.element ) ); @@ -173,7 +174,7 @@ export function createSuggestionPlugin({ if (changed) { suggestionsMenu.update( - getSuggestionsMenuFactoryFunctions( + getSuggestionsMenuProps( next.items, 0, (item) => { @@ -186,14 +187,15 @@ export function createSuggestionPlugin({ }, decorationNode !== null ? decorationNode.getBoundingClientRect() - : new DOMRect() + : new DOMRect(), + editor.options.element ) ); } if (started) { suggestionsMenu.show( - getSuggestionsMenuFactoryFunctions( + getSuggestionsMenuProps( next.items, 0, (item) => { @@ -206,7 +208,8 @@ export function createSuggestionPlugin({ }, decorationNode !== null ? decorationNode.getBoundingClientRect() - : new DOMRect() + : new DOMRect(), + editor.options.element ) ); } @@ -261,7 +264,7 @@ export function createSuggestionPlugin({ ); suggestionsMenu.update( - getSuggestionsMenuFactoryFunctions( + getSuggestionsMenuProps( next.items, next.selectedItemIndex, (item) => { @@ -273,7 +276,8 @@ export function createSuggestionPlugin({ }, decorationNode !== null ? decorationNode.getBoundingClientRect() - : new DOMRect() + : new DOMRect(), + editor.options.element ) ); diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx index 9243e75563..62ce9cd7ee 100644 --- a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx +++ b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx @@ -4,46 +4,68 @@ import tippy from "tippy.js"; import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../BlockNoteTheme"; import { + HyperlinkHoverMenu, HyperlinkHoverMenuFactory, - HyperlinkHoverMenuFactoryFunctions, + HyperlinkHoverMenuProps, } from "../../../core/src/menu-tools/HyperlinkHoverMenu/types"; export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( - hyperlinkHoverMenuFactoryFunctions: HyperlinkHoverMenuFactoryFunctions -) => { + props: HyperlinkHoverMenuProps +): HyperlinkHoverMenu => { const element = document.createElement("div"); const root = createRoot(element); - root.render( - - - - ); - - const menu = tippy(document.body, { + const menu = tippy(props.view.editorElement, { + appendTo: props.view.editorElement, duration: 0, - getReferenceClientRect: - hyperlinkHoverMenuFactoryFunctions.view.getHyperlinkBoundingBox, + getReferenceClientRect: () => props.view.hyperlinkBoundingBox, content: element, interactive: true, trigger: "manual", placement: "top", - hideOnClick: "toggle", + hideOnClick: false, }); menu.show(); return { element: element, - show: menu.show, - hide: menu.hide, - update: () => { - menu.popperInstance?.forceUpdate(); + show: (props: HyperlinkHoverMenuProps) => { + root.render( + + + + ); + + menu.setProps({ + getReferenceClientRect: () => props.view.hyperlinkBoundingBox, + }); + + menu.show(); + }, + hide: () => { + menu.hide(); + }, + update: (newProps: HyperlinkHoverMenuProps) => { + root.render( + + + + ); + + menu.setProps({ + getReferenceClientRect: () => newProps.view.hyperlinkBoundingBox, + }); }, }; }; diff --git a/packages/react/src/shared/components/suggestion/SuggestionList.tsx b/packages/react/src/shared/components/suggestion/SuggestionList.tsx index b1ae8a8088..13e5686b9b 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionList.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionList.tsx @@ -1,10 +1,10 @@ import { createStyles, Menu } from "@mantine/core"; import { SuggestionListItem } from "./SuggestionListItem"; -import { SuggestionsMenuFactoryFunctions } from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; +import { SuggestionsMenuProps } from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; export type SuggestionListProps = - SuggestionsMenuFactoryFunctions; + SuggestionsMenuProps; export function SuggestionList( props: SuggestionListProps diff --git a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx index b50b41354d..c287e20274 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx @@ -3,8 +3,9 @@ import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../../../BlockNoteTheme"; import tippy from "tippy.js"; import { + SuggestionsMenu, SuggestionsMenuFactory, - SuggestionsMenuFactoryFunctions, + SuggestionsMenuProps, } from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; import { SuggestionList } from "./SuggestionList"; import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; @@ -12,14 +13,16 @@ import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< SuggestionItem -> = () => { +> = ( + props: SuggestionsMenuProps +): SuggestionsMenu => { const element = document.createElement("div"); // element.className = rootStyles.bnRoot; const root = createRoot(element); - const menu = tippy(document.body, { + const menu = tippy(props.view.editorElement, { duration: 0, - getReferenceClientRect: () => new DOMRect(), + getReferenceClientRect: () => props.view.selectedBlockBoundingBox, content: element, interactive: true, trigger: "manual", @@ -29,27 +32,29 @@ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< return { element: element as HTMLElement, - show: (props: SuggestionsMenuFactoryFunctions) => { + show: (props: SuggestionsMenuProps) => { root.render( ); - menu.props.getReferenceClientRect = () => - props.view.selectedBlockBoundingBox; + menu.setProps({ + getReferenceClientRect: () => props.view.selectedBlockBoundingBox, + }); menu.show(); }, - update: (newProps: SuggestionsMenuFactoryFunctions) => { + update: (newProps: SuggestionsMenuProps) => { root.render( ); - menu.props.getReferenceClientRect = () => - newProps.view.selectedBlockBoundingBox; + menu.setProps({ + getReferenceClientRect: () => newProps.view.selectedBlockBoundingBox, + }); }, hide: () => { menu.hide(); From 267a238c7f74025d9bad70336c62ad57b7f797d9 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 21 Dec 2022 10:14:17 +0100 Subject: [PATCH 11/55] comments after call --- examples/editor/src/main.tsx | 7 ++++- packages/core/src/BlockNoteEditor.ts | 3 ++- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 4 ++- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 26 +++++++++---------- .../BubbleMenu/getBubbleMenuProps.ts | 1 + .../core/src/menu-tools/BubbleMenu/types.ts | 1 + .../getHyperlinkHoverMenuProps.ts | 1 + .../getSuggestionsMenuProps.ts | 5 +++- .../src/menu-tools/SuggestionsMenu/types.ts | 3 ++- .../plugins/suggestion/SuggestionPlugin.ts | 8 ++++-- .../src/BubbleMenu/BubbleMenuFactory.tsx | 6 ++--- .../components/suggestion/SuggestionList.tsx | 4 +-- .../suggestion/SuggestionsMenuFactory.tsx | 8 +++--- 13 files changed, 48 insertions(+), 29 deletions(-) diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 57573de8e4..948606aabc 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,5 +1,6 @@ -import "./index.css"; import styles from "./App.module.css"; +import "./index.css"; +// TODO: fix imports import { mountBlockNoteEditor } from "../../../packages/core/src/BlockNoteEditor"; import { ReactBubbleMenuFactory } from "../../../packages/react/src/BubbleMenu/BubbleMenuFactory"; import { ReactHyperlinkMenuFactory } from "../../../packages/react/src/HyperlinkMenus/HyperlinkMenuFactory"; @@ -8,6 +9,10 @@ import { ReactSuggestionsMenuFactory } from "../../../packages/react/src/shared/ // type WindowWithProseMirror = Window & // typeof globalThis & { ProseMirror: Editor }; +/* + TODO: + +*/ mountBlockNoteEditor( { bubbleMenuFactory: ReactBubbleMenuFactory, diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index a876086e3e..0f192386dd 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -2,10 +2,10 @@ import { Editor, EditorOptions } from "@tiptap/core"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; -import rootStyles from "./root.module.css"; import { BubbleMenuFactory } from "./menu-tools/BubbleMenu/types"; import { HyperlinkHoverMenuFactory } from "./menu-tools/HyperlinkHoverMenu/types"; import { SuggestionsMenuFactory } from "./menu-tools/SuggestionsMenu/types"; +import rootStyles from "./root.module.css"; import SuggestionItem from "./shared/plugins/suggestion/SuggestionItem"; type BlockNoteEditorOptions = EditorOptions & { @@ -35,6 +35,7 @@ export const mountBlockNoteEditor = ( ? blockNoteExtensions.filter((e) => e.name !== "history") : blockNoteExtensions; + // TODO: review extensions = extensions.map((extension) => { if (extension.name === "BubbleMenuExtension") { return extension.configure({ diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index b8f2fd978b..84fff50d22 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -1,8 +1,8 @@ import { Editor, isTextSelection } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; import { getBubbleMenuProps } from "../../menu-tools/BubbleMenu/getBubbleMenuProps"; +import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files @@ -22,6 +22,7 @@ export interface BubbleMenuPluginProps { | null; } +// TODO: do from previous code export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { const bubbleMenu = options.bubbleMenuFactory( getBubbleMenuProps(options.editor) @@ -36,6 +37,7 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { // ); }; + // TODO: transaction needed? const viewMousedownHandler = (view: EditorView) => { view.dispatch( view.state.tr.setMeta(options.pluginKey, { diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index 28ac49d0a9..2394e75b61 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -1,14 +1,15 @@ import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; -import { HyperlinkHoverMenuFactory } from "../../menu-tools/HyperlinkHoverMenu/types"; import { getHyperlinkHoverMenuProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps"; +import { HyperlinkHoverMenuFactory } from "../../menu-tools/HyperlinkHoverMenu/types"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); export type HyperlinkMenuPluginProps = { hyperlinkMenuFactory: HyperlinkHoverMenuFactory; }; +// Rewrite to class? export const createHyperlinkMenuPlugin = ( editor: Editor, options: HyperlinkMenuPluginProps @@ -41,6 +42,10 @@ export const createHyperlinkMenuPlugin = ( let hyperlinkMark: Mark | undefined; let hyperlinkMarkRange: Range | undefined; + // initialize the hyperlinkMenu UI element + // the actual values are dummy values, as the menu isn't shown / positioned yet + // (TBD: we could also decide not to pass these values upon creation, + // or only initialize a menu upon first-use) let hyperlinkMenu = options.hyperlinkMenuFactory( getHyperlinkHoverMenuProps( "", @@ -60,7 +65,8 @@ export const createHyperlinkMenuPlugin = ( if (editor.state.selection.empty) { const marksAtPos = editor.state.selection.$from.marks(); - let foundHyperlinkMark = false; + keyboardHoveredHyperlinkMark = undefined; + keyboardHoveredHyperlinkMarkRange = undefined; for (const mark of marksAtPos) { if (mark.type.name === editor.schema.mark("link").type.name) { @@ -71,36 +77,28 @@ export const createHyperlinkMenuPlugin = ( mark.type, mark.attrs ) || undefined; - foundHyperlinkMark = true; break; } } - - if (!foundHyperlinkMark) { - keyboardHoveredHyperlinkMark = undefined; - keyboardHoveredHyperlinkMarkRange = undefined; - } } const prevHyperlinkMark = hyperlinkMark; - // Keyboard cursor position overrides mouse cursor position. + hyperlinkMark = undefined; + hyperlinkMarkRange = undefined; + if (mouseHoveredHyperlinkMark) { hyperlinkMark = mouseHoveredHyperlinkMark; hyperlinkMarkRange = mouseHoveredHyperlinkMarkRange; } + // Keyboard cursor position takes precedence over mouse hovered hyperlink. if (keyboardHoveredHyperlinkMark) { hyperlinkMark = keyboardHoveredHyperlinkMark; hyperlinkMarkRange = keyboardHoveredHyperlinkMarkRange; } - if (!mouseHoveredHyperlinkMark && !keyboardHoveredHyperlinkMark) { - hyperlinkMark = undefined; - hyperlinkMarkRange = undefined; - } - // Hides menu. if (prevHyperlinkMark && !hyperlinkMark) { hyperlinkMenu.element?.removeEventListener( diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts index 3b9fd98fbe..a30bd40447 100644 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts @@ -1,6 +1,7 @@ import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; import { BubbleMenuProps } from "./types"; +// TODO: don't create new functions every time, only execute necessary logic (isActive) export function getBubbleMenuProps(editor: Editor): BubbleMenuProps { return { marks: { diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 51b32362c2..210fec2c4d 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -1,5 +1,6 @@ import { Menu, MenuFactory } from "../types"; +// TODO: reconsider .set() function export type BasicMarkProps = { isActive: boolean; toggle: () => void; diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts index 83df9723ed..34ff23405a 100644 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts @@ -1,5 +1,6 @@ import { HyperlinkHoverMenuProps } from "./types"; +// TODO: remove nesting export function getHyperlinkHoverMenuProps( url: string, text: string, diff --git a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts index d28dc2fba3..4f44b8ccb0 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts +++ b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts @@ -1,6 +1,9 @@ -import { SuggestionsMenuProps } from "./types"; import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; +import { SuggestionsMenuProps } from "./types"; +// TODO: maybe later discuss if we want to delegate keyboard handling / filtering +// to the client (with automatic defaults) +// TODO: remove // TODO: Only need either getQuery or matchesQuery, not both. Depends if we want to allow users the ability to define // their own block type aliases/matching algorithm. export function getSuggestionsMenuProps( diff --git a/packages/core/src/menu-tools/SuggestionsMenu/types.ts b/packages/core/src/menu-tools/SuggestionsMenu/types.ts index fc9c2b8611..7035d18799 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/types.ts +++ b/packages/core/src/menu-tools/SuggestionsMenu/types.ts @@ -1,11 +1,12 @@ -import { Menu, MenuFactory } from "../types"; import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; +import { Menu, MenuFactory } from "../types"; export type SuggestionsMenuItem = { name: string; set: () => void; }; +// TODO: remove nesting export type SuggestionsMenuProps = { menuItems: { items: T[]; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index f7ec74e286..1a7a46c8cc 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -3,9 +3,9 @@ import { escapeRegExp } from "lodash"; import { Plugin, PluginKey, Selection } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; -import SuggestionItem from "./SuggestionItem"; -import { SuggestionsMenuFactory } from "../../../menu-tools/SuggestionsMenu/types"; import { getSuggestionsMenuProps } from "../../../menu-tools/SuggestionsMenu/getSuggestionsMenuProps"; +import { SuggestionsMenuFactory } from "../../../menu-tools/SuggestionsMenu/types"; +import SuggestionItem from "./SuggestionItem"; export type SuggestionPluginOptions = { /** @@ -115,6 +115,10 @@ export function createSuggestionPlugin({ view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true })); }; + // initialize the SuggestionsMenu UI element + // the actual values are dummy values, as the menu isn't shown / positioned yet + // (TBD: we could also decide not to pass these values upon creation, + // or only initialize a menu upon first-use) const suggestionsMenu = suggestionsMenuFactory( getSuggestionsMenuProps( [], diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 6ce1708275..24247eb2c9 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -1,13 +1,13 @@ -import { createRoot } from "react-dom/client"; import { MantineProvider } from "@mantine/core"; -import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; -import { BubbleMenu as ReactBubbleMenu } from "./components/BubbleMenu"; +import { createRoot } from "react-dom/client"; import tippy from "tippy.js"; +import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; import { BubbleMenu, BubbleMenuFactory, BubbleMenuProps, } from "../../../core/src/menu-tools/BubbleMenu/types"; +import { BubbleMenu as ReactBubbleMenu } from "./components/BubbleMenu"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactBubbleMenuFactory: BubbleMenuFactory = ( diff --git a/packages/react/src/shared/components/suggestion/SuggestionList.tsx b/packages/react/src/shared/components/suggestion/SuggestionList.tsx index 13e5686b9b..7b00c48454 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionList.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionList.tsx @@ -1,7 +1,7 @@ +import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; import { createStyles, Menu } from "@mantine/core"; -import { SuggestionListItem } from "./SuggestionListItem"; import { SuggestionsMenuProps } from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; -import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; +import { SuggestionListItem } from "./SuggestionListItem"; export type SuggestionListProps = SuggestionsMenuProps; diff --git a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx index c287e20274..f8eb8a41bd 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx @@ -1,14 +1,14 @@ -import { createRoot } from "react-dom/client"; +import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; import { MantineProvider } from "@mantine/core"; -import { BlockNoteTheme } from "../../../BlockNoteTheme"; +import { createRoot } from "react-dom/client"; import tippy from "tippy.js"; import { SuggestionsMenu, SuggestionsMenuFactory, SuggestionsMenuProps, } from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; +import { BlockNoteTheme } from "../../../BlockNoteTheme"; import { SuggestionList } from "./SuggestionList"; -import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< @@ -52,6 +52,8 @@ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory<
); + // setProps is a tippy function, + // update the position based on passed in props menu.setProps({ getReferenceClientRect: () => newProps.view.selectedBlockBoundingBox, }); From e22446b02d63ff177c91350c331a554b137433ad Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 21 Dec 2022 14:11:07 +0100 Subject: [PATCH 12/55] Split bubble menu props into init & update props Moved animation delay timeout to bubble menu plugin --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 13 +- .../BubbleMenu/getBubbleMenuInitProps.ts | 93 ++++++++++++++ .../BubbleMenu/getBubbleMenuUpdateProps.ts | 29 +++++ .../core/src/menu-tools/BubbleMenu/types.ts | 48 ++++++- .../src/BubbleMenu/BubbleMenuFactory.tsx | 75 +++++++++-- .../src/BubbleMenu/components/BubbleMenu.tsx | 118 +++++++++--------- .../components/LinkToolbarButton.tsx | 22 ++-- 7 files changed, 310 insertions(+), 88 deletions(-) create mode 100644 packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts create mode 100644 packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 84fff50d22..3c73a33b06 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -1,8 +1,9 @@ import { Editor, isTextSelection } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { getBubbleMenuProps } from "../../menu-tools/BubbleMenu/getBubbleMenuProps"; +import { getBubbleMenuInitProps } from "../../menu-tools/BubbleMenu/getBubbleMenuInitProps"; import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; +import { getBubbleMenuUpdateProps } from "../../menu-tools/BubbleMenu/getBubbleMenuUpdateProps"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files @@ -25,7 +26,7 @@ export interface BubbleMenuPluginProps { // TODO: do from previous code export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { const bubbleMenu = options.bubbleMenuFactory( - getBubbleMenuProps(options.editor) + getBubbleMenuInitProps(options.editor) ); // TODO: Is this callback needed? @@ -101,7 +102,7 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { const next = options.pluginKey.getState(view.state); if (!prev.show && next.show && !next.preventShow) { - bubbleMenu.show(getBubbleMenuProps(options.editor)); + bubbleMenu.show(getBubbleMenuUpdateProps(options.editor)); bubbleMenu.element!.addEventListener( "mousedown", @@ -120,7 +121,11 @@ export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { !next.preventShow && !next.preventUpdate ) { - bubbleMenu.update(getBubbleMenuProps(options.editor)); + // TODO: Waits 350ms for animations to complete, looks clunky. See TODO in getBubbleMenuInitProps for why + // this is necessary. + setTimeout(() => { + bubbleMenu.update(getBubbleMenuUpdateProps(options.editor)); + }, 350); return; } diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts new file mode 100644 index 0000000000..f6b0a9c6c4 --- /dev/null +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts @@ -0,0 +1,93 @@ +import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; +import { BubbleMenuInitProps } from "./types"; + +export function getBubbleMenuInitProps(editor: Editor): BubbleMenuInitProps { + return { + toggleBold: () => { + // Setting editor focus using a chained command instead causes bubble menu to flicker on click. + editor.view.focus(); + editor.commands.toggleBold(); + }, + toggleItalic: () => { + editor.view.focus(); + editor.commands.toggleItalic(); + }, + toggleUnderline: () => { + editor.view.focus(); + editor.commands.toggleUnderline(); + }, + toggleStrike: () => { + editor.view.focus(); + editor.commands.toggleStrike(); + }, + setHyperlink: (url: string, text?: string) => { + if (url === "") { + return; + } + + let { from, to } = editor.state.selection; + + if (!text) { + text = editor.state.doc.textBetween(from, to); + } + + const mark = editor.schema.mark("link", { href: url }); + + editor.view.dispatch( + editor.view.state.tr + .insertText(text, from, to) + .addMark(from, from + text.length, mark) + ); + }, + setParagraph: () => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "textContent" + ); + }, + setHeading: (level: string = "1") => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "headingContent", + { + headingLevel: level, + } + ); + }, + setListItem: (type: string = "unordered") => { + editor.view.focus(); + editor.commands.BNSetContentType( + editor.state.selection.from, + "listItemContent", + { + listItemType: type, + } + ); + }, + // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are + // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection + // bounding box used to create it is from an editor view that is out of date due to the animation. + getSelectionBoundingBox: () => { + const { state } = editor.view; + const { selection } = state; + + // support for CellSelections + const { ranges } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const to = Math.max(...ranges.map((range) => range.$to.pos)); + + if (isNodeSelection(selection)) { + const node = editor.view.nodeDOM(from) as HTMLElement; + + if (node) { + return node.getBoundingClientRect(); + } + } + + return posToDOMRect(editor.view, from, to); + }, + editorElement: editor.options.element, + }; +} diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts new file mode 100644 index 0000000000..8978664d29 --- /dev/null +++ b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts @@ -0,0 +1,29 @@ +import { Editor } from "@tiptap/core"; +import { BubbleMenuUpdateProps } from "./types"; + +export function getBubbleMenuUpdateProps( + editor: Editor +): BubbleMenuUpdateProps { + return { + boldIsActive: editor.isActive("bold"), + italicIsActive: editor.isActive("italic"), + underlineIsActive: editor.isActive("underline"), + strikeIsActive: editor.isActive("strike"), + hyperlinkIsActive: editor.isActive("link"), + activeHyperlinkUrl: editor.getAttributes("link").href, + activeHyperlinkText: editor.state.doc.textBetween( + editor.state.selection.from, + editor.state.selection.to + ), + paragraphIsActive: + editor.state.selection.$from.node().type.name === "textContent", + headingIsActive: + editor.state.selection.$from.node().type.name === "headingContent", + activeHeadingLevel: + editor.state.selection.$from.node().attrs["headingLevel"], + listItemIsActive: + editor.state.selection.$from.node().type.name === "listItemContent", + activeListItemType: + editor.state.selection.$from.node().attrs["listItemType"], + }; +} diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 210fec2c4d..772846bad0 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -1,4 +1,4 @@ -import { Menu, MenuFactory } from "../types"; +// import { Menu, MenuFactory } from "../types"; // TODO: reconsider .set() function export type BasicMarkProps = { @@ -52,5 +52,47 @@ export type BubbleMenuProps = { }; }; -export type BubbleMenu = Menu; -export type BubbleMenuFactory = MenuFactory; +export type BubbleMenuInitProps = { + toggleBold: () => void; + toggleItalic: () => void; + toggleUnderline: () => void; + toggleStrike: () => void; + setHyperlink: (url: string, text?: string) => void; + setParagraph: () => void; + setHeading: (level: string) => void; + setListItem: (type: string) => void; + getSelectionBoundingBox: () => DOMRect; + editorElement: Element; +}; + +export type BubbleMenuUpdateProps = { + boldIsActive: boolean; + italicIsActive: boolean; + underlineIsActive: boolean; + strikeIsActive: boolean; + hyperlinkIsActive: boolean; + activeHyperlinkUrl: string; + activeHyperlinkText: string; + paragraphIsActive: boolean; + headingIsActive: boolean; + activeHeadingLevel: string; + listItemIsActive: boolean; + activeListItemType: string; +}; + +type Menu = { + element: HTMLElement | undefined; + show: (props: MenuUpdateProps) => void; + hide: () => void; + update: (newProps: MenuUpdateProps) => void; +}; + +type MenuFactory = ( + initProps: MenuInitProps +) => Menu; + +export type BubbleMenu = Menu; +export type BubbleMenuFactory = MenuFactory< + BubbleMenuInitProps, + BubbleMenuUpdateProps +>; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 24247eb2c9..03bb7c79f5 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -5,21 +5,26 @@ import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; import { BubbleMenu, BubbleMenuFactory, - BubbleMenuProps, + BubbleMenuInitProps, + BubbleMenuUpdateProps, } from "../../../core/src/menu-tools/BubbleMenu/types"; -import { BubbleMenu as ReactBubbleMenu } from "./components/BubbleMenu"; +import { + BubbleMenu as ReactBubbleMenu, + BubbleMenuProps, +} from "./components/BubbleMenu"; // import rootStyles from "../../../core/src/root.module.css"; +// TODO: Rename init & update props to something like static & dynamic props? export const ReactBubbleMenuFactory: BubbleMenuFactory = ( - props: BubbleMenuProps + initProps: BubbleMenuInitProps ): BubbleMenu => { const element = document.createElement("div"); // element.className = rootStyles.bnRoot; const root = createRoot(element); - let menu = tippy(props.view.editorElement, { + let menu = tippy(initProps.editorElement, { duration: 0, - getReferenceClientRect: props.view.getSelectionBoundingBox, + getReferenceClientRect: initProps.getSelectionBoundingBox, content: element, interactive: true, trigger: "manual", @@ -27,27 +32,73 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( hideOnClick: "toggle", }); + const bubbleMenuProps: BubbleMenuProps = { + boldIsActive: false, + toggleBold: initProps.toggleBold, + italicIsActive: false, + toggleItalic: initProps.toggleItalic, + underlineIsActive: false, + toggleUnderline: initProps.toggleUnderline, + strikeIsActive: false, + toggleStrike: initProps.toggleStrike, + hyperlinkIsActive: false, + activeHyperlinkUrl: "", + activeHyperlinkText: "", + setHyperlink: initProps.setHyperlink, + + paragraphIsActive: false, + setParagraph: initProps.setParagraph, + headingIsActive: false, + activeHeadingLevel: "", + setHeading: initProps.setHeading, + listItemIsActive: false, + activeListItemType: "", + setListItem: initProps.setListItem, + }; + + function updateBubbleMenuProps(updateProps: BubbleMenuUpdateProps) { + // Can't use a constant and not all update props might be needed, though they are in this case. + // bubbleMenuProps = {...bubbleMenuProps, ...updateProps} + + bubbleMenuProps.boldIsActive = updateProps.boldIsActive; + bubbleMenuProps.italicIsActive = updateProps.italicIsActive; + bubbleMenuProps.underlineIsActive = updateProps.underlineIsActive; + bubbleMenuProps.strikeIsActive = updateProps.strikeIsActive; + bubbleMenuProps.hyperlinkIsActive = updateProps.hyperlinkIsActive; + bubbleMenuProps.activeHyperlinkUrl = updateProps.activeHyperlinkUrl; + bubbleMenuProps.activeHyperlinkText = updateProps.activeHyperlinkText; + + bubbleMenuProps.paragraphIsActive = updateProps.paragraphIsActive; + bubbleMenuProps.headingIsActive = updateProps.headingIsActive; + bubbleMenuProps.activeHeadingLevel = updateProps.activeHeadingLevel; + bubbleMenuProps.listItemIsActive = updateProps.listItemIsActive; + bubbleMenuProps.activeListItemType = updateProps.activeListItemType; + } + return { element: element as HTMLElement, - show: (props: BubbleMenuProps) => { + show: (updateProps: BubbleMenuUpdateProps) => { + updateBubbleMenuProps(updateProps); + root.render( - + ); + menu.show(); }, hide: menu.hide, - update: (newProps: BubbleMenuProps) => { + update: (updateProps: BubbleMenuUpdateProps) => { + updateBubbleMenuProps(updateProps); + root.render( - + ); - // TODO: Waits 500ms for animations to complete, looks clunky. See TODO in getBubbleMenuProps for why this is - // necessary. - setTimeout(() => menu.popperInstance?.forceUpdate(), 350); + menu.popperInstance?.forceUpdate(); }, }; }; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 261ae53bb1..8b61c950b0 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -18,47 +18,69 @@ import { ToolbarDropdown } from "../../shared/components/toolbar/ToolbarDropdown import { Toolbar } from "../../shared/components/toolbar/Toolbar"; import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; -import { BubbleMenuProps } from "../../../../core/src/menu-tools/BubbleMenu/types"; + +export type BubbleMenuProps = { + boldIsActive: boolean; + toggleBold: () => void; + italicIsActive: boolean; + toggleItalic: () => void; + underlineIsActive: boolean; + toggleUnderline: () => void; + strikeIsActive: boolean; + toggleStrike: () => void; + hyperlinkIsActive: boolean; + activeHyperlinkUrl: string; + activeHyperlinkText: string; + setHyperlink: (url: string, text?: string) => void; + + paragraphIsActive: boolean; + setParagraph: () => void; + headingIsActive: boolean; + activeHeadingLevel: string; + setHeading: (level: string) => void; + listItemIsActive: boolean; + activeListItemType: string; + setListItem: (type: string) => void; +}; // TODO: add list options, indentation export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { const getActiveMarks = () => { const activeMarks = new Set(); - props.bubbleMenuProps.marks.bold.isActive && activeMarks.add("bold"); - props.bubbleMenuProps.marks.italic.isActive && activeMarks.add("italic"); - props.bubbleMenuProps.marks.underline.isActive && - activeMarks.add("underline"); - props.bubbleMenuProps.marks.strike.isActive && activeMarks.add("strike"); - props.bubbleMenuProps.marks.hyperlink.isActive && activeMarks.add("link"); + props.bubbleMenuProps.boldIsActive && activeMarks.add("bold"); + props.bubbleMenuProps.italicIsActive && activeMarks.add("italic"); + props.bubbleMenuProps.underlineIsActive && activeMarks.add("underline"); + props.bubbleMenuProps.strikeIsActive && activeMarks.add("strike"); + props.bubbleMenuProps.hyperlinkIsActive && activeMarks.add("link"); return activeMarks; }; const getActiveBlock = () => { - if (props.bubbleMenuProps.blocks.paragraph.isActive) { + if (props.bubbleMenuProps.paragraphIsActive) { return { text: "Text", icon: RiText, }; } - if (props.bubbleMenuProps.blocks.heading.isActive) { - if (props.bubbleMenuProps.blocks.heading.level === "1") { + if (props.bubbleMenuProps.headingIsActive) { + if (props.bubbleMenuProps.activeHeadingLevel === "1") { return { text: "Heading 1", icon: RiH1, }; } - if (props.bubbleMenuProps.blocks.heading.level === "2") { + if (props.bubbleMenuProps.activeHeadingLevel === "2") { return { text: "Heading 2", icon: RiH2, }; } - if (props.bubbleMenuProps.blocks.heading.level === "3") { + if (props.bubbleMenuProps.activeHeadingLevel === "3") { return { text: "Heading 3", icon: RiH3, @@ -66,8 +88,8 @@ export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { } } - if (props.bubbleMenuProps.blocks.listItem.isActive) { - if (props.bubbleMenuProps.blocks.listItem.type === "unordered") { + if (props.bubbleMenuProps.listItemIsActive) { + if (props.bubbleMenuProps.activeListItemType === "unordered") { return { text: "Bullet List", icon: RiListUnordered, @@ -93,97 +115,76 @@ export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { icon={activeBlock!.icon} items={[ { - onClick: () => { - // Setting editor focus using a chained command instead causes bubble menu to flicker on click. - props.bubbleMenuProps.blocks.paragraph.set(); - }, + onClick: () => props.bubbleMenuProps.setParagraph(), text: "Text", icon: RiText, - isSelected: props.bubbleMenuProps.blocks.paragraph.isActive, + isSelected: props.bubbleMenuProps.paragraphIsActive, }, { - onClick: () => { - props.bubbleMenuProps.blocks.heading.set("1"); - }, + onClick: () => props.bubbleMenuProps.setHeading("1"), text: "Heading 1", icon: RiH1, isSelected: - props.bubbleMenuProps.blocks.heading.isActive && - props.bubbleMenuProps.blocks.heading.level === "1", + props.bubbleMenuProps.headingIsActive && + props.bubbleMenuProps.activeHeadingLevel === "1", }, { - onClick: () => { - props.bubbleMenuProps.blocks.heading.set("2"); - }, + onClick: () => props.bubbleMenuProps.setHeading("2"), text: "Heading 2", icon: RiH2, isSelected: - props.bubbleMenuProps.blocks.heading.isActive && - props.bubbleMenuProps.blocks.heading.level === "2", + props.bubbleMenuProps.headingIsActive && + props.bubbleMenuProps.activeHeadingLevel === "2", }, { - onClick: () => { - props.bubbleMenuProps.blocks.heading.set("3"); - }, + onClick: () => props.bubbleMenuProps.setHeading("3"), text: "Heading 3", icon: RiH3, isSelected: - props.bubbleMenuProps.blocks.heading.isActive && - props.bubbleMenuProps.blocks.heading.level === "3", + props.bubbleMenuProps.headingIsActive && + props.bubbleMenuProps.activeHeadingLevel === "3", }, { - onClick: () => { - props.bubbleMenuProps.blocks.listItem.set("unordered"); - }, + onClick: () => props.bubbleMenuProps.setListItem("unordered"), text: "Bullet List", icon: RiListUnordered, isSelected: - props.bubbleMenuProps.blocks.listItem.isActive && - props.bubbleMenuProps.blocks.listItem.type === "unordered", + props.bubbleMenuProps.listItemIsActive && + props.bubbleMenuProps.activeListItemType === "unordered", }, { - onClick: () => { - props.bubbleMenuProps.blocks.listItem.set("ordered"); - }, + onClick: () => props.bubbleMenuProps.setListItem("ordered"), text: "Numbered List", icon: RiListOrdered, isSelected: - props.bubbleMenuProps.blocks.listItem.isActive && - props.bubbleMenuProps.blocks.listItem.type === "ordered", + props.bubbleMenuProps.listItemIsActive && + props.bubbleMenuProps.activeListItemType === "ordered", }, ]} /> { - props.bubbleMenuProps.marks.bold.toggle(); - }} + onClick={props.bubbleMenuProps.toggleBold} isSelected={activeMarks.has("bold")} mainTooltip="Bold" secondaryTooltip={formatKeyboardShortcut("Mod+B")} icon={RiBold} /> { - props.bubbleMenuProps.marks.italic.toggle(); - }} + onClick={props.bubbleMenuProps.toggleItalic} isSelected={activeMarks.has("italic")} mainTooltip="Italic" secondaryTooltip={formatKeyboardShortcut("Mod+I")} icon={RiItalic} /> { - props.bubbleMenuProps.marks.underline.toggle(); - }} + onClick={props.bubbleMenuProps.toggleUnderline} isSelected={activeMarks.has("underline")} mainTooltip="Underline" secondaryTooltip={formatKeyboardShortcut("Mod+U")} icon={RiUnderline} /> { - props.bubbleMenuProps.marks.strike.toggle(); - }} + onClick={props.bubbleMenuProps.toggleStrike} isSelected={activeMarks.has("strike")} mainTooltip="Strike-through" secondaryTooltip={formatKeyboardShortcut("Mod+Shift+X")} @@ -225,11 +226,14 @@ export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { /> {/* void; }; /** * The link menu button opens a tooltip on click */ -export const LinkToolbarButton = (props: Props) => { +export const LinkToolbarButton = (props: HyperlinkButtonProps) => { const [creationMenu, setCreationMenu] = useState(); // TODO: review code; does this pattern still make sense? @@ -22,16 +24,12 @@ export const LinkToolbarButton = (props: Props) => { setCreationMenu( ); - }, [props.hyperlinkMarkProps]); + }, [props]); return ( Date: Wed, 21 Dec 2022 15:05:36 +0100 Subject: [PATCH 13/55] clean up packaging --- examples/editor/package.json | 1 + examples/editor/src/main.tsx | 12 +- examples/editor/vite.config.ts | 6 +- package-lock.json | 214 +++++++++++++----- packages/core/package.json | 9 - packages/core/src/BlockNoteEditor.ts | 2 +- packages/core/src/BlockNoteTheme.ts | 150 ------------ .../DraggableBlocks/DraggableBlocksPlugin.tsx | 27 +-- .../SlashMenu/SlashMenuExtension.ts | 7 +- .../src/extensions/SlashMenu/SlashMenuItem.ts | 2 +- packages/core/src/index.ts | 5 + .../getSuggestionsMenuProps.ts | 2 +- .../src/menu-tools/SuggestionsMenu/types.ts | 2 +- .../plugins/suggestion/SuggestionItem.ts | 2 +- .../plugins/suggestion/SuggestionPlugin.ts | 2 +- packages/core/vite.config.ts | 5 +- packages/react/package.json | 27 ++- .../src/BubbleMenu/BubbleMenuFactory.tsx | 10 +- .../src/BubbleMenu/components/BubbleMenu.tsx | 4 +- .../components/LinkToolbarButton.tsx | 4 +- packages/react/src/Editor/useEditor.ts | 2 +- packages/react/src/EditorContent.tsx | 2 + .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 12 +- packages/react/src/index.ts | 5 + .../components/suggestion/SuggestionList.tsx | 3 +- .../suggestion/SuggestionListItem.tsx | 6 +- .../suggestion/SuggestionsMenuFactory.tsx | 10 +- packages/react/tsconfig.json | 24 ++ packages/react/vite.config.ts | 31 +++ 29 files changed, 314 insertions(+), 274 deletions(-) delete mode 100644 packages/core/src/BlockNoteTheme.ts create mode 100644 packages/react/src/EditorContent.tsx create mode 100644 packages/react/src/index.ts create mode 100644 packages/react/tsconfig.json create mode 100644 packages/react/vite.config.ts diff --git a/examples/editor/package.json b/examples/editor/package.json index a14029b314..b8145f8c85 100644 --- a/examples/editor/package.json +++ b/examples/editor/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@blocknote/core": "^0.1.2", + "@blocknote/react": "^0.1.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 948606aabc..e75e6b727b 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,10 +1,12 @@ import styles from "./App.module.css"; import "./index.css"; -// TODO: fix imports -import { mountBlockNoteEditor } from "../../../packages/core/src/BlockNoteEditor"; -import { ReactBubbleMenuFactory } from "../../../packages/react/src/BubbleMenu/BubbleMenuFactory"; -import { ReactHyperlinkMenuFactory } from "../../../packages/react/src/HyperlinkMenus/HyperlinkMenuFactory"; -import { ReactSuggestionsMenuFactory } from "../../../packages/react/src/shared/components/suggestion/SuggestionsMenuFactory"; + +import { mountBlockNoteEditor } from "@blocknote/core"; +import { + ReactBubbleMenuFactory, + ReactHyperlinkMenuFactory, + ReactSuggestionsMenuFactory, +} from "@blocknote/react"; // type WindowWithProseMirror = Window & // typeof globalThis & { ProseMirror: Editor }; diff --git a/examples/editor/vite.config.ts b/examples/editor/vite.config.ts index 9c31e3ff42..4910cd49aa 100644 --- a/examples/editor/vite.config.ts +++ b/examples/editor/vite.config.ts @@ -13,12 +13,16 @@ export default defineConfig((conf) => ({ conf.command === "build" ? {} : { - // Comment out the line below to load a built version of blocknote + // Comment out the lines below to load a built version of blocknote // or, keep as is to load live from sources with live reload working "@blocknote/core": path.resolve( __dirname, "../../packages/core/src/" ), + "@blocknote/react": path.resolve( + __dirname, + "../../packages/react/src/" + ), }, }, })); diff --git a/package-lock.json b/package-lock.json index 5d34b07529..47637d1ec5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "version": "0.1.2", "dependencies": { "@blocknote/core": "^0.1.2", + "@blocknote/react": "^0.1.2", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -2061,6 +2062,10 @@ "resolved": "examples/editor", "link": true }, + "node_modules/@blocknote/react": { + "resolved": "packages/react", + "link": true + }, "node_modules/@emotion/babel-plugin": { "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", @@ -4212,10 +4217,12 @@ } }, "node_modules/@tiptap/extension-bubble-menu": { - "version": "2.0.0-beta.199", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.199.tgz", - "integrity": "sha512-T3K8xoDbX6J62lhIUpclQoW/1XFt7yfI5DCoxtVWUeKaF+pG6kdsB3CPG5C/+AQVlz2jSIJmQuPf8RQFpQs+yg==", + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.209.tgz", + "integrity": "sha512-tceZAuDpy3J96uGyCzpJFD3fHABJDTJTq5E0hm+TRQT+eVGVqZI0PE3/4yVFgkCshioTuJq8veMDFcqNsSkKsQ==", "dependencies": { + "@tiptap/core": "^2.0.0-beta.209", + "lodash": "^4.17.21", "prosemirror-state": "^1.4.1", "prosemirror-view": "^1.28.2", "tippy.js": "^6.3.7" @@ -4225,7 +4232,37 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/core": "^2.0.0-beta.193" + "@tiptap/core": "^2.0.0-beta.193", + "prosemirror-state": "^1.4.1", + "prosemirror-view": "^1.28.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu/node_modules/@tiptap/core": { + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.0-beta.209.tgz", + "integrity": "sha512-DOOzfo2XKD5Qt2oEGW33/6ugwSnvpl4WbxtlKdPadLoApk6Kja3K1Eps3pihBgIGmo4tkctkCzmj8wNWS7KeWg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "prosemirror-commands": "^1.3.1", + "prosemirror-keymap": "^1.2.0", + "prosemirror-model": "^1.18.1", + "prosemirror-schema-list": "^1.2.2", + "prosemirror-state": "^1.4.1", + "prosemirror-transform": "^1.7.0", + "prosemirror-view": "^1.28.2" + } + }, + "node_modules/@tiptap/extension-bubble-menu/node_modules/prosemirror-view": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz", + "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==", + "dependencies": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" } }, "node_modules/@tiptap/extension-code": { @@ -4286,23 +4323,6 @@ "@tiptap/core": "^2.0.0-beta.193" } }, - "node_modules/@tiptap/extension-floating-menu": { - "version": "2.0.0-beta.199", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.199.tgz", - "integrity": "sha512-ELjqnNbxW66uqg54zlP2b4EVYUWvT2WvHmeOXALzoLlNzbqUopIl3XNRsvU2Dv1W88C1UjKgnRZIkHKFE1X3CA==", - "dependencies": { - "prosemirror-state": "^1.4.1", - "prosemirror-view": "^1.28.2", - "tippy.js": "^6.3.7" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/ueberdosis" - }, - "peerDependencies": { - "@tiptap/core": "^2.0.0-beta.193" - } - }, "node_modules/@tiptap/extension-gapcursor": { "version": "2.0.0-beta.199", "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.199.tgz", @@ -4438,12 +4458,12 @@ } }, "node_modules/@tiptap/react": { - "version": "2.0.0-beta.199", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.199.tgz", - "integrity": "sha512-AjBtoavcJ7WOoEXdJlrVEdEv6xuI5UFnqB88w8NlORSkWbfQ3uuOm3A0LUZ92/SsBz6NISZbsFahMy0DYgGbIA==", + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.209.tgz", + "integrity": "sha512-iuJ+hgaSPETPMX39QpX6e0tZmFAj9azl0qGhNm6NNB1biCehkB4qMfcfwecWFRWVpZKG5UtjJvjJ3UZM167Jlg==", "dependencies": { - "@tiptap/extension-bubble-menu": "^2.0.0-beta.199", - "@tiptap/extension-floating-menu": "^2.0.0-beta.199", + "@tiptap/extension-bubble-menu": "^2.0.0-beta.209", + "@tiptap/extension-floating-menu": "^2.0.0-beta.209", "prosemirror-view": "^1.28.2" }, "funding": { @@ -4456,6 +4476,33 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, + "node_modules/@tiptap/react/node_modules/@tiptap/extension-floating-menu": { + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.209.tgz", + "integrity": "sha512-m5ucAguqDxuOvNcsmvuSLcN8TMkbhFmiC6dTJOyaAGjGn6d8Ly6aZh+lEwU228TebM0TKHTp8Xob1cLjV4TGgg==", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0-beta.193", + "prosemirror-state": "^1.4.1", + "prosemirror-view": "^1.28.2" + } + }, + "node_modules/@tiptap/react/node_modules/prosemirror-view": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz", + "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==", + "dependencies": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -13645,8 +13692,6 @@ "@emotion/cache": "^11.10.5", "@emotion/serialize": "^1.1.1", "@emotion/utils": "^1.2.0", - "@mantine/core": "^5.6.1", - "@tippyjs/react": "^4.2.6", "@tiptap/core": "^2.0.0-beta.182", "@tiptap/extension-bold": "^2.0.0-beta.28", "@tiptap/extension-code": "^2.0.0-beta.28", @@ -13663,13 +13708,10 @@ "@tiptap/extension-strike": "^2.0.0-beta.29", "@tiptap/extension-text": "^2.0.0-beta.17", "@tiptap/extension-underline": "^2.0.0-beta.25", - "@tiptap/react": "^2.0.0-beta.114", "lodash": "^4.17.21", "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", "react-icons": "^4.3.1", "uuid": "^8.3.2" }, @@ -13684,6 +13726,26 @@ "typescript": "^4.5.4", "vite": "^3.0.5", "vite-plugin-eslint": "^1.7.0" + } + }, + "packages/react": { + "version": "0.1.2", + "dependencies": { + "@blocknote/core": "^0.1.2", + "@mantine/core": "^5.6.1", + "@tippyjs/react": "^4.2.6", + "@tiptap/react": "^2.0.0-beta.207" + }, + "devDependencies": { + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "@vitejs/plugin-react": "^1.0.7", + "eslint": "^8.10.0", + "eslint-config-react-app": "^7.0.0", + "prettier": "^2.7.1", + "typescript": "^4.5.4", + "vite": "^3.0.5", + "vite-plugin-eslint": "^1.7.0" }, "peerDependencies": { "react": "^18", @@ -15092,8 +15154,6 @@ "@emotion/cache": "^11.10.5", "@emotion/serialize": "^1.1.1", "@emotion/utils": "^1.2.0", - "@mantine/core": "^5.6.1", - "@tippyjs/react": "^4.2.6", "@tiptap/core": "^2.0.0-beta.182", "@tiptap/extension-bold": "^2.0.0-beta.28", "@tiptap/extension-code": "^2.0.0-beta.28", @@ -15110,7 +15170,6 @@ "@tiptap/extension-strike": "^2.0.0-beta.29", "@tiptap/extension-text": "^2.0.0-beta.17", "@tiptap/extension-underline": "^2.0.0-beta.25", - "@tiptap/react": "^2.0.0-beta.114", "@types/lodash": "^4.14.179", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", @@ -15122,8 +15181,6 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", "react-icons": "^4.3.1", "typescript": "^4.5.4", "uuid": "^8.3.2", @@ -15135,6 +15192,7 @@ "version": "file:examples/editor", "requires": { "@blocknote/core": "^0.1.2", + "@blocknote/react": "^0.1.2", "@types/react": "^18.0.25", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react": "^1.0.7", @@ -15147,6 +15205,24 @@ "vite-plugin-eslint": "^1.7.0" } }, + "@blocknote/react": { + "version": "file:packages/react", + "requires": { + "@blocknote/core": "^0.1.2", + "@mantine/core": "^5.6.1", + "@tippyjs/react": "^4.2.6", + "@tiptap/react": "^2.0.0-beta.207", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "@vitejs/plugin-react": "^1.0.7", + "eslint": "^8.10.0", + "eslint-config-react-app": "^7.0.0", + "prettier": "^2.7.1", + "typescript": "^4.5.4", + "vite": "^3.0.5", + "vite-plugin-eslint": "^1.7.0" + } + }, "@emotion/babel-plugin": { "version": "11.10.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz", @@ -16860,13 +16936,33 @@ "requires": {} }, "@tiptap/extension-bubble-menu": { - "version": "2.0.0-beta.199", - "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.199.tgz", - "integrity": "sha512-T3K8xoDbX6J62lhIUpclQoW/1XFt7yfI5DCoxtVWUeKaF+pG6kdsB3CPG5C/+AQVlz2jSIJmQuPf8RQFpQs+yg==", + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.0.0-beta.209.tgz", + "integrity": "sha512-tceZAuDpy3J96uGyCzpJFD3fHABJDTJTq5E0hm+TRQT+eVGVqZI0PE3/4yVFgkCshioTuJq8veMDFcqNsSkKsQ==", "requires": { + "@tiptap/core": "^2.0.0-beta.209", + "lodash": "^4.17.21", "prosemirror-state": "^1.4.1", "prosemirror-view": "^1.28.2", "tippy.js": "^6.3.7" + }, + "dependencies": { + "@tiptap/core": { + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.0.0-beta.209.tgz", + "integrity": "sha512-DOOzfo2XKD5Qt2oEGW33/6ugwSnvpl4WbxtlKdPadLoApk6Kja3K1Eps3pihBgIGmo4tkctkCzmj8wNWS7KeWg==", + "requires": {} + }, + "prosemirror-view": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz", + "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==", + "requires": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + } } }, "@tiptap/extension-code": { @@ -16900,16 +16996,6 @@ "prosemirror-dropcursor": "1.5.0" } }, - "@tiptap/extension-floating-menu": { - "version": "2.0.0-beta.199", - "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.199.tgz", - "integrity": "sha512-ELjqnNbxW66uqg54zlP2b4EVYUWvT2WvHmeOXALzoLlNzbqUopIl3XNRsvU2Dv1W88C1UjKgnRZIkHKFE1X3CA==", - "requires": { - "prosemirror-state": "^1.4.1", - "prosemirror-view": "^1.28.2", - "tippy.js": "^6.3.7" - } - }, "@tiptap/extension-gapcursor": { "version": "2.0.0-beta.199", "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.0.0-beta.199.tgz", @@ -16981,13 +17067,33 @@ "requires": {} }, "@tiptap/react": { - "version": "2.0.0-beta.199", - "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.199.tgz", - "integrity": "sha512-AjBtoavcJ7WOoEXdJlrVEdEv6xuI5UFnqB88w8NlORSkWbfQ3uuOm3A0LUZ92/SsBz6NISZbsFahMy0DYgGbIA==", + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.0.0-beta.209.tgz", + "integrity": "sha512-iuJ+hgaSPETPMX39QpX6e0tZmFAj9azl0qGhNm6NNB1biCehkB4qMfcfwecWFRWVpZKG5UtjJvjJ3UZM167Jlg==", "requires": { - "@tiptap/extension-bubble-menu": "^2.0.0-beta.199", - "@tiptap/extension-floating-menu": "^2.0.0-beta.199", + "@tiptap/extension-bubble-menu": "^2.0.0-beta.209", + "@tiptap/extension-floating-menu": "^2.0.0-beta.209", "prosemirror-view": "^1.28.2" + }, + "dependencies": { + "@tiptap/extension-floating-menu": { + "version": "2.0.0-beta.209", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.0.0-beta.209.tgz", + "integrity": "sha512-m5ucAguqDxuOvNcsmvuSLcN8TMkbhFmiC6dTJOyaAGjGn6d8Ly6aZh+lEwU228TebM0TKHTp8Xob1cLjV4TGgg==", + "requires": { + "tippy.js": "^6.3.7" + } + }, + "prosemirror-view": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.29.1.tgz", + "integrity": "sha512-OhujVZSDsh0l0PyHNdfaBj6DBkbhYaCfbaxmTeFrMKd/eWS+G6IC+OAbmR9IsLC8Se1HSbphMaXnsXjupHL3UQ==", + "requires": { + "prosemirror-model": "^1.16.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + } } }, "@tootallnate/once": { diff --git a/packages/core/package.json b/packages/core/package.json index 41142850f6..8e572620d0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -49,8 +49,6 @@ "@emotion/cache": "^11.10.5", "@emotion/serialize": "^1.1.1", "@emotion/utils": "^1.2.0", - "@mantine/core": "^5.6.1", - "@tippyjs/react": "^4.2.6", "@tiptap/core": "^2.0.0-beta.182", "@tiptap/extension-bold": "^2.0.0-beta.28", "@tiptap/extension-code": "^2.0.0-beta.28", @@ -67,20 +65,13 @@ "@tiptap/extension-strike": "^2.0.0-beta.29", "@tiptap/extension-text": "^2.0.0-beta.17", "@tiptap/extension-underline": "^2.0.0-beta.25", - "@tiptap/react": "^2.0.0-beta.114", "lodash": "^4.17.21", "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", "react-icons": "^4.3.1", "uuid": "^8.3.2" }, - "peerDependencies": { - "react": "^18", - "react-dom": "^18" - }, "devDependencies": { "@types/lodash": "^4.14.179", "@types/react": "^18.0.25", diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 0f192386dd..d64c6698b8 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -6,7 +6,7 @@ import { BubbleMenuFactory } from "./menu-tools/BubbleMenu/types"; import { HyperlinkHoverMenuFactory } from "./menu-tools/HyperlinkHoverMenu/types"; import { SuggestionsMenuFactory } from "./menu-tools/SuggestionsMenu/types"; import rootStyles from "./root.module.css"; -import SuggestionItem from "./shared/plugins/suggestion/SuggestionItem"; +import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; diff --git a/packages/core/src/BlockNoteTheme.ts b/packages/core/src/BlockNoteTheme.ts deleted file mode 100644 index 61b69bd9fc..0000000000 --- a/packages/core/src/BlockNoteTheme.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { MantineThemeOverride } from "@mantine/core"; - -export const BlockNoteTheme: MantineThemeOverride = { - activeStyles: { - // Removes button press effect. - transform: "none", - }, - colorScheme: "light", - colors: { - brandFinal: [ - "#F6F6F8", - "#ECEDF0", - "#DFE1E6", - "#C2C7D0", - "#A6ADBA", - "#8993A4", - "#6D798F", - "#505F79", - "#344563", - "#172B4D", - ], - }, - components: { - Menu: { - styles: (theme) => ({ - dropdown: { - backgroundColor: "white", - boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`, - border: `1px solid ${theme.colors.brandFinal[1]}`, - borderRadius: "6px", - padding: "2px", - }, - }), - }, - DragHandleMenu: { - styles: (theme) => ({ - root: { - ".mantine-Menu-item": { - color: theme.colors.brandFinal, - fontSize: "12px", - height: "34px", - }, - }, - }), - }, - EditHyperlinkMenu: { - styles: (theme) => ({ - root: { - backgroundColor: "white", - boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`, - border: `1px solid ${theme.colors.brandFinal[1]}`, - borderRadius: "6px", - gap: "4px", - minWidth: "145px", - padding: "2px", - // Row - ".mantine-Group-root": { - flexWrap: "nowrap", - gap: "8px", - paddingInline: "6px", - // Row icon - ".mantine-Container-root": { - color: theme.colors.brandFinal, - display: "flex", - justifyContent: "center", - padding: "0", - width: "fit-content", - }, - // Row input field - ".mantine-TextInput-root": { - background: "transparent", - width: "300px", - ".mantine-TextInput-wrapper": { - ".mantine-TextInput-input": { - fontSize: "12px", - border: 0, - padding: 0, - }, - }, - }, - }, - }, - }), - }, - Toolbar: { - styles: (theme) => ({ - root: { - backgroundColor: "white", - boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`, - border: `1px solid ${theme.colors.brandFinal[1]}`, - borderRadius: "6px", - flexWrap: "nowrap", - gap: "2px", - padding: "2px", - width: "fit-content", - // Button (including dropdown target) - ".mantine-UnstyledButton-root": { - borderRadius: "4px", - }, - // Dropdown - ".mantine-Menu-dropdown": { - // Dropdown item - ".mantine-Menu-item": { - color: theme.colors.brandFinal, - fontSize: "12px", - height: "34px", - ".mantine-Menu-itemRightSection": { - paddingLeft: "5px", - }, - }, - }, - }, - }), - }, - SuggestionList: { - styles: (theme) => ({ - root: { - // ...theme.other.defaultMenuStyles(theme), - ".mantine-Menu-item": { - // Icon - ".mantine-Menu-itemIcon": { - padding: "8px", - border: `1px solid ${theme.colors.brandFinal[2]}`, - backgroundColor: theme.colors.brandFinal[0], - borderRadius: "4px", - color: theme.colors.brandFinal, - }, - // Text - ".mantine-Menu-itemLabel": { - color: theme.colors.brandFinal, - paddingRight: "16px", - ".mantine-Stack-root": { - gap: "0", - }, - }, - // Badge (keyboard shortcut) - ".mantine-Menu-itemRightSection": { - ".mantine-Badge-root": { - border: `1px solid ${theme.colors.brandFinal[2]}`, - }, - }, - }, - }, - }), - }, - }, - fontFamily: "Inter", - primaryColor: "brandFinal", - primaryShade: 9, -}; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx index d007ccb089..f0f5011239 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx @@ -1,11 +1,10 @@ -import { MantineProvider } from "@mantine/core"; +import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state"; import * as pv from "prosemirror-view"; import { EditorView } from "prosemirror-view"; -import { Editor } from "@tiptap/core"; import { createRoot, Root } from "react-dom/client"; -import { BlockNoteTheme } from "../../BlockNoteTheme"; +// import { BlockNoteTheme } from "../../BlockNoteTheme"; import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection"; import { DragHandle } from "./components/DragHandle"; @@ -355,17 +354,19 @@ export const createDraggableBlocksPlugin = (editor: Editor) => { dropElement.style.left = left + "px"; dropElement.style.top = rect.top + "px"; + // MantineProvider has been commented out because I removed + // BlockNoteTheme. I know this will be part of the DraggableBlocks rewrite anyway dropElementRoot.render( - - - + // + + // ); return true; }, diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts index ca51d42415..8cbde64e42 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts @@ -1,11 +1,13 @@ import { Extension } from "@tiptap/core"; +import { PluginKey } from "prosemirror-state"; +import { SuggestionsMenuFactory } from "../../menu-tools/SuggestionsMenu/types"; import { createSuggestionPlugin } from "../../shared/plugins/suggestion/SuggestionPlugin"; import defaultCommands from "./defaultCommands"; import { SlashMenuItem } from "./SlashMenuItem"; -import { PluginKey } from "prosemirror-state"; export type SlashMenuOptions = { commands: { [key: string]: SlashMenuItem }; + suggestionsMenuFactory: SuggestionsMenuFactory | undefined; }; export const SlashMenuPluginKey = new PluginKey("suggestions-slash-commands"); @@ -16,6 +18,7 @@ export const SlashMenuExtension = Extension.create({ addOptions() { return { commands: defaultCommands, + suggestionsMenuFactory: undefined, // TODO: fix undefined }; }, @@ -25,7 +28,7 @@ export const SlashMenuExtension = Extension.create({ pluginKey: SlashMenuPluginKey, editor: this.editor, char: "/", - suggestionsMenuFactory: this.options.suggestionsMenuFactory, + suggestionsMenuFactory: this.options.suggestionsMenuFactory!, items: (query) => { const commands = []; diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts b/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts index 0124ecb76b..76e76da6ce 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts @@ -1,6 +1,6 @@ import { Editor, Range } from "@tiptap/core"; -import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; import { IconType } from "react-icons"; +import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; export type SlashMenuCallback = (editor: Editor, range: Range) => boolean; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4ec3fed60a..62f1c1af0d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,10 @@ import "./globals.css"; +export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; export * from "./EditorContent"; +export * from "./menu-tools/BubbleMenu/types"; +export * from "./menu-tools/HyperlinkHoverMenu/types"; +export * from "./menu-tools/SuggestionsMenu/types"; +export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; export * from "./useEditor"; diff --git a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts index 4f44b8ccb0..31e6654532 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts +++ b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts @@ -1,4 +1,4 @@ -import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; +import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; import { SuggestionsMenuProps } from "./types"; // TODO: maybe later discuss if we want to delegate keyboard handling / filtering diff --git a/packages/core/src/menu-tools/SuggestionsMenu/types.ts b/packages/core/src/menu-tools/SuggestionsMenu/types.ts index 7035d18799..60cfff93cd 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/types.ts +++ b/packages/core/src/menu-tools/SuggestionsMenu/types.ts @@ -1,4 +1,4 @@ -import SuggestionItem from "../../shared/plugins/suggestion/SuggestionItem"; +import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; import { Menu, MenuFactory } from "../types"; export type SuggestionsMenuItem = { diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts b/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts index 9f22687fb8..688aaea88e 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts @@ -3,7 +3,7 @@ import { IconType } from "react-icons"; /** * A generic interface used in all suggestion menus (slash menu, mentions, etc) */ -export default interface SuggestionItem { +export interface SuggestionItem { /** * The name of the item */ diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 1a7a46c8cc..9bbd91b335 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -5,7 +5,7 @@ import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; import { getSuggestionsMenuProps } from "../../../menu-tools/SuggestionsMenu/getSuggestionsMenuProps"; import { SuggestionsMenuFactory } from "../../../menu-tools/SuggestionsMenu/types"; -import SuggestionItem from "./SuggestionItem"; +import { SuggestionItem } from "./SuggestionItem"; export type SuggestionPluginOptions = { /** diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index b7bd0b0704..ff80ac1d93 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -20,10 +20,7 @@ export default defineConfig({ output: { // Provide global variables to use in the UMD build // for externalized deps - globals: { - react: "React", - "react-dom": "ReactDOM", - }, + globals: {}, }, }, }, diff --git a/packages/react/package.json b/packages/react/package.json index f67e65a9c4..9f8a787c98 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -3,6 +3,21 @@ "homepage": "https://github.com/yousefed/blocknote", "private": false, "version": "0.1.2", + "type": "module", + "source": "src/index.ts", + "types": "./types/src/index.d.ts", + "main": "./dist/blocknote-react.umd.cjs", + "module": "./dist/blocknote-react.js", + "exports": { + ".": { + "import": "./dist/blocknote-react.js", + "require": "./dist/blocknote-react.umd.cjs" + }, + "./style.css": { + "import": "./dist/style.css", + "require": "./dist/style.css" + } + }, "scripts": { "dev": "vite", "build": "tsc && vite build", @@ -11,11 +26,10 @@ "lint": "eslint ../menus/src --max-warnings 0" }, "dependencies": { + "@mantine/core": "^5.6.1", "@blocknote/core": "^0.1.2", "@tippyjs/react": "^4.2.6", - "@tiptap/react": "^2.0.0-beta.207", - "react": "^18", - "react-dom": "^18" + "@tiptap/react": "^2.0.0-beta.207" }, "devDependencies": { "@types/react": "^18.0.25", @@ -25,7 +39,12 @@ "prettier": "^2.7.1", "typescript": "^4.5.4", "vite": "^3.0.5", - "vite-plugin-eslint": "^1.7.0" + "vite-plugin-eslint": "^1.7.0", + "@vitejs/plugin-react": "^1.0.7" + }, + "peerDependencies": { + "react": "^18", + "react-dom": "^18" }, "eslintConfig": { "extends": [ diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 24247eb2c9..81614ebf1e 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -1,12 +1,12 @@ -import { MantineProvider } from "@mantine/core"; -import { createRoot } from "react-dom/client"; -import tippy from "tippy.js"; -import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; import { BubbleMenu, BubbleMenuFactory, BubbleMenuProps, -} from "../../../core/src/menu-tools/BubbleMenu/types"; +} from "@blocknote/core"; +import { MantineProvider } from "@mantine/core"; +import { createRoot } from "react-dom/client"; +import tippy from "tippy.js"; +import { BlockNoteTheme } from "../BlockNoteTheme"; import { BubbleMenu as ReactBubbleMenu } from "./components/BubbleMenu"; // import rootStyles from "../../../core/src/root.module.css"; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 261ae53bb1..16391ba1ba 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -1,3 +1,4 @@ +import { BubbleMenuProps } from "@blocknote/core"; import { RiBold, RiH1, @@ -13,12 +14,11 @@ import { RiText, RiUnderline, } from "react-icons/ri"; +import { Toolbar } from "../../shared/components/toolbar/Toolbar"; import { ToolbarButton } from "../../shared/components/toolbar/ToolbarButton"; import { ToolbarDropdown } from "../../shared/components/toolbar/ToolbarDropdown"; -import { Toolbar } from "../../shared/components/toolbar/Toolbar"; import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; -import { BubbleMenuProps } from "../../../../core/src/menu-tools/BubbleMenu/types"; // TODO: add list options, indentation export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { diff --git a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx index 2c2420ae2b..88dd525db7 100644 --- a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx +++ b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx @@ -1,11 +1,11 @@ +import { HyperlinkMarkProps } from "@blocknote/core"; import Tippy from "@tippyjs/react"; import { useCallback, useState } from "react"; +import { EditHyperlinkMenu } from "../../HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenu"; import { ToolbarButton, ToolbarButtonProps, } from "../../shared/components/toolbar/ToolbarButton"; -import { EditHyperlinkMenu } from "../../HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenu"; -import { HyperlinkMarkProps } from "../../../../core/src/menu-tools/BubbleMenu/types"; type Props = ToolbarButtonProps & { hyperlinkMarkProps: HyperlinkMarkProps; diff --git a/packages/react/src/Editor/useEditor.ts b/packages/react/src/Editor/useEditor.ts index 3d6c7a2d68..9dc75db22b 100644 --- a/packages/react/src/Editor/useEditor.ts +++ b/packages/react/src/Editor/useEditor.ts @@ -1,7 +1,7 @@ import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react"; +import { getBlockNoteExtensions } from "@blocknote/core"; import { DependencyList } from "react"; -import { getBlockNoteExtensions } from "../../../core/src/BlockNoteExtensions"; // import styles from "../../../core/src/editor.module.css"; // import rootStyles from "../../../core/src/root.module.css"; diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/EditorContent.tsx new file mode 100644 index 0000000000..7bcccbc6cd --- /dev/null +++ b/packages/react/src/EditorContent.tsx @@ -0,0 +1,2 @@ +// BlockNote uses a similar pattern as Tiptap, so for now we can just export that +export { EditorContent } from "@tiptap/react"; diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx index 62ce9cd7ee..fef1d2cb4f 100644 --- a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx +++ b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx @@ -1,13 +1,13 @@ -import { createRoot } from "react-dom/client"; -import { HyperlinkMenu } from "./components/HyperlinkMenu"; -import tippy from "tippy.js"; -import { MantineProvider } from "@mantine/core"; -import { BlockNoteTheme } from "../BlockNoteTheme"; import { HyperlinkHoverMenu, HyperlinkHoverMenuFactory, HyperlinkHoverMenuProps, -} from "../../../core/src/menu-tools/HyperlinkHoverMenu/types"; +} from "@blocknote/core"; +import { MantineProvider } from "@mantine/core"; +import { createRoot } from "react-dom/client"; +import tippy from "tippy.js"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +import { HyperlinkMenu } from "./components/HyperlinkMenu"; export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( props: HyperlinkHoverMenuProps diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000000..f54a34b5d4 --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,5 @@ +// TODO: review directories +export * from "./BubbleMenu/BubbleMenuFactory"; +export * from "./EditorContent"; +export * from "./HyperlinkMenus/HyperlinkMenuFactory"; +export * from "./shared/components/suggestion/SuggestionsMenuFactory"; diff --git a/packages/react/src/shared/components/suggestion/SuggestionList.tsx b/packages/react/src/shared/components/suggestion/SuggestionList.tsx index 7b00c48454..876cea906a 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionList.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionList.tsx @@ -1,6 +1,5 @@ -import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; +import { SuggestionItem, SuggestionsMenuProps } from "@blocknote/core"; import { createStyles, Menu } from "@mantine/core"; -import { SuggestionsMenuProps } from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; import { SuggestionListItem } from "./SuggestionListItem"; export type SuggestionListProps = diff --git a/packages/react/src/shared/components/suggestion/SuggestionListItem.tsx b/packages/react/src/shared/components/suggestion/SuggestionListItem.tsx index b89ae8c442..31a5cbf61c 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionListItem.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionListItem.tsx @@ -1,14 +1,14 @@ // import SuggestionItem from "../SuggestionItem"; -import { useEffect, useRef } from "react"; import { Badge, createStyles, Menu, Stack, Text } from "@mantine/core"; +import { useEffect, useRef } from "react"; import { IconType } from "react-icons"; const MIN_LEFT_MARGIN = 5; export type SuggestionGroupItemProps = { name: string; - hint: string; - icon: IconType; + hint: string | undefined; + icon: IconType | undefined; shortcut?: string; isSelected: boolean; set: () => void; diff --git a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx index f8eb8a41bd..d9afd13902 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx @@ -1,12 +1,12 @@ -import SuggestionItem from "@blocknote/core/types/src/shared/plugins/suggestion/SuggestionItem"; -import { MantineProvider } from "@mantine/core"; -import { createRoot } from "react-dom/client"; -import tippy from "tippy.js"; import { + SuggestionItem, SuggestionsMenu, SuggestionsMenuFactory, SuggestionsMenuProps, -} from "../../../../../core/src/menu-tools/SuggestionsMenu/types"; +} from "@blocknote/core"; +import { MantineProvider } from "@mantine/core"; +import { createRoot } from "react-dom/client"; +import tippy from "tippy.js"; import { BlockNoteTheme } from "../../../BlockNoteTheme"; import { SuggestionList } from "./SuggestionList"; // import rootStyles from "../../../core/src/root.module.css"; diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000000..a4c523ab6e --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "jsx": "react-jsx", + "strict": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "outDir": "dist", + "declaration": true, + "declarationDir": "types", + "composite": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts new file mode 100644 index 0000000000..f652122490 --- /dev/null +++ b/packages/react/vite.config.ts @@ -0,0 +1,31 @@ +import react from "@vitejs/plugin-react"; +import * as path from "path"; +import { defineConfig } from "vite"; +import pkg from "./package.json"; +// import eslintPlugin from "vite-plugin-eslint"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + build: { + sourcemap: true, + lib: { + entry: path.resolve(__dirname, "src/index.ts"), + name: "blocknote-react", + fileName: "blocknote-react", + }, + rollupOptions: { + // make sure to externalize deps that shouldn't be bundled + // into your library + external: Object.keys(pkg.dependencies), + output: { + // Provide global variables to use in the UMD build + // for externalized deps + globals: { + react: "React", + "react-dom": "ReactDOM", + }, + }, + }, + }, +}); From 1bc1a761c31bb95c8cf1e8dae2e6627b170a96e6 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 21 Dec 2022 15:27:13 +0100 Subject: [PATCH 14/55] fix react error --- examples/editor/src/main.tsx | 6 +++--- examples/editor/vite.config.ts | 3 +++ packages/react/vite.config.ts | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index e75e6b727b..bafd903d69 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,12 +1,12 @@ -import styles from "./App.module.css"; -import "./index.css"; - import { mountBlockNoteEditor } from "@blocknote/core"; +import "@blocknote/core/style.css"; import { ReactBubbleMenuFactory, ReactHyperlinkMenuFactory, ReactSuggestionsMenuFactory, } from "@blocknote/react"; +import styles from "./App.module.css"; +import "./index.css"; // type WindowWithProseMirror = Window & // typeof globalThis & { ProseMirror: Editor }; diff --git a/examples/editor/vite.config.ts b/examples/editor/vite.config.ts index 4910cd49aa..d135de28dc 100644 --- a/examples/editor/vite.config.ts +++ b/examples/editor/vite.config.ts @@ -8,6 +8,9 @@ export default defineConfig((conf) => ({ optimizeDeps: { // link: ['vite-react-ts-components'], }, + build: { + sourcemap: true, + }, resolve: { alias: conf.command === "build" diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index f652122490..4d66118a1e 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -17,7 +17,11 @@ export default defineConfig({ rollupOptions: { // make sure to externalize deps that shouldn't be bundled // into your library - external: Object.keys(pkg.dependencies), + external: Object.keys({ + ...pkg.dependencies, + ...pkg.peerDependencies, + ...pkg.devDependencies, + }), output: { // Provide global variables to use in the UMD build // for externalized deps From b3b22664415624677d713231b8f069b07b125609 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 21 Dec 2022 16:32:08 +0100 Subject: [PATCH 15/55] Changed bubble menu plugin view back to use a separate class --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 319 ++++++++++-------- .../src/BubbleMenu/BubbleMenuFactory.tsx | 39 ++- .../src/BubbleMenu/components/BubbleMenu.tsx | 12 +- 3 files changed, 208 insertions(+), 162 deletions(-) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 3c73a33b06..3d59749be1 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -2,7 +2,10 @@ import { Editor, isTextSelection } from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { getBubbleMenuInitProps } from "../../menu-tools/BubbleMenu/getBubbleMenuInitProps"; -import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; +import { + BubbleMenu, + BubbleMenuFactory, +} from "../../menu-tools/BubbleMenu/types"; import { getBubbleMenuUpdateProps } from "../../menu-tools/BubbleMenu/getBubbleMenuUpdateProps"; // Same as TipTap bubblemenu plugin, but with these changes: @@ -23,176 +26,214 @@ export interface BubbleMenuPluginProps { | null; } -// TODO: do from previous code -export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { - const bubbleMenu = options.bubbleMenuFactory( - getBubbleMenuInitProps(options.editor) - ); - - // TODO: Is this callback needed? - const mousedownHandler = (_view: EditorView) => { - // view.dispatch( - // view.state.tr.setMeta(options.pluginKey, { - // preventHide: true, - // }) - // ); +export type BubbleMenuViewProps = BubbleMenuPluginProps & { + view: EditorView; +}; + +export class BubbleMenuView { + public editor: Editor; + + public bubbleMenu: BubbleMenu; + + public view: EditorView; + + public preventHide = false; + + public preventShow = false; + + public menuIsOpen = false; + + public shouldShow: Exclude = ({ + view, + state, + from, + to, + }) => { + const { doc, selection } = state; + const { empty } = selection; + + // Sometime check for `empty` is not enough. + // Doubleclick an empty paragraph returns a node size of 2. + // So we check also for an empty text size. + const isEmptyTextBlock = + !doc.textBetween(from, to).length && isTextSelection(state.selection); + + if (!view.hasFocus() || empty || isEmptyTextBlock) { + return false; + } + + return true; }; - // TODO: transaction needed? - const viewMousedownHandler = (view: EditorView) => { - view.dispatch( - view.state.tr.setMeta(options.pluginKey, { - preventShow: true, - }) - ); + constructor({ + editor, + bubbleMenuFactory, + view, + shouldShow, + }: BubbleMenuViewProps) { + this.editor = editor; + this.bubbleMenu = bubbleMenuFactory(getBubbleMenuInitProps(editor)); + this.view = view; + + if (shouldShow) { + this.shouldShow = shouldShow; + } + + this.view.dom.addEventListener("mousedown", this.viewMousedownHandler); + this.view.dom.addEventListener("mouseup", this.viewMouseupHandler); + this.view.dom.addEventListener("dragstart", this.dragstartHandler); + + this.editor.on("focus", this.focusHandler); + this.editor.on("blur", this.blurHandler); + } + + mousedownHandler = () => { + this.preventHide = true; }; - const viewMouseupHandler = (view: EditorView) => { - view.dispatch( - view.state.tr.setMeta(options.pluginKey, { - preventShow: false, - }) - ); + viewMousedownHandler = () => { + this.preventShow = true; }; - const dragstartHandler = () => { - bubbleMenu.hide(); + viewMouseupHandler = () => { + this.preventShow = false; + setTimeout(() => this.update(this.editor.view)); }; - // TODO: Is this callback needed? - const focusHandler = () => { - // we use `setTimeout` to make sure `selection` is already updated - // setTimeout(() => this.update(this.editor.view)); + dragstartHandler = () => { + this.bubbleMenu.element!.removeEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, + } + ); + + this.bubbleMenu.hide(); + this.menuIsOpen = false; }; - const blurHandler = ({ event }: { event: FocusEvent }, view: EditorView) => { - const pluginState = options.pluginKey.getState(view.state); + focusHandler = () => { + // we use `setTimeout` to make sure `selection` is already updated + setTimeout(() => this.update(this.editor.view)); + }; - if (pluginState.preventHide) { - pluginState.preventHide = false; + blurHandler = ({ event }: { event: FocusEvent }) => { + if (this.preventHide) { + this.preventHide = false; return; } if ( event?.relatedTarget && - bubbleMenu.element?.parentNode?.contains(event.relatedTarget as Node) + this.bubbleMenu.element?.parentNode?.contains(event.relatedTarget as Node) ) { return; } - bubbleMenu.hide(); + this.bubbleMenu.element!.removeEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, + } + ); + + this.bubbleMenu.hide(); + this.menuIsOpen = false; }; - return new Plugin({ - key: options.pluginKey, - view: (view) => { - view.dom.addEventListener("mousedown", () => viewMousedownHandler(view)); - view.dom.addEventListener("mouseup", () => viewMouseupHandler(view)); - view.dom.addEventListener("dragstart", dragstartHandler); - - options.editor.on("focus", focusHandler); - options.editor.on("blur", ({ event }: { event: FocusEvent }) => - blurHandler({ event }, view) - ); + update(view: EditorView, oldState?: EditorState) { + const { state, composing } = view; + const { doc, selection } = state; + const isSame = + oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); - return { - update: (view, prevState) => { - const prev = options.pluginKey.getState(prevState); - const next = options.pluginKey.getState(view.state); - - if (!prev.show && next.show && !next.preventShow) { - bubbleMenu.show(getBubbleMenuUpdateProps(options.editor)); - - bubbleMenu.element!.addEventListener( - "mousedown", - () => mousedownHandler(view), - { - capture: true, - } - ); - - return; - } - - if ( - prev.show && - next.show && - !next.preventShow && - !next.preventUpdate - ) { - // TODO: Waits 350ms for animations to complete, looks clunky. See TODO in getBubbleMenuInitProps for why - // this is necessary. - setTimeout(() => { - bubbleMenu.update(getBubbleMenuUpdateProps(options.editor)); - }, 350); - - return; - } - - if (prev.show && !next.show && !next.preventHide) { - bubbleMenu.element!.removeEventListener( - "mousedown", - () => mousedownHandler(view), - { - capture: true, - } - ); - - bubbleMenu.hide(); - - return; - } - }, - }; - }, - state: { - init: () => { - return { - show: false, - preventShow: false, - preventUpdate: false, - preventHide: false, - }; - }, - apply: (tr, prev, oldState, state) => { - const next = { ...prev }; - const { doc, selection } = state; - - if (tr.getMeta(options.pluginKey)?.preventShow !== undefined) { - next.preventShow = tr.getMeta(options.pluginKey).preventShow; + if (composing || isSame) { + return; + } + + // support for CellSelections + const { ranges } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const to = Math.max(...ranges.map((range) => range.$to.pos)); + + const shouldShow = this.shouldShow?.({ + editor: this.editor, + view, + state, + oldState, + from, + to, + }); + + // Checks if menu should be hidden. + if ( + this.menuIsOpen && + !this.preventHide && + (!shouldShow || this.preventShow) + ) { + this.bubbleMenu.element!.removeEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, } + ); - next.preventUpdate = - oldState && oldState.doc.eq(doc) && oldState.selection.eq(selection); + this.bubbleMenu.hide(); + this.menuIsOpen = false; - if (tr.getMeta(options.pluginKey)?.preventHide !== undefined) { - next.preventHide = tr.getMeta(options.pluginKey).preventHide; - } + return; + } - // Support for CellSelections - const { ranges, empty } = selection; - const from = Math.min(...ranges.map((range) => range.$from.pos)); - const to = Math.max(...ranges.map((range) => range.$to.pos)); + // Checks if menu should be updated. + if ( + this.menuIsOpen && + !this.preventShow && + (shouldShow || this.preventHide) + ) { + setTimeout( + () => this.bubbleMenu.update(getBubbleMenuUpdateProps(this.editor)), + 350 + ); - // Sometime check for `empty` is not enough. - // Doubleclick an empty paragraph returns a node size of 2. - // So we check also for an empty text size. - const isEmptyTextBlock = - !doc.textBetween(from, to).length && isTextSelection(state.selection); + return; + } - if ((empty || isEmptyTextBlock) && !next.preventHide) { - next.show = false; - return next; + // Checks if menu should be shown. + if ( + !this.menuIsOpen && + !this.preventShow && + (shouldShow || this.preventHide) + ) { + this.bubbleMenu.show(getBubbleMenuUpdateProps(this.editor)); + this.menuIsOpen = true; + + this.bubbleMenu.element!.addEventListener( + "mousedown", + this.mousedownHandler, + { + capture: true, } + ); + } + } - if (!next.preventShow) { - next.show = true; - return next; - } + destroy() { + this.view.dom.removeEventListener("mousedown", this.viewMousedownHandler); + this.view.dom.removeEventListener("mouseup", this.viewMouseupHandler); + this.view.dom.removeEventListener("dragstart", this.dragstartHandler); + + this.editor.off("focus", this.focusHandler); + this.editor.off("blur", this.blurHandler); + } +} - return next; - }, - }, +export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { + return new Plugin({ + key: new PluginKey("BubbleMenuPlugin"), + view: (view) => new BubbleMenuView({ view, ...options }), }); }; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 03bb7c79f5..e84e9921f4 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -18,20 +18,6 @@ import { export const ReactBubbleMenuFactory: BubbleMenuFactory = ( initProps: BubbleMenuInitProps ): BubbleMenu => { - const element = document.createElement("div"); - // element.className = rootStyles.bnRoot; - const root = createRoot(element); - - let menu = tippy(initProps.editorElement, { - duration: 0, - getReferenceClientRect: initProps.getSelectionBoundingBox, - content: element, - interactive: true, - trigger: "manual", - placement: "top", - hideOnClick: "toggle", - }); - const bubbleMenuProps: BubbleMenuProps = { boldIsActive: false, toggleBold: initProps.toggleBold, @@ -75,6 +61,21 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( bubbleMenuProps.activeListItemType = updateProps.activeListItemType; } + const element = document.createElement("div"); + // element.className = rootStyles.bnRoot; + + const root = createRoot(element); + + let menu = tippy(initProps.editorElement, { + duration: 0, + getReferenceClientRect: initProps.getSelectionBoundingBox, + content: element, + interactive: true, + trigger: "manual", + placement: "top", + hideOnClick: "toggle", + }); + return { element: element as HTMLElement, show: (updateProps: BubbleMenuUpdateProps) => { @@ -86,6 +87,11 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( ); + // Ensures that the component is rendered so that Tippy can display it in the correct position. + setTimeout(() => { + menu.popperInstance.forceUpdate(); + }); + menu.show(); }, hide: menu.hide, @@ -98,7 +104,10 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( ); - menu.popperInstance?.forceUpdate(); + // Ensures that the component is rendered so that Tippy can display it in the correct position. + setTimeout(() => { + menu.popperInstance.forceUpdate(); + }); }, }; }; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 8b61c950b0..2a1fccf799 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -58,13 +58,6 @@ export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { }; const getActiveBlock = () => { - if (props.bubbleMenuProps.paragraphIsActive) { - return { - text: "Text", - icon: RiText, - }; - } - if (props.bubbleMenuProps.headingIsActive) { if (props.bubbleMenuProps.activeHeadingLevel === "1") { return { @@ -102,7 +95,10 @@ export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { } } - return undefined; + return { + text: "Text", + icon: RiText, + }; }; const activeMarks = getActiveMarks(); From f6f7836e3313f38293617de6145be5c9c7a20a71 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 21 Dec 2022 17:32:43 +0100 Subject: [PATCH 16/55] Split hyperlink menu props into init & update props --- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 103 ++++++++---------- .../core/src/menu-tools/BubbleMenu/types.ts | 13 +-- .../getHyperlinkHoverMenuInitProps.ts | 13 +++ .../getHyperlinkMenuUpdateProps.ts | 13 +++ .../menu-tools/HyperlinkHoverMenu/types.ts | 19 +++- packages/core/src/menu-tools/types.ts | 10 +- .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 53 +++++---- .../components/HyperlinkMenu.tsx | 2 +- 8 files changed, 128 insertions(+), 98 deletions(-) create mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts create mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index 2394e75b61..ca51c22a3d 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -1,8 +1,9 @@ import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; -import { getHyperlinkHoverMenuProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps"; import { HyperlinkHoverMenuFactory } from "../../menu-tools/HyperlinkHoverMenu/types"; +import { getHyperlinkHoverMenuInitProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps"; +import { getHyperlinkHoverMenuUpdateProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); export type HyperlinkMenuPluginProps = { @@ -14,6 +15,40 @@ export const createHyperlinkMenuPlugin = ( editor: Editor, options: HyperlinkMenuPluginProps ) => { + const editHyperlink = (url: string, text: string) => { + const tr = editor.view.state.tr.insertText( + text, + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.to + ); + tr.addMark( + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.from + text.length, + editor.schema.mark("link", { href: url }) + ); + editor.view.dispatch(tr); + }; + + const deleteHyperlink = () => { + editor.view.dispatch( + editor.view.state.tr + .removeMark( + hyperlinkMarkRange!.from, + hyperlinkMarkRange!.to, + hyperlinkMark!.type + ) + .setMeta("preventAutolink", true) + ); + }; + + let hyperlinkMenu = options.hyperlinkMenuFactory( + getHyperlinkHoverMenuInitProps( + editHyperlink, + deleteHyperlink, + editor.options.element + ) + ); + let menuHideTimer: NodeJS.Timeout | undefined; const startMenuHideTimer = () => { menuHideTimer = setTimeout(() => { @@ -42,21 +77,6 @@ export const createHyperlinkMenuPlugin = ( let hyperlinkMark: Mark | undefined; let hyperlinkMarkRange: Range | undefined; - // initialize the hyperlinkMenu UI element - // the actual values are dummy values, as the menu isn't shown / positioned yet - // (TBD: we could also decide not to pass these values upon creation, - // or only initialize a menu upon first-use) - let hyperlinkMenu = options.hyperlinkMenuFactory( - getHyperlinkHoverMenuProps( - "", - "", - () => {}, - () => {}, - new DOMRect(), - editor.options.element - ) - ); - return new Plugin({ key: PLUGIN_KEY, view() { @@ -117,52 +137,24 @@ export const createHyperlinkMenuPlugin = ( if (hyperlinkMark) { // Gets all variables/functions needed to render menu. - const url = hyperlinkMark.attrs.href; - const text = editor.view.state.doc.textBetween( + const hyperlinkUrl = hyperlinkMark.attrs.href; + const hyperlinkText = editor.view.state.doc.textBetween( hyperlinkMarkRange!.from, hyperlinkMarkRange!.to ); - const editHyperlink = (url: string, text: string) => { - const tr = editor.view.state.tr.insertText( - text, - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.to - ); - tr.addMark( - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.from + text.length, - editor.schema.mark("link", { href: url }) - ); - editor.view.dispatch(tr); - }; - const deleteHyperlink = () => { - editor.view.dispatch( - editor.view.state.tr - .removeMark( - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.to, - hyperlinkMark!.type - ) - .setMeta("preventAutolink", true) - ); - }; const hyperlinkBoundingBox = posToDOMRect( editor.view, hyperlinkMarkRange!.from, hyperlinkMarkRange!.to ); - const editorElement = editor.view.dom; // Shows menu. if (!prevHyperlinkMark) { hyperlinkMenu.show( - getHyperlinkHoverMenuProps( - url, - text, - editHyperlink, - deleteHyperlink, - hyperlinkBoundingBox, - editorElement + getHyperlinkHoverMenuUpdateProps( + hyperlinkUrl, + hyperlinkText, + hyperlinkBoundingBox ) ); @@ -180,13 +172,10 @@ export const createHyperlinkMenuPlugin = ( // Updates menu. hyperlinkMenu.update( - getHyperlinkHoverMenuProps( - url, - text, - editHyperlink, - deleteHyperlink, - hyperlinkBoundingBox, - editorElement + getHyperlinkHoverMenuUpdateProps( + hyperlinkUrl, + hyperlinkText, + hyperlinkBoundingBox ) ); } diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 772846bad0..9e6991903e 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -1,4 +1,4 @@ -// import { Menu, MenuFactory } from "../types"; +import { Menu, MenuFactory } from "../types"; // TODO: reconsider .set() function export type BasicMarkProps = { @@ -80,17 +80,6 @@ export type BubbleMenuUpdateProps = { activeListItemType: string; }; -type Menu = { - element: HTMLElement | undefined; - show: (props: MenuUpdateProps) => void; - hide: () => void; - update: (newProps: MenuUpdateProps) => void; -}; - -type MenuFactory = ( - initProps: MenuInitProps -) => Menu; - export type BubbleMenu = Menu; export type BubbleMenuFactory = MenuFactory< BubbleMenuInitProps, diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts new file mode 100644 index 0000000000..378888af45 --- /dev/null +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts @@ -0,0 +1,13 @@ +import { HyperlinkHoverMenuInitProps } from "./types"; + +export function getHyperlinkHoverMenuInitProps( + editHyperlink: (url: string, text: string) => void, + deleteHyperlink: () => void, + editorElement: Element +): HyperlinkHoverMenuInitProps { + return { + editHyperlink: editHyperlink, + deleteHyperlink: deleteHyperlink, + editorElement: editorElement, + }; +} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts new file mode 100644 index 0000000000..62eb4dc49b --- /dev/null +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts @@ -0,0 +1,13 @@ +import { HyperlinkHoverMenuUpdateProps } from "./types"; + +export function getHyperlinkHoverMenuUpdateProps( + hyperlinkUrl: string, + hyperlinkText: string, + hyperlinkBoundingBox: DOMRect +): HyperlinkHoverMenuUpdateProps { + return { + hyperlinkUrl: hyperlinkUrl, + hyperlinkText: hyperlinkText, + hyperlinkBoundingBox: hyperlinkBoundingBox, + }; +} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts index 6639bdb7ea..7bb8623acd 100644 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts @@ -13,5 +13,20 @@ export type HyperlinkHoverMenuProps = { }; }; -export type HyperlinkHoverMenu = Menu; -export type HyperlinkHoverMenuFactory = MenuFactory; +export type HyperlinkHoverMenuInitProps = { + editHyperlink: (url: string, text: string) => void; + deleteHyperlink: () => void; + editorElement: Element; +}; + +export type HyperlinkHoverMenuUpdateProps = { + hyperlinkUrl: string; + hyperlinkText: string; + hyperlinkBoundingBox: DOMRect; +}; + +export type HyperlinkHoverMenu = Menu; +export type HyperlinkHoverMenuFactory = MenuFactory< + HyperlinkHoverMenuInitProps, + HyperlinkHoverMenuUpdateProps +>; diff --git a/packages/core/src/menu-tools/types.ts b/packages/core/src/menu-tools/types.ts index 3b610b46e1..90519aa80e 100644 --- a/packages/core/src/menu-tools/types.ts +++ b/packages/core/src/menu-tools/types.ts @@ -1,8 +1,10 @@ -export type Menu = { +export type Menu = { element: HTMLElement | undefined; - show: (props: MenuProps) => void; + show: (props: MenuUpdateProps) => void; hide: () => void; - update: (newProps: MenuProps) => void; + update: (newProps: MenuUpdateProps) => void; }; -export type MenuFactory = (props: MenuProps) => Menu; +export type MenuFactory = ( + initProps: MenuInitProps +) => Menu; diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx index 62ce9cd7ee..d5af59795e 100644 --- a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx +++ b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx @@ -1,24 +1,39 @@ import { createRoot } from "react-dom/client"; -import { HyperlinkMenu } from "./components/HyperlinkMenu"; +import { HyperlinkMenu, HyperlinkMenuProps } from "./components/HyperlinkMenu"; import tippy from "tippy.js"; import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../BlockNoteTheme"; import { HyperlinkHoverMenu, HyperlinkHoverMenuFactory, - HyperlinkHoverMenuProps, + HyperlinkHoverMenuInitProps, + HyperlinkHoverMenuUpdateProps, } from "../../../core/src/menu-tools/HyperlinkHoverMenu/types"; export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( - props: HyperlinkHoverMenuProps + initProps: HyperlinkHoverMenuInitProps ): HyperlinkHoverMenu => { + const hyperlinkMenuProps: HyperlinkMenuProps = { + url: "", + text: "", + update: initProps.editHyperlink, + remove: initProps.deleteHyperlink, + }; + + function updateHyperlinkMenuProps( + updateProps: HyperlinkHoverMenuUpdateProps + ) { + hyperlinkMenuProps.url = updateProps.hyperlinkUrl; + hyperlinkMenuProps.text = updateProps.hyperlinkText; + } + const element = document.createElement("div"); const root = createRoot(element); - const menu = tippy(props.view.editorElement, { - appendTo: props.view.editorElement, + const menu = tippy(initProps.editorElement, { + appendTo: initProps.editorElement, duration: 0, - getReferenceClientRect: () => props.view.hyperlinkBoundingBox, + getReferenceClientRect: () => new DOMRect(), content: element, interactive: true, trigger: "manual", @@ -30,20 +45,17 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( return { element: element, - show: (props: HyperlinkHoverMenuProps) => { + show: (updateProps: HyperlinkHoverMenuUpdateProps) => { + updateHyperlinkMenuProps(updateProps); + root.render( - + ); menu.setProps({ - getReferenceClientRect: () => props.view.hyperlinkBoundingBox, + getReferenceClientRect: () => updateProps.hyperlinkBoundingBox, }); menu.show(); @@ -51,20 +63,17 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( hide: () => { menu.hide(); }, - update: (newProps: HyperlinkHoverMenuProps) => { + update: (updateProps: HyperlinkHoverMenuUpdateProps) => { + updateHyperlinkMenuProps(updateProps); + root.render( - + ); menu.setProps({ - getReferenceClientRect: () => newProps.view.hyperlinkBoundingBox, + getReferenceClientRect: () => updateProps.hyperlinkBoundingBox, }); }, }; diff --git a/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx b/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx index cdf5534e57..65f65f77f1 100644 --- a/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx @@ -3,7 +3,7 @@ import { EditHyperlinkMenu } from "../EditHyperlinkMenu/components/EditHyperlink import { HoverHyperlinkMenu } from "../HoverHyperlinkMenu/components/HoverHyperlinkMenu"; // import rootStyles from "../../../root.module.css"; -type HyperlinkMenuProps = { +export type HyperlinkMenuProps = { url: string; text: string; update: (url: string, text: string) => void; From 0294da0ba59d9e66ed8343e47ebd7def741430af Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 21 Dec 2022 18:39:50 +0100 Subject: [PATCH 17/55] Changed hyperlink menu plugin view to use a separate class --- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 457 +++++++++--------- 1 file changed, 223 insertions(+), 234 deletions(-) diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index ca51c22a3d..38b0e20494 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -1,7 +1,10 @@ import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; -import { HyperlinkHoverMenuFactory } from "../../menu-tools/HyperlinkHoverMenu/types"; +import { + HyperlinkHoverMenu, + HyperlinkHoverMenuFactory, +} from "../../menu-tools/HyperlinkHoverMenu/types"; import { getHyperlinkHoverMenuInitProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps"; import { getHyperlinkHoverMenuUpdateProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); @@ -10,256 +13,242 @@ export type HyperlinkMenuPluginProps = { hyperlinkMenuFactory: HyperlinkHoverMenuFactory; }; -// Rewrite to class? -export const createHyperlinkMenuPlugin = ( - editor: Editor, - options: HyperlinkMenuPluginProps -) => { - const editHyperlink = (url: string, text: string) => { - const tr = editor.view.state.tr.insertText( - text, - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.to - ); - tr.addMark( - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.from + text.length, - editor.schema.mark("link", { href: url }) - ); - editor.view.dispatch(tr); - }; - - const deleteHyperlink = () => { - editor.view.dispatch( - editor.view.state.tr - .removeMark( - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.to, - hyperlinkMark!.type - ) - .setMeta("preventAutolink", true) - ); - }; - - let hyperlinkMenu = options.hyperlinkMenuFactory( - getHyperlinkHoverMenuInitProps( - editHyperlink, - deleteHyperlink, - editor.options.element - ) - ); - - let menuHideTimer: NodeJS.Timeout | undefined; - const startMenuHideTimer = () => { - menuHideTimer = setTimeout(() => { - editor.view.dispatch( - editor.view.state.tr.setMeta(PLUGIN_KEY, { - hoveredLinkChanged: true, - }) - ); - }, 250); - }; - const stopMenuHideTimer = () => { - if (menuHideTimer) { - clearTimeout(menuHideTimer); - menuHideTimer = undefined; - } - - return false; - }; - - let mouseHoveredHyperlinkMark: Mark | undefined; - let mouseHoveredHyperlinkMarkRange: Range | undefined; +export type HyperlinkHoverMenuViewProps = { + editor: Editor; + hyperlinkHoverMenuFactory: HyperlinkHoverMenuFactory; +}; - let keyboardHoveredHyperlinkMark: Mark | undefined; - let keyboardHoveredHyperlinkMarkRange: Range | undefined; +class HyperlinkHoverMenuView { + editor: Editor; - let hyperlinkMark: Mark | undefined; - let hyperlinkMarkRange: Range | undefined; + hyperlinkHoverMenu: HyperlinkHoverMenu; - return new Plugin({ - key: PLUGIN_KEY, - view() { - return { - update: async (_view, _prevState) => { - if (editor.state.selection.empty) { - const marksAtPos = editor.state.selection.$from.marks(); - - keyboardHoveredHyperlinkMark = undefined; - keyboardHoveredHyperlinkMarkRange = undefined; - - for (const mark of marksAtPos) { - if (mark.type.name === editor.schema.mark("link").type.name) { - keyboardHoveredHyperlinkMark = mark; - keyboardHoveredHyperlinkMarkRange = - getMarkRange( - editor.state.selection.$from, - mark.type, - mark.attrs - ) || undefined; - - break; - } - } - } + menuUpdateTimer: NodeJS.Timeout | undefined; + startMenuUpdateTimer: () => void; + stopMenuUpdateTimer: () => void; - const prevHyperlinkMark = hyperlinkMark; + mouseHoveredHyperlinkMark: Mark | undefined; + mouseHoveredHyperlinkMarkRange: Range | undefined; - hyperlinkMark = undefined; - hyperlinkMarkRange = undefined; + keyboardHoveredHyperlinkMark: Mark | undefined; + keyboardHoveredHyperlinkMarkRange: Range | undefined; - if (mouseHoveredHyperlinkMark) { - hyperlinkMark = mouseHoveredHyperlinkMark; - hyperlinkMarkRange = mouseHoveredHyperlinkMarkRange; - } + hyperlinkMark: Mark | undefined; + hyperlinkMarkRange: Range | undefined; - // Keyboard cursor position takes precedence over mouse hovered hyperlink. - if (keyboardHoveredHyperlinkMark) { - hyperlinkMark = keyboardHoveredHyperlinkMark; - hyperlinkMarkRange = keyboardHoveredHyperlinkMarkRange; - } + constructor({ + editor, + hyperlinkHoverMenuFactory, + }: HyperlinkHoverMenuViewProps) { + this.editor = editor; - // Hides menu. - if (prevHyperlinkMark && !hyperlinkMark) { - hyperlinkMenu.element?.removeEventListener( - "mouseleave", - startMenuHideTimer - ); - hyperlinkMenu.element?.removeEventListener( - "mouseenter", - stopMenuHideTimer - ); + const editHyperlink = (url: string, text: string) => { + const tr = editor.view.state.tr.insertText( + text, + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to + ); + tr.addMark( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.from + text.length, + editor.schema.mark("link", { href: url }) + ); + editor.view.dispatch(tr); + }; - hyperlinkMenu.hide(); + const deleteHyperlink = () => { + editor.view.dispatch( + editor.view.state.tr + .removeMark( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to, + this.hyperlinkMark!.type + ) + .setMeta("preventAutolink", true) + ); + }; + + this.hyperlinkHoverMenu = hyperlinkHoverMenuFactory( + getHyperlinkHoverMenuInitProps( + editHyperlink, + deleteHyperlink, + editor.options.element + ) + ); - return; + this.startMenuUpdateTimer = () => { + this.menuUpdateTimer = setTimeout(() => { + this.update(); + }, 250); + }; + + this.stopMenuUpdateTimer = () => { + if (this.menuUpdateTimer) { + clearTimeout(this.menuUpdateTimer); + this.menuUpdateTimer = undefined; + } + + return false; + }; + + editor.view.dom.addEventListener("mouseover", (event) => { + // Resets the hyperlink mark currently hovered by the mouse cursor. + this.mouseHoveredHyperlinkMark = undefined; + this.mouseHoveredHyperlinkMarkRange = undefined; + + this.stopMenuUpdateTimer(); + + if ( + event.target instanceof HTMLAnchorElement && + event.target.nodeName === "A" + ) { + // Finds link mark at the hovered element's position to update mouseHoveredHyperlinkMark and + // mouseHoveredHyperlinkMarkRange. + const hoveredHyperlinkElement = event.target; + const posInHoveredHyperlinkMark = + editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1; + const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve( + posInHoveredHyperlinkMark + ); + const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks(); + + for (const mark of marksAtPos) { + if (mark.type.name === editor.schema.mark("link").type.name) { + this.mouseHoveredHyperlinkMark = mark; + this.mouseHoveredHyperlinkMarkRange = + getMarkRange( + resolvedPosInHoveredHyperlinkMark, + mark.type, + mark.attrs + ) || undefined; + + break; } + } + } + + this.startMenuUpdateTimer(); + + return false; + }); + } + + update() { + // Saves the currently hovered hyperlink mark before it's updated. + const prevHyperlinkMark = this.hyperlinkMark; + + // Resets the currently hovered hyperlink mark. + this.hyperlinkMark = undefined; + this.hyperlinkMarkRange = undefined; + + // Resets the hyperlink mark currently hovered by the keyboard cursor. + this.keyboardHoveredHyperlinkMark = undefined; + this.keyboardHoveredHyperlinkMarkRange = undefined; + + // Finds link mark at the editor selection's position to update keyboardHoveredHyperlinkMark and + // keyboardHoveredHyperlinkMarkRange. + if (this.editor.state.selection.empty) { + const marksAtPos = this.editor.state.selection.$from.marks(); + + for (const mark of marksAtPos) { + if (mark.type.name === this.editor.schema.mark("link").type.name) { + this.keyboardHoveredHyperlinkMark = mark; + this.keyboardHoveredHyperlinkMarkRange = + getMarkRange( + this.editor.state.selection.$from, + mark.type, + mark.attrs + ) || undefined; + + break; + } + } + } - if (hyperlinkMark) { - // Gets all variables/functions needed to render menu. - const hyperlinkUrl = hyperlinkMark.attrs.href; - const hyperlinkText = editor.view.state.doc.textBetween( - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.to - ); - const hyperlinkBoundingBox = posToDOMRect( - editor.view, - hyperlinkMarkRange!.from, - hyperlinkMarkRange!.to - ); - - // Shows menu. - if (!prevHyperlinkMark) { - hyperlinkMenu.show( - getHyperlinkHoverMenuUpdateProps( - hyperlinkUrl, - hyperlinkText, - hyperlinkBoundingBox - ) - ); - - hyperlinkMenu.element?.addEventListener( - "mouseleave", - startMenuHideTimer - ); - hyperlinkMenu.element?.addEventListener( - "mouseenter", - stopMenuHideTimer - ); - - return; - } - - // Updates menu. - hyperlinkMenu.update( - getHyperlinkHoverMenuUpdateProps( - hyperlinkUrl, - hyperlinkText, - hyperlinkBoundingBox - ) - ); - } - }, - }; - }, - - props: { - handleDOMEvents: { - // Updates view when an anchor () element is hovered. - mouseover: (view, event: Event) => { - console.log(event.target); - - // Checks if target element is an anchor () element. - if ( - !(event.target instanceof HTMLAnchorElement) || - event.target.nodeName !== "A" - ) { - mouseHoveredHyperlinkMark = undefined; - mouseHoveredHyperlinkMarkRange = undefined; - - return false; - } + if (this.mouseHoveredHyperlinkMark) { + this.hyperlinkMark = this.mouseHoveredHyperlinkMark; + this.hyperlinkMarkRange = this.mouseHoveredHyperlinkMarkRange; + } - stopMenuHideTimer(); - - // Finds link mark at the hovered element's position to update mouseHoveredHyperlinkMark and - // mouseHoveredHyperlinkMarkRange. - const hoveredHyperlinkElement = event.target; - const posInHoveredHyperlinkMark = - editor.view.posAtDOM(hoveredHyperlinkElement, 0) + 1; - const resolvedPosInHoveredHyperlinkMark = editor.state.doc.resolve( - posInHoveredHyperlinkMark - ); - const marksAtPos = resolvedPosInHoveredHyperlinkMark.marks(); - - let foundHyperlinkMark = false; - - for (const mark of marksAtPos) { - if (mark.type.name === editor.schema.mark("link").type.name) { - mouseHoveredHyperlinkMark = mark; - mouseHoveredHyperlinkMarkRange = - getMarkRange( - resolvedPosInHoveredHyperlinkMark, - mark.type, - mark.attrs - ) || undefined; - foundHyperlinkMark = true; - - break; - } - } + // Keyboard cursor position takes precedence over mouse hovered hyperlink. + if (this.keyboardHoveredHyperlinkMark) { + this.hyperlinkMark = this.keyboardHoveredHyperlinkMark; + this.hyperlinkMarkRange = this.keyboardHoveredHyperlinkMarkRange; + } - // Resets mouseHoveredHyperlinkMark and mouseHoveredHyperlinkMarkRange if no link mark was found. - if (!foundHyperlinkMark) { - mouseHoveredHyperlinkMark = undefined; - mouseHoveredHyperlinkMarkRange = undefined; + // Hides menu. + if (prevHyperlinkMark && !this.hyperlinkMark) { + this.hyperlinkHoverMenu.element?.removeEventListener( + "mouseleave", + this.startMenuUpdateTimer + ); + this.hyperlinkHoverMenu.element?.removeEventListener( + "mouseenter", + this.stopMenuUpdateTimer + ); - return false; - } + this.hyperlinkHoverMenu.hide(); - // Dispatches transaction to update the view. - view.dispatch( - view.state.tr.setMeta(PLUGIN_KEY, { - hoveredLinkChanged: true, - }) - ); - - return false; - }, - // Updates view half a second after the cursor leaves an anchor () element. This update is cancelled if - mouseout: (_view, event: Event) => { - if ( - !(event.target instanceof HTMLAnchorElement) || - event.target.nodeName !== "A" - ) { - return false; - } + return; + } - startMenuHideTimer(); + if (this.hyperlinkMark) { + // Gets all variables/functions needed to render menu. + const hyperlinkUrl = this.hyperlinkMark.attrs.href; + const hyperlinkText = this.editor.view.state.doc.textBetween( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to + ); + const hyperlinkBoundingBox = posToDOMRect( + this.editor.view, + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to + ); + + // Shows menu. + if (!prevHyperlinkMark) { + this.hyperlinkHoverMenu.show( + getHyperlinkHoverMenuUpdateProps( + hyperlinkUrl, + hyperlinkText, + hyperlinkBoundingBox + ) + ); + + this.hyperlinkHoverMenu.element?.addEventListener( + "mouseleave", + this.startMenuUpdateTimer + ); + this.hyperlinkHoverMenu.element?.addEventListener( + "mouseenter", + this.stopMenuUpdateTimer + ); + + return; + } + + // Updates menu. + this.hyperlinkHoverMenu.update( + getHyperlinkHoverMenuUpdateProps( + hyperlinkUrl, + hyperlinkText, + hyperlinkBoundingBox + ) + ); + } + } +} - return false; - }, - }, - }, +export const createHyperlinkMenuPlugin = ( + editor: Editor, + options: HyperlinkMenuPluginProps +) => { + return new Plugin({ + key: PLUGIN_KEY, + view: () => + new HyperlinkHoverMenuView({ + editor: editor, + hyperlinkHoverMenuFactory: options.hyperlinkMenuFactory, + }), }); }; From e827f49b4050e29a842dcdb8833c50a8b99f6c67 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 21 Dec 2022 19:33:11 +0100 Subject: [PATCH 18/55] fix gitignore --- .gitignore | 3 ++- packages/react/src/types/styles.d.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 packages/react/src/types/styles.d.ts diff --git a/.gitignore b/.gitignore index 61030b7716..ef4e11d35d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,8 @@ # dependencies node_modules dist -types +packages/*/types +examples/*/types /.pnp .pnp.js diff --git a/packages/react/src/types/styles.d.ts b/packages/react/src/types/styles.d.ts new file mode 100644 index 0000000000..f57bdaee63 --- /dev/null +++ b/packages/react/src/types/styles.d.ts @@ -0,0 +1 @@ +declare module "*.module.css"; \ No newline at end of file From b51092df6a8a57db86aaabfb6c81230900877d1f Mon Sep 17 00:00:00 2001 From: yousefed Date: Thu, 22 Dec 2022 13:12:33 +0100 Subject: [PATCH 19/55] fix tsconfig --- packages/react/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index a4c523ab6e..8841d64b1c 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -10,7 +10,7 @@ "sourceMap": true, "resolveJsonModule": true, "esModuleInterop": true, - "noEmit": true, + "noEmit": false, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, From fea3a0f947a297b0204a66c7dcea6afade8cac93 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 22 Dec 2022 15:24:41 +0100 Subject: [PATCH 20/55] Major cleanup for bubble menu factory code --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 209 +++++++++++++++--- .../BubbleMenu/getBubbleMenuInitProps.ts | 93 -------- .../BubbleMenu/getBubbleMenuProps.ts | 133 ----------- .../BubbleMenu/getBubbleMenuUpdateProps.ts | 29 --- .../core/src/menu-tools/BubbleMenu/types.ts | 86 ++----- packages/core/src/menu-tools/types.ts | 10 +- .../src/BubbleMenu/BubbleMenuFactory.tsx | 100 +++++---- 7 files changed, 247 insertions(+), 413 deletions(-) delete mode 100644 packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts delete mode 100644 packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts delete mode 100644 packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 3d59749be1..4434497f45 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -1,12 +1,16 @@ -import { Editor, isTextSelection } from "@tiptap/core"; +import { + Editor, + isNodeSelection, + isTextSelection, + posToDOMRect, +} from "@tiptap/core"; import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { getBubbleMenuInitProps } from "../../menu-tools/BubbleMenu/getBubbleMenuInitProps"; import { BubbleMenu, BubbleMenuFactory, + BubbleMenuParams, } from "../../menu-tools/BubbleMenu/types"; -import { getBubbleMenuUpdateProps } from "../../menu-tools/BubbleMenu/getBubbleMenuUpdateProps"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files @@ -33,10 +37,12 @@ export type BubbleMenuViewProps = BubbleMenuPluginProps & { export class BubbleMenuView { public editor: Editor; - public bubbleMenu: BubbleMenu; - public view: EditorView; + public bubbleMenuParams: BubbleMenuParams; + + public bubbleMenu: BubbleMenu; + public preventHide = false; public preventShow = false; @@ -58,11 +64,7 @@ export class BubbleMenuView { const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection); - if (!view.hasFocus() || empty || isEmptyTextBlock) { - return false; - } - - return true; + return !(!view.hasFocus() || empty || isEmptyTextBlock); }; constructor({ @@ -72,9 +74,11 @@ export class BubbleMenuView { shouldShow, }: BubbleMenuViewProps) { this.editor = editor; - this.bubbleMenu = bubbleMenuFactory(getBubbleMenuInitProps(editor)); this.view = view; + this.bubbleMenuParams = this.initBubbleMenuParams(); + this.bubbleMenu = bubbleMenuFactory(this.bubbleMenuParams); + if (shouldShow) { this.shouldShow = shouldShow; } @@ -168,24 +172,23 @@ export class BubbleMenuView { to, }); - // Checks if menu should be hidden. + // Checks if menu should be shown. if ( - this.menuIsOpen && - !this.preventHide && - (!shouldShow || this.preventShow) + !this.menuIsOpen && + !this.preventShow && + (shouldShow || this.preventHide) ) { - this.bubbleMenu.element!.removeEventListener( + this.updateBubbleMenuParams(); + this.bubbleMenu.show(this.bubbleMenuParams); + this.menuIsOpen = true; + + this.bubbleMenu.element!.addEventListener( "mousedown", this.mousedownHandler, { capture: true, } ); - - this.bubbleMenu.hide(); - this.menuIsOpen = false; - - return; } // Checks if menu should be updated. @@ -194,30 +197,32 @@ export class BubbleMenuView { !this.preventShow && (shouldShow || this.preventHide) ) { - setTimeout( - () => this.bubbleMenu.update(getBubbleMenuUpdateProps(this.editor)), - 350 - ); + setTimeout(() => { + this.updateBubbleMenuParams(); + this.bubbleMenu.update(this.bubbleMenuParams); + }, 400); return; } - // Checks if menu should be shown. + // Checks if menu should be hidden. if ( - !this.menuIsOpen && - !this.preventShow && - (shouldShow || this.preventHide) + this.menuIsOpen && + !this.preventHide && + (!shouldShow || this.preventShow) ) { - this.bubbleMenu.show(getBubbleMenuUpdateProps(this.editor)); - this.menuIsOpen = true; - - this.bubbleMenu.element!.addEventListener( + this.bubbleMenu.element!.removeEventListener( "mousedown", this.mousedownHandler, { capture: true, } ); + + this.bubbleMenu.hide(); + this.menuIsOpen = false; + + return; } } @@ -229,6 +234,144 @@ export class BubbleMenuView { this.editor.off("focus", this.focusHandler); this.editor.off("blur", this.blurHandler); } + + getSelectionBoundingBox() { + const { state } = this.editor.view; + const { selection } = state; + + // support for CellSelections + const { ranges } = selection; + const from = Math.min(...ranges.map((range) => range.$from.pos)); + const to = Math.max(...ranges.map((range) => range.$to.pos)); + + if (isNodeSelection(selection)) { + const node = this.editor.view.nodeDOM(from) as HTMLElement; + + if (node) { + return node.getBoundingClientRect(); + } + } + + return posToDOMRect(this.editor.view, from, to); + } + + initBubbleMenuParams() { + return { + boldIsActive: false, + toggleBold: () => { + this.editor.view.focus(); + this.editor.commands.toggleBold(); + }, + italicIsActive: this.editor.isActive("italic"), + toggleItalic: () => { + this.editor.view.focus(); + this.editor.commands.toggleItalic(); + }, + underlineIsActive: this.editor.isActive("underline"), + toggleUnderline: () => { + this.editor.view.focus(); + this.editor.commands.toggleUnderline(); + }, + strikeIsActive: this.editor.isActive("strike"), + toggleStrike: () => { + this.editor.view.focus(); + this.editor.commands.toggleStrike(); + }, + hyperlinkIsActive: this.editor.isActive("link"), + activeHyperlinkUrl: this.editor.getAttributes("link").href, + activeHyperlinkText: this.editor.state.doc.textBetween( + this.editor.state.selection.from, + this.editor.state.selection.to + ), + setHyperlink: (url: string, text?: string) => { + if (url === "") { + return; + } + + let { from, to } = this.editor.state.selection; + + if (!text) { + text = this.editor.state.doc.textBetween(from, to); + } + + const mark = this.editor.schema.mark("link", { href: url }); + + this.editor.view.dispatch( + this.editor.view.state.tr + .insertText(text, from, to) + .addMark(from, from + text.length, mark) + ); + }, + paragraphIsActive: + this.editor.state.selection.$from.node().type.name === "textContent", + setParagraph: () => { + this.editor.view.focus(); + this.editor.commands.BNSetContentType( + this.editor.state.selection.from, + "textContent" + ); + }, + headingIsActive: + this.editor.state.selection.$from.node().type.name === "headingContent", + activeHeadingLevel: + this.editor.state.selection.$from.node().attrs["headingLevel"], + setHeading: (level: string = "1") => { + this.editor.view.focus(); + this.editor.commands.BNSetContentType( + this.editor.state.selection.from, + "headingContent", + { + headingLevel: level, + } + ); + }, + listItemIsActive: + this.editor.state.selection.$from.node().type.name === + "listItemContent", + activeListItemType: + this.editor.state.selection.$from.node().attrs["listItemType"], + setListItem: (type: string = "unordered") => { + this.editor.view.focus(); + this.editor.commands.BNSetContentType( + this.editor.state.selection.from, + "listItemContent", + { + listItemType: type, + } + ); + }, + selectionBoundingBox: this.getSelectionBoundingBox(), + editorElement: this.editor.options.element, + }; + } + + updateBubbleMenuParams() { + this.bubbleMenuParams.boldIsActive = this.editor.isActive("bold"); + this.bubbleMenuParams.italicIsActive = this.editor.isActive("italic"); + this.bubbleMenuParams.underlineIsActive = this.editor.isActive("underline"); + this.bubbleMenuParams.strikeIsActive = this.editor.isActive("strike"); + this.bubbleMenuParams.hyperlinkIsActive = this.editor.isActive("link"); + this.bubbleMenuParams.activeHyperlinkUrl = + this.editor.getAttributes("link").href; + this.bubbleMenuParams.activeHyperlinkText = + this.editor.state.doc.textBetween( + this.editor.state.selection.from, + this.editor.state.selection.to + ); + + this.bubbleMenuParams.paragraphIsActive = + this.editor.state.selection.$from.node().type.name === "textContent"; + this.bubbleMenuParams.headingIsActive = + this.editor.state.selection.$from.node().type.name === "headingContent"; + this.bubbleMenuParams.activeHeadingLevel = + this.editor.state.selection.$from.node().attrs["headingLevel"]; + this.bubbleMenuParams.listItemIsActive = + this.editor.state.selection.$from.node().type.name === "listItemContent"; + this.bubbleMenuParams.activeListItemType = + this.editor.state.selection.$from.node().attrs["listItemType"]; + + this.bubbleMenuParams.selectionBoundingBox = this.getSelectionBoundingBox(); + } } export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts deleted file mode 100644 index f6b0a9c6c4..0000000000 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuInitProps.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; -import { BubbleMenuInitProps } from "./types"; - -export function getBubbleMenuInitProps(editor: Editor): BubbleMenuInitProps { - return { - toggleBold: () => { - // Setting editor focus using a chained command instead causes bubble menu to flicker on click. - editor.view.focus(); - editor.commands.toggleBold(); - }, - toggleItalic: () => { - editor.view.focus(); - editor.commands.toggleItalic(); - }, - toggleUnderline: () => { - editor.view.focus(); - editor.commands.toggleUnderline(); - }, - toggleStrike: () => { - editor.view.focus(); - editor.commands.toggleStrike(); - }, - setHyperlink: (url: string, text?: string) => { - if (url === "") { - return; - } - - let { from, to } = editor.state.selection; - - if (!text) { - text = editor.state.doc.textBetween(from, to); - } - - const mark = editor.schema.mark("link", { href: url }); - - editor.view.dispatch( - editor.view.state.tr - .insertText(text, from, to) - .addMark(from, from + text.length, mark) - ); - }, - setParagraph: () => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "textContent" - ); - }, - setHeading: (level: string = "1") => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "headingContent", - { - headingLevel: level, - } - ); - }, - setListItem: (type: string = "unordered") => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "listItemContent", - { - listItemType: type, - } - ); - }, - // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are - // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection - // bounding box used to create it is from an editor view that is out of date due to the animation. - getSelectionBoundingBox: () => { - const { state } = editor.view; - const { selection } = state; - - // support for CellSelections - const { ranges } = selection; - const from = Math.min(...ranges.map((range) => range.$from.pos)); - const to = Math.max(...ranges.map((range) => range.$to.pos)); - - if (isNodeSelection(selection)) { - const node = editor.view.nodeDOM(from) as HTMLElement; - - if (node) { - return node.getBoundingClientRect(); - } - } - - return posToDOMRect(editor.view, from, to); - }, - editorElement: editor.options.element, - }; -} diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts deleted file mode 100644 index a30bd40447..0000000000 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuProps.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Editor, isNodeSelection, posToDOMRect } from "@tiptap/core"; -import { BubbleMenuProps } from "./types"; - -// TODO: don't create new functions every time, only execute necessary logic (isActive) -export function getBubbleMenuProps(editor: Editor): BubbleMenuProps { - return { - marks: { - bold: { - isActive: editor.isActive("bold"), - toggle: () => { - editor.view.focus(); - editor.commands.toggleBold(); - }, - }, - italic: { - isActive: editor.isActive("italic"), - toggle: () => { - editor.view.focus(); - editor.commands.toggleItalic(); - }, - }, - underline: { - isActive: editor.isActive("underline"), - toggle: () => { - editor.view.focus(); - editor.commands.toggleUnderline(); - }, - }, - strike: { - isActive: editor.isActive("strike"), - toggle: () => { - editor.view.focus(); - editor.commands.toggleStrike(); - }, - }, - hyperlink: { - isActive: editor.isActive("link"), - url: editor.getAttributes("link").href, - text: editor.state.doc.textBetween( - editor.state.selection.from, - editor.state.selection.to - ), - set: (url: string, text?: string) => { - if (url === "") { - return; - } - - let { from, to } = editor.state.selection; - - if (!text) { - text = editor.state.doc.textBetween(from, to); - } - - const mark = editor.schema.mark("link", { href: url }); - - editor.view.dispatch( - editor.view.state.tr - .insertText(text, from, to) - .addMark(from, from + text.length, mark) - ); - }, - }, - }, - blocks: { - paragraph: { - isActive: - editor.state.selection.$from.node().type.name === "textContent", - set: () => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "textContent" - ); - }, - }, - heading: { - isActive: - editor.state.selection.$from.node().type.name === "headingContent", - level: editor.state.selection.$from.node().attrs["headingLevel"], - set: (level: string = "1") => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "headingContent", - { - headingLevel: level, - } - ); - }, - }, - listItem: { - isActive: - editor.state.selection.$from.node().type.name === "listItemContent", - type: editor.state.selection.$from.node().attrs["listItemType"], - set: (type: string = "unordered") => { - editor.view.focus(); - editor.commands.BNSetContentType( - editor.state.selection.from, - "listItemContent", - { - listItemType: type, - } - ); - }, - }, - }, - view: { - // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are - // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection - // bounding box used to create it is from an editor view that is out of date due to the animation. - getSelectionBoundingBox: () => { - const { state } = editor.view; - const { selection } = state; - - // support for CellSelections - const { ranges } = selection; - const from = Math.min(...ranges.map((range) => range.$from.pos)); - const to = Math.max(...ranges.map((range) => range.$to.pos)); - - if (isNodeSelection(selection)) { - const node = editor.view.nodeDOM(from) as HTMLElement; - - if (node) { - return node.getBoundingClientRect(); - } - } - - return posToDOMRect(editor.view, from, to); - }, - editorElement: editor.options.element, - }, - }; -} diff --git a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts b/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts deleted file mode 100644 index 8978664d29..0000000000 --- a/packages/core/src/menu-tools/BubbleMenu/getBubbleMenuUpdateProps.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { BubbleMenuUpdateProps } from "./types"; - -export function getBubbleMenuUpdateProps( - editor: Editor -): BubbleMenuUpdateProps { - return { - boldIsActive: editor.isActive("bold"), - italicIsActive: editor.isActive("italic"), - underlineIsActive: editor.isActive("underline"), - strikeIsActive: editor.isActive("strike"), - hyperlinkIsActive: editor.isActive("link"), - activeHyperlinkUrl: editor.getAttributes("link").href, - activeHyperlinkText: editor.state.doc.textBetween( - editor.state.selection.from, - editor.state.selection.to - ), - paragraphIsActive: - editor.state.selection.$from.node().type.name === "textContent", - headingIsActive: - editor.state.selection.$from.node().type.name === "headingContent", - activeHeadingLevel: - editor.state.selection.$from.node().attrs["headingLevel"], - listItemIsActive: - editor.state.selection.$from.node().type.name === "listItemContent", - activeListItemType: - editor.state.selection.$from.node().attrs["listItemType"], - }; -} diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/menu-tools/BubbleMenu/types.ts index 9e6991903e..a49490d5c5 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/menu-tools/BubbleMenu/types.ts @@ -1,87 +1,31 @@ import { Menu, MenuFactory } from "../types"; -// TODO: reconsider .set() function -export type BasicMarkProps = { - isActive: boolean; - toggle: () => void; -}; - -export type HyperlinkMarkProps = { - isActive: boolean; - url: string; - text: string; - set: (url: string, text?: string) => void; -}; - -export type ParagraphBlockProps = { - isActive: boolean; - set: () => void; -}; - -export type HeadingBlockProps = { - isActive: boolean; - level: string; - set: (level: string) => void; -}; - -export type ListItemBlockProps = { - isActive: boolean; - type: string; - set: (type: string) => void; -}; - -export type BubbleMenuProps = { - marks: { - bold: BasicMarkProps; - italic: BasicMarkProps; - underline: BasicMarkProps; - strike: BasicMarkProps; - hyperlink: HyperlinkMarkProps; - }; - blocks: { - paragraph: ParagraphBlockProps; - heading: HeadingBlockProps; - listItem: ListItemBlockProps; - }; - view: { - // TODO: Menu currently needs to delay getting the bounding box, as the editor, and its corresponding view, are - // passed in when an animation starts. This means that the DOMRect found will be incorrect, as the selection - // bounding box used to create it is from an editor view that is out of date due to the animation. - getSelectionBoundingBox: () => DOMRect; - editorElement: Element; - }; -}; - -export type BubbleMenuInitProps = { - toggleBold: () => void; - toggleItalic: () => void; - toggleUnderline: () => void; - toggleStrike: () => void; - setHyperlink: (url: string, text?: string) => void; - setParagraph: () => void; - setHeading: (level: string) => void; - setListItem: (type: string) => void; - getSelectionBoundingBox: () => DOMRect; - editorElement: Element; -}; - -export type BubbleMenuUpdateProps = { +export type BubbleMenuParams = { boldIsActive: boolean; + toggleBold: () => void; italicIsActive: boolean; + toggleItalic: () => void; underlineIsActive: boolean; + toggleUnderline: () => void; strikeIsActive: boolean; + toggleStrike: () => void; hyperlinkIsActive: boolean; activeHyperlinkUrl: string; activeHyperlinkText: string; + setHyperlink: (url: string, text?: string) => void; + paragraphIsActive: boolean; + setParagraph: () => void; headingIsActive: boolean; activeHeadingLevel: string; + setHeading: (level: string) => void; + setListItem: (type: string) => void; listItemIsActive: boolean; activeListItemType: string; + + selectionBoundingBox: DOMRect; + editorElement: Element; }; -export type BubbleMenu = Menu; -export type BubbleMenuFactory = MenuFactory< - BubbleMenuInitProps, - BubbleMenuUpdateProps ->; +export type BubbleMenu = Menu; +export type BubbleMenuFactory = MenuFactory; diff --git a/packages/core/src/menu-tools/types.ts b/packages/core/src/menu-tools/types.ts index 90519aa80e..3b610b46e1 100644 --- a/packages/core/src/menu-tools/types.ts +++ b/packages/core/src/menu-tools/types.ts @@ -1,10 +1,8 @@ -export type Menu = { +export type Menu = { element: HTMLElement | undefined; - show: (props: MenuUpdateProps) => void; + show: (props: MenuProps) => void; hide: () => void; - update: (newProps: MenuUpdateProps) => void; + update: (newProps: MenuProps) => void; }; -export type MenuFactory = ( - initProps: MenuInitProps -) => Menu; +export type MenuFactory = (props: MenuProps) => Menu; diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index e84e9921f4..5574de4146 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -5,8 +5,7 @@ import { BlockNoteTheme } from "../../../core/src/BlockNoteTheme"; import { BubbleMenu, BubbleMenuFactory, - BubbleMenuInitProps, - BubbleMenuUpdateProps, + BubbleMenuParams, } from "../../../core/src/menu-tools/BubbleMenu/types"; import { BubbleMenu as ReactBubbleMenu, @@ -16,49 +15,50 @@ import { // TODO: Rename init & update props to something like static & dynamic props? export const ReactBubbleMenuFactory: BubbleMenuFactory = ( - initProps: BubbleMenuInitProps + params: BubbleMenuParams ): BubbleMenu => { + // TODO: Maybe just use {...params}? const bubbleMenuProps: BubbleMenuProps = { - boldIsActive: false, - toggleBold: initProps.toggleBold, - italicIsActive: false, - toggleItalic: initProps.toggleItalic, - underlineIsActive: false, - toggleUnderline: initProps.toggleUnderline, - strikeIsActive: false, - toggleStrike: initProps.toggleStrike, - hyperlinkIsActive: false, - activeHyperlinkUrl: "", - activeHyperlinkText: "", - setHyperlink: initProps.setHyperlink, + boldIsActive: params.boldIsActive, + toggleBold: params.toggleBold, + italicIsActive: params.italicIsActive, + toggleItalic: params.toggleItalic, + underlineIsActive: params.underlineIsActive, + toggleUnderline: params.toggleUnderline, + strikeIsActive: params.strikeIsActive, + toggleStrike: params.toggleStrike, + hyperlinkIsActive: params.hyperlinkIsActive, + activeHyperlinkUrl: params.activeHyperlinkUrl, + activeHyperlinkText: params.activeHyperlinkText, + setHyperlink: params.setHyperlink, - paragraphIsActive: false, - setParagraph: initProps.setParagraph, - headingIsActive: false, - activeHeadingLevel: "", - setHeading: initProps.setHeading, - listItemIsActive: false, - activeListItemType: "", - setListItem: initProps.setListItem, + paragraphIsActive: params.paragraphIsActive, + setParagraph: params.setParagraph, + headingIsActive: params.headingIsActive, + activeHeadingLevel: params.activeHeadingLevel, + setHeading: params.setHeading, + listItemIsActive: params.listItemIsActive, + activeListItemType: params.activeListItemType, + setListItem: params.setListItem, }; - function updateBubbleMenuProps(updateProps: BubbleMenuUpdateProps) { - // Can't use a constant and not all update props might be needed, though they are in this case. - // bubbleMenuProps = {...bubbleMenuProps, ...updateProps} + function updateBubbleMenuProps(params: BubbleMenuParams) { + // Can't use a constant and not all update props are needed. + // bubbleMenuProps = {...params} - bubbleMenuProps.boldIsActive = updateProps.boldIsActive; - bubbleMenuProps.italicIsActive = updateProps.italicIsActive; - bubbleMenuProps.underlineIsActive = updateProps.underlineIsActive; - bubbleMenuProps.strikeIsActive = updateProps.strikeIsActive; - bubbleMenuProps.hyperlinkIsActive = updateProps.hyperlinkIsActive; - bubbleMenuProps.activeHyperlinkUrl = updateProps.activeHyperlinkUrl; - bubbleMenuProps.activeHyperlinkText = updateProps.activeHyperlinkText; + bubbleMenuProps.boldIsActive = params.boldIsActive; + bubbleMenuProps.italicIsActive = params.italicIsActive; + bubbleMenuProps.underlineIsActive = params.underlineIsActive; + bubbleMenuProps.strikeIsActive = params.strikeIsActive; + bubbleMenuProps.hyperlinkIsActive = params.hyperlinkIsActive; + bubbleMenuProps.activeHyperlinkUrl = params.activeHyperlinkUrl; + bubbleMenuProps.activeHyperlinkText = params.activeHyperlinkText; - bubbleMenuProps.paragraphIsActive = updateProps.paragraphIsActive; - bubbleMenuProps.headingIsActive = updateProps.headingIsActive; - bubbleMenuProps.activeHeadingLevel = updateProps.activeHeadingLevel; - bubbleMenuProps.listItemIsActive = updateProps.listItemIsActive; - bubbleMenuProps.activeListItemType = updateProps.activeListItemType; + bubbleMenuProps.paragraphIsActive = params.paragraphIsActive; + bubbleMenuProps.headingIsActive = params.headingIsActive; + bubbleMenuProps.activeHeadingLevel = params.activeHeadingLevel; + bubbleMenuProps.listItemIsActive = params.listItemIsActive; + bubbleMenuProps.activeListItemType = params.activeListItemType; } const element = document.createElement("div"); @@ -66,9 +66,9 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( const root = createRoot(element); - let menu = tippy(initProps.editorElement, { + let menu = tippy(params.editorElement, { duration: 0, - getReferenceClientRect: initProps.getSelectionBoundingBox, + getReferenceClientRect: () => params.selectionBoundingBox, content: element, interactive: true, trigger: "manual", @@ -78,8 +78,8 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( return { element: element as HTMLElement, - show: (updateProps: BubbleMenuUpdateProps) => { - updateBubbleMenuProps(updateProps); + show: (params: BubbleMenuParams) => { + updateBubbleMenuProps(params); root.render( @@ -87,16 +87,18 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( ); - // Ensures that the component is rendered so that Tippy can display it in the correct position. + // Ensures that the component finishes rendering so that Tippy can display it in the correct position. setTimeout(() => { - menu.popperInstance.forceUpdate(); + menu.setProps({ + getReferenceClientRect: () => params.selectionBoundingBox, + }); }); menu.show(); }, hide: menu.hide, - update: (updateProps: BubbleMenuUpdateProps) => { - updateBubbleMenuProps(updateProps); + update: (params: BubbleMenuParams) => { + updateBubbleMenuProps(params); root.render( @@ -104,9 +106,11 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( ); - // Ensures that the component is rendered so that Tippy can display it in the correct position. + // Ensures that the component finishes rendering so that Tippy can display it in the correct position. setTimeout(() => { - menu.popperInstance.forceUpdate(); + menu.setProps({ + getReferenceClientRect: () => params.selectionBoundingBox, + }); }); }, }; From ebc7d8ad2bed46757a5e9c6f2533a79708fef9f0 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 22 Dec 2022 16:41:18 +0100 Subject: [PATCH 21/55] Major cleanup for hyperlink menu factory code --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 2 +- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 117 +++++++++--------- .../getHyperlinkHoverMenuInitProps.ts | 13 -- .../getHyperlinkHoverMenuProps.ts | 24 ---- .../getHyperlinkMenuUpdateProps.ts | 13 -- .../menu-tools/HyperlinkHoverMenu/types.ts | 30 +---- .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 45 +++---- 7 files changed, 86 insertions(+), 158 deletions(-) delete mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts delete mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts delete mode 100644 packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 4434497f45..e7ea68f59f 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -257,7 +257,7 @@ export class BubbleMenuView { initBubbleMenuParams() { return { - boldIsActive: false, + boldIsActive: this.editor.isActive("bold"), toggleBold: () => { this.editor.view.focus(); this.editor.commands.toggleBold(); diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index 38b0e20494..b5f9b804d8 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -4,9 +4,8 @@ import { Plugin, PluginKey } from "prosemirror-state"; import { HyperlinkHoverMenu, HyperlinkHoverMenuFactory, + HyperlinkHoverMenuParams, } from "../../menu-tools/HyperlinkHoverMenu/types"; -import { getHyperlinkHoverMenuInitProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps"; -import { getHyperlinkHoverMenuUpdateProps } from "../../menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); export type HyperlinkMenuPluginProps = { @@ -21,6 +20,7 @@ export type HyperlinkHoverMenuViewProps = { class HyperlinkHoverMenuView { editor: Editor; + hyperlinkHoverMenuParams: HyperlinkHoverMenuParams; hyperlinkHoverMenu: HyperlinkHoverMenu; menuUpdateTimer: NodeJS.Timeout | undefined; @@ -42,38 +42,9 @@ class HyperlinkHoverMenuView { }: HyperlinkHoverMenuViewProps) { this.editor = editor; - const editHyperlink = (url: string, text: string) => { - const tr = editor.view.state.tr.insertText( - text, - this.hyperlinkMarkRange!.from, - this.hyperlinkMarkRange!.to - ); - tr.addMark( - this.hyperlinkMarkRange!.from, - this.hyperlinkMarkRange!.from + text.length, - editor.schema.mark("link", { href: url }) - ); - editor.view.dispatch(tr); - }; - - const deleteHyperlink = () => { - editor.view.dispatch( - editor.view.state.tr - .removeMark( - this.hyperlinkMarkRange!.from, - this.hyperlinkMarkRange!.to, - this.hyperlinkMark!.type - ) - .setMeta("preventAutolink", true) - ); - }; - + this.hyperlinkHoverMenuParams = this.initHyperlinkHoverMenuParams(); this.hyperlinkHoverMenu = hyperlinkHoverMenuFactory( - getHyperlinkHoverMenuInitProps( - editHyperlink, - deleteHyperlink, - editor.options.element - ) + this.hyperlinkHoverMenuParams ); this.startMenuUpdateTimer = () => { @@ -193,27 +164,11 @@ class HyperlinkHoverMenuView { } if (this.hyperlinkMark) { - // Gets all variables/functions needed to render menu. - const hyperlinkUrl = this.hyperlinkMark.attrs.href; - const hyperlinkText = this.editor.view.state.doc.textBetween( - this.hyperlinkMarkRange!.from, - this.hyperlinkMarkRange!.to - ); - const hyperlinkBoundingBox = posToDOMRect( - this.editor.view, - this.hyperlinkMarkRange!.from, - this.hyperlinkMarkRange!.to - ); + this.updateHyperlinkHoverMenuParams(); // Shows menu. if (!prevHyperlinkMark) { - this.hyperlinkHoverMenu.show( - getHyperlinkHoverMenuUpdateProps( - hyperlinkUrl, - hyperlinkText, - hyperlinkBoundingBox - ) - ); + this.hyperlinkHoverMenu.show(this.hyperlinkHoverMenuParams); this.hyperlinkHoverMenu.element?.addEventListener( "mouseleave", @@ -228,12 +183,60 @@ class HyperlinkHoverMenuView { } // Updates menu. - this.hyperlinkHoverMenu.update( - getHyperlinkHoverMenuUpdateProps( - hyperlinkUrl, - hyperlinkText, - hyperlinkBoundingBox - ) + this.hyperlinkHoverMenu.update(this.hyperlinkHoverMenuParams); + } + } + + initHyperlinkHoverMenuParams() { + return { + hyperlinkUrl: "", + hyperlinkText: "", + editHyperlink: (url: string, text: string) => { + const tr = this.editor.view.state.tr.insertText( + text, + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to + ); + tr.addMark( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.from + text.length, + this.editor.schema.mark("link", { href: url }) + ); + this.editor.view.dispatch(tr); + }, + deleteHyperlink: () => { + this.editor.view.dispatch( + this.editor.view.state.tr + .removeMark( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to, + this.hyperlinkMark!.type + ) + .setMeta("preventAutolink", true) + ); + }, + + hyperlinkBoundingBox: new DOMRect(), + editorElement: this.editor.options.element, + }; + } + + updateHyperlinkHoverMenuParams() { + if (this.hyperlinkMark) { + this.hyperlinkHoverMenuParams.hyperlinkUrl = + this.hyperlinkMark.attrs.href; + this.hyperlinkHoverMenuParams.hyperlinkText = + this.editor.view.state.doc.textBetween( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to + ); + } + + if (this.hyperlinkMarkRange) { + this.hyperlinkHoverMenuParams.hyperlinkBoundingBox = posToDOMRect( + this.editor.view, + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to ); } } diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts deleted file mode 100644 index 378888af45..0000000000 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuInitProps.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HyperlinkHoverMenuInitProps } from "./types"; - -export function getHyperlinkHoverMenuInitProps( - editHyperlink: (url: string, text: string) => void, - deleteHyperlink: () => void, - editorElement: Element -): HyperlinkHoverMenuInitProps { - return { - editHyperlink: editHyperlink, - deleteHyperlink: deleteHyperlink, - editorElement: editorElement, - }; -} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts deleted file mode 100644 index 34ff23405a..0000000000 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkHoverMenuProps.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { HyperlinkHoverMenuProps } from "./types"; - -// TODO: remove nesting -export function getHyperlinkHoverMenuProps( - url: string, - text: string, - editHyperlink: (url: string, text: string) => void, - deleteHyperlink: () => void, - hyperlinkBoundingBox: DOMRect, - editorElement: Element -): HyperlinkHoverMenuProps { - return { - hyperlink: { - url: url, - text: text, - edit: editHyperlink, - delete: deleteHyperlink, - }, - view: { - hyperlinkBoundingBox: hyperlinkBoundingBox, - editorElement: editorElement, - }, - }; -} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts deleted file mode 100644 index 62eb4dc49b..0000000000 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/getHyperlinkMenuUpdateProps.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HyperlinkHoverMenuUpdateProps } from "./types"; - -export function getHyperlinkHoverMenuUpdateProps( - hyperlinkUrl: string, - hyperlinkText: string, - hyperlinkBoundingBox: DOMRect -): HyperlinkHoverMenuUpdateProps { - return { - hyperlinkUrl: hyperlinkUrl, - hyperlinkText: hyperlinkText, - hyperlinkBoundingBox: hyperlinkBoundingBox, - }; -} diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts index 7bb8623acd..d2e128ed6d 100644 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts +++ b/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts @@ -1,32 +1,14 @@ import { Menu, MenuFactory } from "../types"; -export type HyperlinkHoverMenuProps = { - hyperlink: { - url: string; - text: string; - edit: (url: string, text: string) => void; - delete: () => void; - }; - view: { - hyperlinkBoundingBox: DOMRect; - editorElement: Element; - }; -}; - -export type HyperlinkHoverMenuInitProps = { +export type HyperlinkHoverMenuParams = { + hyperlinkUrl: string; + hyperlinkText: string; editHyperlink: (url: string, text: string) => void; deleteHyperlink: () => void; - editorElement: Element; -}; -export type HyperlinkHoverMenuUpdateProps = { - hyperlinkUrl: string; - hyperlinkText: string; hyperlinkBoundingBox: DOMRect; + editorElement: Element; }; -export type HyperlinkHoverMenu = Menu; -export type HyperlinkHoverMenuFactory = MenuFactory< - HyperlinkHoverMenuInitProps, - HyperlinkHoverMenuUpdateProps ->; +export type HyperlinkHoverMenu = Menu; +export type HyperlinkHoverMenuFactory = MenuFactory; diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx index d5af59795e..110c688ef9 100644 --- a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx +++ b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx @@ -6,34 +6,31 @@ import { BlockNoteTheme } from "../BlockNoteTheme"; import { HyperlinkHoverMenu, HyperlinkHoverMenuFactory, - HyperlinkHoverMenuInitProps, - HyperlinkHoverMenuUpdateProps, + HyperlinkHoverMenuParams, } from "../../../core/src/menu-tools/HyperlinkHoverMenu/types"; export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( - initProps: HyperlinkHoverMenuInitProps + params: HyperlinkHoverMenuParams ): HyperlinkHoverMenu => { const hyperlinkMenuProps: HyperlinkMenuProps = { - url: "", - text: "", - update: initProps.editHyperlink, - remove: initProps.deleteHyperlink, + url: params.hyperlinkUrl, + text: params.hyperlinkText, + update: params.editHyperlink, + remove: params.deleteHyperlink, }; - function updateHyperlinkMenuProps( - updateProps: HyperlinkHoverMenuUpdateProps - ) { - hyperlinkMenuProps.url = updateProps.hyperlinkUrl; - hyperlinkMenuProps.text = updateProps.hyperlinkText; + function updateHyperlinkMenuProps(params: HyperlinkHoverMenuParams) { + hyperlinkMenuProps.url = params.hyperlinkUrl; + hyperlinkMenuProps.text = params.hyperlinkText; } const element = document.createElement("div"); + const root = createRoot(element); - const menu = tippy(initProps.editorElement, { - appendTo: initProps.editorElement, + const menu = tippy(params.editorElement, { duration: 0, - getReferenceClientRect: () => new DOMRect(), + getReferenceClientRect: () => params.hyperlinkBoundingBox, content: element, interactive: true, trigger: "manual", @@ -41,12 +38,10 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( hideOnClick: false, }); - menu.show(); - return { element: element, - show: (updateProps: HyperlinkHoverMenuUpdateProps) => { - updateHyperlinkMenuProps(updateProps); + show: (params: HyperlinkHoverMenuParams) => { + updateHyperlinkMenuProps(params); root.render( @@ -55,16 +50,14 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( ); menu.setProps({ - getReferenceClientRect: () => updateProps.hyperlinkBoundingBox, + getReferenceClientRect: () => params.hyperlinkBoundingBox, }); menu.show(); }, - hide: () => { - menu.hide(); - }, - update: (updateProps: HyperlinkHoverMenuUpdateProps) => { - updateHyperlinkMenuProps(updateProps); + hide: menu.hide, + update: (params: HyperlinkHoverMenuParams) => { + updateHyperlinkMenuProps(params); root.render( @@ -73,7 +66,7 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( ); menu.setProps({ - getReferenceClientRect: () => updateProps.hyperlinkBoundingBox, + getReferenceClientRect: () => params.hyperlinkBoundingBox, }); }, }; From 95b1d6e053115e1b0d7e06a57a2dc1864e6e5754 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 22 Dec 2022 17:35:29 +0100 Subject: [PATCH 22/55] Minor fixes --- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 36 ------------------- .../Hyperlinks/HyperlinkMenuPlugin.tsx | 32 ++++++++--------- .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 2 +- 3 files changed, 17 insertions(+), 53 deletions(-) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index e7ea68f59f..c5bc50fdc2 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -91,10 +91,6 @@ export class BubbleMenuView { this.editor.on("blur", this.blurHandler); } - mousedownHandler = () => { - this.preventHide = true; - }; - viewMousedownHandler = () => { this.preventShow = true; }; @@ -105,14 +101,6 @@ export class BubbleMenuView { }; dragstartHandler = () => { - this.bubbleMenu.element!.removeEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); - this.bubbleMenu.hide(); this.menuIsOpen = false; }; @@ -136,14 +124,6 @@ export class BubbleMenuView { return; } - this.bubbleMenu.element!.removeEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); - this.bubbleMenu.hide(); this.menuIsOpen = false; }; @@ -181,14 +161,6 @@ export class BubbleMenuView { this.updateBubbleMenuParams(); this.bubbleMenu.show(this.bubbleMenuParams); this.menuIsOpen = true; - - this.bubbleMenu.element!.addEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); } // Checks if menu should be updated. @@ -211,14 +183,6 @@ export class BubbleMenuView { !this.preventHide && (!shouldShow || this.preventShow) ) { - this.bubbleMenu.element!.removeEventListener( - "mousedown", - this.mousedownHandler, - { - capture: true, - } - ); - this.bubbleMenu.hide(); this.menuIsOpen = false; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index b5f9b804d8..a61536e4a4 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -147,22 +147,6 @@ class HyperlinkHoverMenuView { this.hyperlinkMarkRange = this.keyboardHoveredHyperlinkMarkRange; } - // Hides menu. - if (prevHyperlinkMark && !this.hyperlinkMark) { - this.hyperlinkHoverMenu.element?.removeEventListener( - "mouseleave", - this.startMenuUpdateTimer - ); - this.hyperlinkHoverMenu.element?.removeEventListener( - "mouseenter", - this.stopMenuUpdateTimer - ); - - this.hyperlinkHoverMenu.hide(); - - return; - } - if (this.hyperlinkMark) { this.updateHyperlinkHoverMenuParams(); @@ -185,6 +169,22 @@ class HyperlinkHoverMenuView { // Updates menu. this.hyperlinkHoverMenu.update(this.hyperlinkHoverMenuParams); } + + // Hides menu. + if (prevHyperlinkMark && !this.hyperlinkMark) { + this.hyperlinkHoverMenu.element?.removeEventListener( + "mouseleave", + this.startMenuUpdateTimer + ); + this.hyperlinkHoverMenu.element?.removeEventListener( + "mouseenter", + this.stopMenuUpdateTimer + ); + + this.hyperlinkHoverMenu.hide(); + + return; + } } initHyperlinkHoverMenuParams() { diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx index 4c311c18ec..1c3f3f1064 100644 --- a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx +++ b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx @@ -1,7 +1,7 @@ import { MantineProvider } from "@mantine/core"; import { createRoot } from "react-dom/client"; import tippy from "tippy.js"; -import { HyperlinkMenu } from "./components/HyperlinkMenu"; +import { HyperlinkMenu, HyperlinkMenuProps } from "./components/HyperlinkMenu"; import { BlockNoteTheme } from "../BlockNoteTheme"; import { HyperlinkHoverMenu, From 86d34b3f82ac1a2044a12aa981031c46d5d25df6 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 4 Jan 2023 19:27:04 +0100 Subject: [PATCH 23/55] Converted suggestion plugin view to use a class --- .../getSuggestionsMenuProps.ts | 27 -- .../src/menu-tools/SuggestionsMenu/types.ts | 22 +- .../plugins/suggestion/SuggestionPlugin.ts | 258 +++++++++--------- .../components/suggestion/SuggestionList.tsx | 22 +- .../suggestion/SuggestionsMenuFactory.tsx | 40 ++- 5 files changed, 177 insertions(+), 192 deletions(-) delete mode 100644 packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts diff --git a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts b/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts deleted file mode 100644 index 31e6654532..0000000000 --- a/packages/core/src/menu-tools/SuggestionsMenu/getSuggestionsMenuProps.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; -import { SuggestionsMenuProps } from "./types"; - -// TODO: maybe later discuss if we want to delegate keyboard handling / filtering -// to the client (with automatic defaults) -// TODO: remove -// TODO: Only need either getQuery or matchesQuery, not both. Depends if we want to allow users the ability to define -// their own block type aliases/matching algorithm. -export function getSuggestionsMenuProps( - items: T[], - selectedItemIndex: number, - itemCallback: (item: T) => void, - selectedBlockBoundingBox: DOMRect, - editorElement: Element -): SuggestionsMenuProps { - return { - menuItems: { - items: items, - selectedItemIndex: selectedItemIndex, - itemCallback: itemCallback, - }, - view: { - selectedBlockBoundingBox: selectedBlockBoundingBox, - editorElement: editorElement, - }, - }; -} diff --git a/packages/core/src/menu-tools/SuggestionsMenu/types.ts b/packages/core/src/menu-tools/SuggestionsMenu/types.ts index 60cfff93cd..e7bae26ae5 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/types.ts +++ b/packages/core/src/menu-tools/SuggestionsMenu/types.ts @@ -6,22 +6,18 @@ export type SuggestionsMenuItem = { set: () => void; }; -// TODO: remove nesting -export type SuggestionsMenuProps = { - menuItems: { - items: T[]; - selectedItemIndex: number; - itemCallback: (item: T) => void; - }; - view: { - selectedBlockBoundingBox: DOMRect; - editorElement: Element; - }; +export type SuggestionsMenuParams = { + items: T[]; + selectedItemIndex: number; + itemCallback: (item: T) => void; + + queryStartBoundingBox: DOMRect; + editorElement: Element; }; export type SuggestionsMenu = Menu< - SuggestionsMenuProps + SuggestionsMenuParams >; export type SuggestionsMenuFactory = MenuFactory< - SuggestionsMenuProps + SuggestionsMenuParams >; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 9bbd91b335..c6fa1e2b8c 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -1,10 +1,13 @@ import { Editor, Range } from "@tiptap/core"; import { escapeRegExp } from "lodash"; -import { Plugin, PluginKey, Selection } from "prosemirror-state"; +import { EditorState, Plugin, PluginKey, Selection } from "prosemirror-state"; import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; -import { getSuggestionsMenuProps } from "../../../menu-tools/SuggestionsMenu/getSuggestionsMenuProps"; -import { SuggestionsMenuFactory } from "../../../menu-tools/SuggestionsMenu/types"; +import { + SuggestionsMenu, + SuggestionsMenuFactory, + SuggestionsMenuParams, +} from "../../../menu-tools/SuggestionsMenu/types"; import { SuggestionItem } from "./SuggestionItem"; export type SuggestionPluginOptions = { @@ -44,6 +47,13 @@ export type SuggestionPluginOptions = { allow?: (props: { editor: Editor; range: Range }) => boolean; }; +type SuggestionPluginViewOptions = { + editor: Editor; + pluginKey: PluginKey; + onSelectItem: (props: { item: T; editor: Editor; range: Range }) => void; + suggestionsMenuFactory: SuggestionsMenuFactory; +}; + export type MenuType = "slash" | "drag"; /** @@ -85,6 +95,111 @@ export function findCommandBeforeCursor( }; } +class SuggestionPluginView { + editor: Editor; + pluginKey: PluginKey; + + itemCallback: (props: { item: T; editor: Editor; range: Range }) => void; + + suggestionsMenuParams: SuggestionsMenuParams; + suggestionsMenu: SuggestionsMenu; + + constructor({ + editor, + pluginKey, + onSelectItem: selectItemCallback = () => {}, + suggestionsMenuFactory, + }: SuggestionPluginViewOptions) { + this.editor = editor; + this.pluginKey = pluginKey; + + this.itemCallback = selectItemCallback; + + this.suggestionsMenuParams = this.initSuggestionsMenuParams(); + this.suggestionsMenu = suggestionsMenuFactory(this.suggestionsMenuParams); + } + + update(view: EditorView, prevState: EditorState) { + const prev = this.pluginKey.getState(prevState); + const next = this.pluginKey.getState(view.state); + + // See how the state changed + const started = !prev.active && next.active; + const stopped = prev.active && !next.active; + // TODO: Currently also true for cases in which an update isn't needed so selected list item index updates still + // cause the view to update. May need to be more strict. + const changed = prev.active && next.active; + + // Cancel when suggestion isn't active + if (!started && !changed && !stopped) { + return; + } + + const state = stopped ? prev : next; + + if (stopped) { + this.suggestionsMenu.hide(); + + // Listener stops focus moving to the menu on click. + this.suggestionsMenu.element!.removeEventListener("mousedown", (event) => + event.preventDefault() + ); + } + + if (changed) { + this.updateSuggestionsMenuParams(state); + this.suggestionsMenu.update(this.suggestionsMenuParams); + } + + if (started) { + this.updateSuggestionsMenuParams(state); + this.suggestionsMenu.show(this.suggestionsMenuParams); + + // Listener stops focus moving to the menu on click. + this.suggestionsMenu.element!.addEventListener("mousedown", (event) => + event.preventDefault() + ); + } + } + + initSuggestionsMenuParams() { + return { + items: [], + selectedItemIndex: 0, + itemCallback: (item: T) => { + this.itemCallback({ + item: item, + editor: this.editor, + range: { from: 0, to: 0 }, + }); + }, + queryStartBoundingBox: new DOMRect(), + editorElement: this.editor.options.element, + }; + } + + updateSuggestionsMenuParams(pluginState: any) { + this.suggestionsMenuParams.items = pluginState.items; + this.suggestionsMenuParams.selectedItemIndex = + pluginState.selectedItemIndex; + this.suggestionsMenuParams.itemCallback = (item: T) => { + this.itemCallback({ + item: item, + editor: this.editor, + range: pluginState.range, + }); + }; + + const decorationNode = document.querySelector( + `[data-decoration-id="${pluginState.decorationId}"]` + ); + this.suggestionsMenuParams.queryStartBoundingBox = + decorationNode !== null + ? decorationNode.getBoundingClientRect() + : new DOMRect(); + } +} + /** * A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions. * @@ -115,111 +230,20 @@ export function createSuggestionPlugin({ view.dispatch(view.state.tr.setMeta(pluginKey, { deactivate: true })); }; - // initialize the SuggestionsMenu UI element - // the actual values are dummy values, as the menu isn't shown / positioned yet - // (TBD: we could also decide not to pass these values upon creation, - // or only initialize a menu upon first-use) - const suggestionsMenu = suggestionsMenuFactory( - getSuggestionsMenuProps( - [], - 0, - (item) => { - deactivate(editor.view); - selectItemCallback({ - item: item, - editor: editor, - range: { from: 0, to: 0 }, - }); - }, - new DOMRect(), - editor.options.element - ) - ); - // Plugin key is passed in parameter so it can be exported and used in draghandle return new Plugin({ key: pluginKey, - filterTransaction(transaction) { - // prevent blurring when clicking with the mouse inside the popup menu - const blurMeta = transaction.getMeta("blur"); - if (blurMeta?.event.relatedTarget) { - if (suggestionsMenu.element?.contains(blurMeta.event.relatedTarget)) { - return false; - } - } - return true; - }, - - view() { - return { - update: async (view, prevState) => { - const prev = this.key?.getState(prevState); - const next = this.key?.getState(view.state); - - // See how the state changed - const started = !prev.active && next.active; - const stopped = prev.active && !next.active; - const changed = !started && !stopped && prev.query !== next.query; - - // Cancel when suggestion isn't active - if (!started && !changed && !stopped) { - return; - } - - const state = stopped ? prev : next; - const decorationNode = document.querySelector( - `[data-decoration-id="${state.decorationId}"]` - ); - - if (stopped) { - suggestionsMenu.hide(); - } - - if (changed) { - suggestionsMenu.update( - getSuggestionsMenuProps( - next.items, - 0, - (item) => { - deactivate(editor.view); - selectItemCallback({ - item: item, - editor: editor, - range: state.range, - }); - }, - decorationNode !== null - ? decorationNode.getBoundingClientRect() - : new DOMRect(), - editor.options.element - ) - ); - } - - if (started) { - suggestionsMenu.show( - getSuggestionsMenuProps( - next.items, - 0, - (item) => { - deactivate(editor.view); - selectItemCallback({ - item: item, - editor: editor, - range: state.range, - }); - }, - decorationNode !== null - ? decorationNode.getBoundingClientRect() - : new DOMRect(), - editor.options.element - ) - ); - } + view: (view: EditorView) => + new SuggestionPluginView({ + editor: editor, + pluginKey: pluginKey, + onSelectItem: (props: { item: T; editor: Editor; range: Range }) => { + deactivate(view); + selectItemCallback(props); }, - }; - }, + suggestionsMenuFactory, + }), state: { // Initialize the plugin's internal state. @@ -237,7 +261,7 @@ export function createSuggestionPlugin({ }, // Apply changes to the plugin state from a view transaction. - apply(transaction, prev, _oldState, _newState) { + apply(transaction, prev, _oldState, newState) { const { selection } = transaction; const next = { ...prev }; @@ -250,6 +274,7 @@ export function createSuggestionPlugin({ if ( transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined ) { + console.log("INDEX"); let newIndex = transaction.getMeta(pluginKey).selectedItemIndexChanged; @@ -263,28 +288,6 @@ export function createSuggestionPlugin({ next.selectedItemIndex = newIndex; - const decorationNode = document.querySelector( - `[data-decoration-id="${next.decorationId}"]` - ); - - suggestionsMenu.update( - getSuggestionsMenuProps( - next.items, - next.selectedItemIndex, - (item) => { - selectItemCallback({ - item: item, - editor: editor, - range: next.range, - }); - }, - decorationNode !== null - ? decorationNode.getBoundingClientRect() - : new DOMRect(), - editor.options.element - ) - ); - return next; } @@ -321,7 +324,7 @@ export function createSuggestionPlugin({ // otherwise we get the whole query const match = findCommandBeforeCursor( prev.type === "slash" ? char : "", - selection + newState.selection ); if (!match) { throw new Error("active but no match (suggestions)"); @@ -332,6 +335,7 @@ export function createSuggestionPlugin({ next.decorationId = prev.decorationId; next.query = match.query; next.selectedItemIndex = 0; + console.log(match.query); } } else { next.active = false; diff --git a/packages/react/src/shared/components/suggestion/SuggestionList.tsx b/packages/react/src/shared/components/suggestion/SuggestionList.tsx index 876cea906a..0e701b5db5 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionList.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionList.tsx @@ -1,9 +1,12 @@ -import { SuggestionItem, SuggestionsMenuProps } from "@blocknote/core"; +import { SuggestionItem } from "@blocknote/core"; import { createStyles, Menu } from "@mantine/core"; import { SuggestionListItem } from "./SuggestionListItem"; -export type SuggestionListProps = - SuggestionsMenuProps; +export type SuggestionListProps = { + items: T[]; + selectedItemIndex: number; + itemCallback: (item: T) => void; +}; export function SuggestionList( props: SuggestionListProps @@ -11,14 +14,11 @@ export function SuggestionList( const { classes } = createStyles({ root: {} })(undefined, { name: "SuggestionList", }); - console.log(props.menuItems.items); const headingGroup = []; const basicBlockGroup = []; - for (const item of props.menuItems.items) { - console.log(item.name); - + for (const item of props.items) { if (item.name === "Heading") { headingGroup.push(item); } @@ -59,8 +59,8 @@ export function SuggestionList( name={item.name} hint={item.hint} icon={item.icon} - isSelected={props.menuItems.selectedItemIndex === index} - set={() => props.menuItems.itemCallback(item)} + isSelected={props.selectedItemIndex === index} + set={() => props.itemCallback(item)} /> ); index++; @@ -79,8 +79,8 @@ export function SuggestionList( name={item.name} hint={item.hint} icon={item.icon} - isSelected={props.menuItems.selectedItemIndex === index} - set={() => props.menuItems.itemCallback(item)} + isSelected={props.selectedItemIndex === index} + set={() => props.itemCallback(item)} /> ); index++; diff --git a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx index d9afd13902..e81cf51aac 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx @@ -2,27 +2,37 @@ import { SuggestionItem, SuggestionsMenu, SuggestionsMenuFactory, - SuggestionsMenuProps, + SuggestionsMenuParams, } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; import { createRoot } from "react-dom/client"; import tippy from "tippy.js"; import { BlockNoteTheme } from "../../../BlockNoteTheme"; -import { SuggestionList } from "./SuggestionList"; +import { SuggestionList, SuggestionListProps } from "./SuggestionList"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< SuggestionItem > = ( - props: SuggestionsMenuProps + params: SuggestionsMenuParams ): SuggestionsMenu => { + const suggestionsMenuProps: SuggestionListProps = { + ...params, + }; + + function updateSuggestionsMenuProps(params: SuggestionsMenuParams) { + suggestionsMenuProps.items = params.items; + suggestionsMenuProps.selectedItemIndex = params.selectedItemIndex; + suggestionsMenuProps.itemCallback = params.itemCallback; + } + const element = document.createElement("div"); // element.className = rootStyles.bnRoot; const root = createRoot(element); - const menu = tippy(props.view.editorElement, { + const menu = tippy(params.editorElement, { duration: 0, - getReferenceClientRect: () => props.view.selectedBlockBoundingBox, + getReferenceClientRect: () => params.queryStartBoundingBox, content: element, interactive: true, trigger: "manual", @@ -32,34 +42,36 @@ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< return { element: element as HTMLElement, - show: (props: SuggestionsMenuProps) => { + show: (params: SuggestionsMenuParams) => { + updateSuggestionsMenuProps(params); + root.render( - + ); menu.setProps({ - getReferenceClientRect: () => props.view.selectedBlockBoundingBox, + getReferenceClientRect: () => params.queryStartBoundingBox, }); menu.show(); }, - update: (newProps: SuggestionsMenuProps) => { + hide: menu.hide, + update: (params: SuggestionsMenuParams) => { + updateSuggestionsMenuProps(params); + root.render( - + ); // setProps is a tippy function, // update the position based on passed in props menu.setProps({ - getReferenceClientRect: () => newProps.view.selectedBlockBoundingBox, + getReferenceClientRect: () => params.queryStartBoundingBox, }); }, - hide: () => { - menu.hide(); - }, }; }; From 09ebc391a9617f792954e0506c322bb7bf05edf2 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 5 Jan 2023 14:50:30 +0100 Subject: [PATCH 24/55] Changed editor example to use React as before --- examples/editor/src/App.tsx | 39 +++++++++ examples/editor/src/main.tsx | 74 +++++++++-------- .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 18 +++++ packages/core/src/useEditor.ts | 27 ++++++- .../src/BubbleMenu/BubbleMenuFactory.tsx | 79 ++++++++----------- .../HyperlinkMenus/HyperlinkMenuFactory.tsx | 74 ++++++++--------- .../suggestion/SuggestionsMenuFactory.tsx | 75 +++++++++--------- 7 files changed, 232 insertions(+), 154 deletions(-) create mode 100644 examples/editor/src/App.tsx diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx new file mode 100644 index 0000000000..aa55a05d99 --- /dev/null +++ b/examples/editor/src/App.tsx @@ -0,0 +1,39 @@ +// import logo from './logo.svg' +import { EditorContent, useEditor } from "@blocknote/core"; +import "@blocknote/core/style.css"; +import { Editor } from "@tiptap/core"; +import styles from "./App.module.css"; +import { + ReactBubbleMenuFactory, + ReactHyperlinkMenuFactory, + ReactSuggestionsMenuFactory, +} from "@blocknote/react"; + +type WindowWithProseMirror = Window & + typeof globalThis & { ProseMirror: Editor }; + +function App() { + const editor = useEditor( + { + bubbleMenuFactory: ReactBubbleMenuFactory, + hyperlinkMenuFactory: ReactHyperlinkMenuFactory, + suggestionsMenuFactory: ReactSuggestionsMenuFactory, + }, + { + onUpdate: ({ editor }) => { + console.log(editor.getJSON()); + (window as WindowWithProseMirror).ProseMirror = editor; // Give tests a way to get editor instance + }, + editorProps: { + attributes: { + class: styles.editor, + "data-test": "editor", + }, + }, + } + ); + + return ; +} + +export default App; diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index bafd903d69..789b8e0fc1 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -1,37 +1,45 @@ -import { mountBlockNoteEditor } from "@blocknote/core"; -import "@blocknote/core/style.css"; -import { - ReactBubbleMenuFactory, - ReactHyperlinkMenuFactory, - ReactSuggestionsMenuFactory, -} from "@blocknote/react"; -import styles from "./App.module.css"; +import React from "react"; +import { createRoot } from "react-dom/client"; +import App from "./App"; import "./index.css"; -// type WindowWithProseMirror = Window & -// typeof globalThis & { ProseMirror: Editor }; +window.React = React; -/* - TODO: - -*/ -mountBlockNoteEditor( - { - bubbleMenuFactory: ReactBubbleMenuFactory, - hyperlinkMenuFactory: ReactHyperlinkMenuFactory, - suggestionsMenuFactory: ReactSuggestionsMenuFactory, - }, - { - element: document.getElementById("root")!, - onUpdate: ({ editor }) => { - console.log(editor.getJSON()); - (window as any).ProseMirror = editor; // Give tests a way to get editor instance - }, - editorProps: { - attributes: { - class: styles.editor, - "data-test": "editor", - }, - }, - } +const root = createRoot(document.getElementById("root")!); +root.render( + + + ); + +// TODO: Separate non-React example using code below. +// import { mountBlockNoteEditor } from "@blocknote/core"; +// import "@blocknote/core/style.css"; +// import { +// ReactBubbleMenuFactory, +// ReactHyperlinkMenuFactory, +// ReactSuggestionsMenuFactory, +// } from "@blocknote/react"; +// import styles from "./App.module.css"; +// import "./index.css"; +// +// mountBlockNoteEditor( +// { +// bubbleMenuFactory: ReactBubbleMenuFactory, +// hyperlinkMenuFactory: ReactHyperlinkMenuFactory, +// suggestionsMenuFactory: ReactSuggestionsMenuFactory, +// }, +// { +// element: document.getElementById("root")!, +// onUpdate: ({ editor }) => { +// console.log(editor.getJSON()); +// (window as any).ProseMirror = editor; // Give tests a way to get editor instance +// }, +// editorProps: { +// attributes: { +// class: styles.editor, +// "data-test": "editor", +// }, +// }, +// } +// ); diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index c5bc50fdc2..d9b56e4f9b 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -161,6 +161,14 @@ export class BubbleMenuView { this.updateBubbleMenuParams(); this.bubbleMenu.show(this.bubbleMenuParams); this.menuIsOpen = true; + + // TODO: Is this necessary? Also for other menu plugins. + // Listener stops focus moving to the menu on click. + this.bubbleMenu.element!.addEventListener("mousedown", (event) => + event.preventDefault() + ); + + return; } // Checks if menu should be updated. @@ -169,6 +177,11 @@ export class BubbleMenuView { !this.preventShow && (shouldShow || this.preventHide) ) { + // Hacky fix to account for animations. Since the bounding boxes/DOMRects of elements are calculated based on how + // they are displayed on the screen, we need to wait until a given animation is completed to get the correct + // values for the selectionBoundingBox param. + // TODO: Find a better solution. The delay can cause menu updates to occur while the menu is hidden, which may + // cause issues depending on the menu factory implementation. setTimeout(() => { this.updateBubbleMenuParams(); this.bubbleMenu.update(this.bubbleMenuParams); @@ -186,6 +199,11 @@ export class BubbleMenuView { this.bubbleMenu.hide(); this.menuIsOpen = false; + // Listener stops focus moving to the menu on click. + this.bubbleMenu.element!.removeEventListener("mousedown", (event) => + event.preventDefault() + ); + return; } } diff --git a/packages/core/src/useEditor.ts b/packages/core/src/useEditor.ts index c058ee58e8..4d62fc1c6f 100644 --- a/packages/core/src/useEditor.ts +++ b/packages/core/src/useEditor.ts @@ -4,6 +4,7 @@ import { DependencyList } from "react"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import rootStyles from "./root.module.css"; +import { MenuFactories } from "./BlockNoteEditor"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; @@ -22,13 +23,37 @@ const blockNoteOptions = { * Main hook for importing a BlockNote editor into a react project */ export const useEditor = ( + menuFactories: MenuFactories, options: Partial = {}, deps: DependencyList = [] ) => { - const extensions = options.disableHistoryExtension + let extensions = options.disableHistoryExtension ? blockNoteExtensions.filter((e) => e.name !== "history") : blockNoteExtensions; + // TODO: review + extensions = extensions.map((extension) => { + if (extension.name === "BubbleMenuExtension") { + return extension.configure({ + bubbleMenuFactory: menuFactories.bubbleMenuFactory, + }); + } + + if (extension.name === "link") { + return extension.configure({ + hyperlinkMenuFactory: menuFactories.hyperlinkMenuFactory, + }); + } + + if (extension.name === "slash-command") { + return extension.configure({ + suggestionsMenuFactory: menuFactories.suggestionsMenuFactory, + }); + } + + return extension; + }); + const tiptapOptions = { ...blockNoteOptions, ...options, diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 5840542402..6fd78043d2 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -1,11 +1,11 @@ -import { MantineProvider } from "@mantine/core"; -import { createRoot } from "react-dom/client"; -import tippy from "tippy.js"; +import { createRoot, Root } from "react-dom/client"; import { BubbleMenu, BubbleMenuFactory, BubbleMenuParams, } from "@blocknote/core"; +import { MantineProvider } from "@mantine/core"; +import Tippy from "@tippyjs/react"; import { BubbleMenu as ReactBubbleMenu, BubbleMenuProps, @@ -42,9 +42,6 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( }; function updateBubbleMenuProps(params: BubbleMenuParams) { - // Can't use a constant and not all update props are needed. - // bubbleMenuProps = {...params} - bubbleMenuProps.boldIsActive = params.boldIsActive; bubbleMenuProps.italicIsActive = params.italicIsActive; bubbleMenuProps.underlineIsActive = params.underlineIsActive; @@ -60,57 +57,49 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( bubbleMenuProps.activeListItemType = params.activeListItemType; } - const element = document.createElement("div"); - // element.className = rootStyles.bnRoot; - - const root = createRoot(element); + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; - let menu = tippy(params.editorElement, { - duration: 0, - getReferenceClientRect: () => params.selectionBoundingBox, - content: element, - interactive: true, - trigger: "manual", - placement: "top", - hideOnClick: "toggle", - }); + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.selectionBoundingBox} + hideOnClick={false} + interactive={true} + placement={"top"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } return { - element: element as HTMLElement, + element: menuRootElement, show: (params: BubbleMenuParams) => { updateBubbleMenuProps(params); - root.render( - - - - ); + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); - // Ensures that the component finishes rendering so that Tippy can display it in the correct position. - setTimeout(() => { - menu.setProps({ - getReferenceClientRect: () => params.selectionBoundingBox, - }); - }); + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot!.unmount(); - menu.show(); + menuRootElement.remove(); }, - hide: menu.hide, update: (params: BubbleMenuParams) => { updateBubbleMenuProps(params); - root.render( - - - - ); - - // Ensures that the component finishes rendering so that Tippy can display it in the correct position. - setTimeout(() => { - menu.setProps({ - getReferenceClientRect: () => params.selectionBoundingBox, - }); - }); + menuRoot!.render(getMenuComponent()); }, }; }; diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx index 1c3f3f1064..396bd02401 100644 --- a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx +++ b/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx @@ -1,13 +1,14 @@ -import { MantineProvider } from "@mantine/core"; -import { createRoot } from "react-dom/client"; -import tippy from "tippy.js"; -import { HyperlinkMenu, HyperlinkMenuProps } from "./components/HyperlinkMenu"; -import { BlockNoteTheme } from "../BlockNoteTheme"; +import { createRoot, Root } from "react-dom/client"; import { HyperlinkHoverMenu, HyperlinkHoverMenuFactory, HyperlinkHoverMenuParams, } from "@blocknote/core"; +import { MantineProvider } from "@mantine/core"; +import Tippy from "@tippyjs/react"; +import { HyperlinkMenu, HyperlinkMenuProps } from "./components/HyperlinkMenu"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +// import rootStyles from "../../../core/src/root.module.css"; export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( params: HyperlinkHoverMenuParams @@ -24,50 +25,49 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( hyperlinkMenuProps.text = params.hyperlinkText; } - const element = document.createElement("div"); - - const root = createRoot(element); + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; - const menu = tippy(params.editorElement, { - duration: 0, - getReferenceClientRect: () => params.hyperlinkBoundingBox, - content: element, - interactive: true, - trigger: "manual", - placement: "top", - hideOnClick: false, - }); + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.hyperlinkBoundingBox} + hideOnClick={false} + interactive={true} + placement={"top"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } return { - element: element, + element: menuRootElement, show: (params: HyperlinkHoverMenuParams) => { updateHyperlinkMenuProps(params); - root.render( - - - - ); + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); - menu.setProps({ - getReferenceClientRect: () => params.hyperlinkBoundingBox, - }); + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot!.unmount(); - menu.show(); + menuRootElement.remove(); }, - hide: menu.hide, update: (params: HyperlinkHoverMenuParams) => { updateHyperlinkMenuProps(params); - root.render( - - - - ); - - menu.setProps({ - getReferenceClientRect: () => params.hyperlinkBoundingBox, - }); + menuRoot!.render(getMenuComponent()); }, }; }; diff --git a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx index e81cf51aac..f578319475 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx +++ b/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx @@ -1,3 +1,4 @@ +import { createRoot, Root } from "react-dom/client"; import { SuggestionItem, SuggestionsMenu, @@ -5,10 +6,9 @@ import { SuggestionsMenuParams, } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; -import { createRoot } from "react-dom/client"; -import tippy from "tippy.js"; -import { BlockNoteTheme } from "../../../BlockNoteTheme"; +import Tippy from "@tippyjs/react"; import { SuggestionList, SuggestionListProps } from "./SuggestionList"; +import { BlockNoteTheme } from "../../../BlockNoteTheme"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< @@ -20,58 +20,57 @@ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< ...params, }; - function updateSuggestionsMenuProps(params: SuggestionsMenuParams) { + function updateSuggestionsMenuProps( + params: SuggestionsMenuParams + ) { suggestionsMenuProps.items = params.items; suggestionsMenuProps.selectedItemIndex = params.selectedItemIndex; suggestionsMenuProps.itemCallback = params.itemCallback; } - const element = document.createElement("div"); - // element.className = rootStyles.bnRoot; - const root = createRoot(element); + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; - const menu = tippy(params.editorElement, { - duration: 0, - getReferenceClientRect: () => params.queryStartBoundingBox, - content: element, - interactive: true, - trigger: "manual", - placement: "bottom-start", - hideOnClick: "toggle", - }); + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.queryStartBoundingBox} + hideOnClick={false} + interactive={true} + placement={"bottom-start"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } return { - element: element as HTMLElement, + element: menuRootElement as HTMLElement, show: (params: SuggestionsMenuParams) => { updateSuggestionsMenuProps(params); - root.render( - - - - ); + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); - menu.setProps({ - getReferenceClientRect: () => params.queryStartBoundingBox, - }); + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot!.unmount(); - menu.show(); + menuRootElement.remove(); }, - hide: menu.hide, update: (params: SuggestionsMenuParams) => { updateSuggestionsMenuProps(params); - root.render( - - - - ); - - // setProps is a tippy function, - // update the position based on passed in props - menu.setProps({ - getReferenceClientRect: () => params.queryStartBoundingBox, - }); + menuRoot!.render(getMenuComponent()); }, }; }; From 6db79ff66074688bde46f7ee34f8ae678f39eb6a Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 5 Jan 2023 18:39:54 +0100 Subject: [PATCH 25/55] Improved file structure and minor tweaks --- packages/core/src/BlockNoteEditor.ts | 6 +++--- packages/core/src/MenuFactoryTypes.ts | 10 ++++++++++ .../src/extensions/BubbleMenu/BubbleMenuExtension.tsx | 2 +- .../BubbleMenu/BubbleMenuFactoryTypes.ts} | 2 +- .../core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts | 2 +- .../Hyperlinks/HyperlinkMenuFactoryTypes.ts} | 2 +- .../src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx | 2 +- .../src/extensions/SlashMenu/SlashMenuExtension.ts | 2 +- packages/core/src/index.ts | 6 +++--- packages/core/src/menu-tools/types.ts | 8 -------- .../src/shared/plugins/suggestion/SuggestionPlugin.ts | 4 +--- .../plugins/suggestion/SuggestionsMenuFactoryTypes.ts} | 9 ++------- .../react/src/BubbleMenu/components/BubbleMenu.tsx | 6 +++--- .../src/BubbleMenu/components/LinkToolbarButton.tsx | 2 +- .../components/EditHyperlinkMenuItemIcon.tsx | 2 +- .../components/HoverHyperlinkMenu.tsx | 4 ++-- .../Toolbar/components}/Toolbar.tsx | 0 .../Toolbar/components}/ToolbarButton.tsx | 2 +- .../Toolbar/components}/ToolbarDropdown.tsx | 0 .../Toolbar/components}/ToolbarDropdownItem.tsx | 0 .../Toolbar/components}/ToolbarDropdownTarget.tsx | 0 .../Tooltip}/TooltipContent.module.css | 0 .../Tooltip/components}/TooltipContent.tsx | 2 +- .../SuggestionsMenuFactory.tsx | 7 +++++-- .../components}/SuggestionList.tsx | 0 .../components}/SuggestionListItem.tsx | 0 packages/react/src/index.ts | 2 +- 27 files changed, 40 insertions(+), 42 deletions(-) create mode 100644 packages/core/src/MenuFactoryTypes.ts rename packages/core/src/{menu-tools/BubbleMenu/types.ts => extensions/BubbleMenu/BubbleMenuFactoryTypes.ts} (93%) rename packages/core/src/{menu-tools/HyperlinkHoverMenu/types.ts => extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts} (86%) delete mode 100644 packages/core/src/menu-tools/types.ts rename packages/core/src/{menu-tools/SuggestionsMenu/types.ts => shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts} (66%) rename packages/react/src/{shared/components/toolbar => SharedComponents/Toolbar/components}/Toolbar.tsx (100%) rename packages/react/src/{shared/components/toolbar => SharedComponents/Toolbar/components}/ToolbarButton.tsx (95%) rename packages/react/src/{shared/components/toolbar => SharedComponents/Toolbar/components}/ToolbarDropdown.tsx (100%) rename packages/react/src/{shared/components/toolbar => SharedComponents/Toolbar/components}/ToolbarDropdownItem.tsx (100%) rename packages/react/src/{shared/components/toolbar => SharedComponents/Toolbar/components}/ToolbarDropdownTarget.tsx (100%) rename packages/react/src/{shared/components/tooltip => SharedComponents/Tooltip}/TooltipContent.module.css (100%) rename packages/react/src/{shared/components/tooltip => SharedComponents/Tooltip/components}/TooltipContent.tsx (91%) rename packages/react/src/{shared/components/suggestion => SuggestionsMenu}/SuggestionsMenuFactory.tsx (94%) rename packages/react/src/{shared/components/suggestion => SuggestionsMenu/components}/SuggestionList.tsx (100%) rename packages/react/src/{shared/components/suggestion => SuggestionsMenu/components}/SuggestionListItem.tsx (100%) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index d64c6698b8..73d4bc034c 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -2,9 +2,9 @@ import { Editor, EditorOptions } from "@tiptap/core"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; -import { BubbleMenuFactory } from "./menu-tools/BubbleMenu/types"; -import { HyperlinkHoverMenuFactory } from "./menu-tools/HyperlinkHoverMenu/types"; -import { SuggestionsMenuFactory } from "./menu-tools/SuggestionsMenu/types"; +import { BubbleMenuFactory } from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; +import { HyperlinkHoverMenuFactory } from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; +import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; import rootStyles from "./root.module.css"; import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; diff --git a/packages/core/src/MenuFactoryTypes.ts b/packages/core/src/MenuFactoryTypes.ts new file mode 100644 index 0000000000..9192e29c29 --- /dev/null +++ b/packages/core/src/MenuFactoryTypes.ts @@ -0,0 +1,10 @@ +export type Menu> = { + element: HTMLElement | undefined; + show: (params: MenuParams) => void; + hide: () => void; + update: (params: MenuParams) => void; +}; + +export type MenuFactory> = ( + params: MenuParams +) => Menu; diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx index 7b72b47788..a3f852b890 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx @@ -1,7 +1,7 @@ import { Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; import { createBubbleMenuPlugin } from "./BubbleMenuPlugin"; -import { BubbleMenuFactory } from "../../menu-tools/BubbleMenu/types"; +import { BubbleMenuFactory } from "./BubbleMenuFactoryTypes"; /** * The menu that is displayed when selecting a piece of text. diff --git a/packages/core/src/menu-tools/BubbleMenu/types.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts similarity index 93% rename from packages/core/src/menu-tools/BubbleMenu/types.ts rename to packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts index a49490d5c5..75327f65aa 100644 --- a/packages/core/src/menu-tools/BubbleMenu/types.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts @@ -1,4 +1,4 @@ -import { Menu, MenuFactory } from "../types"; +import { Menu, MenuFactory } from "../../MenuFactoryTypes"; export type BubbleMenuParams = { boldIsActive: boolean; diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index d9b56e4f9b..9d03e2b99f 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -10,7 +10,7 @@ import { BubbleMenu, BubbleMenuFactory, BubbleMenuParams, -} from "../../menu-tools/BubbleMenu/types"; +} from "./BubbleMenuFactoryTypes"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files diff --git a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts similarity index 86% rename from packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts rename to packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts index d2e128ed6d..1645f7ceb0 100644 --- a/packages/core/src/menu-tools/HyperlinkHoverMenu/types.ts +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts @@ -1,4 +1,4 @@ -import { Menu, MenuFactory } from "../types"; +import { Menu, MenuFactory } from "../../MenuFactoryTypes"; export type HyperlinkHoverMenuParams = { hyperlinkUrl: string; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index a61536e4a4..7335a2dc3a 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -5,7 +5,7 @@ import { HyperlinkHoverMenu, HyperlinkHoverMenuFactory, HyperlinkHoverMenuParams, -} from "../../menu-tools/HyperlinkHoverMenu/types"; +} from "./HyperlinkMenuFactoryTypes"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); export type HyperlinkMenuPluginProps = { diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts index 8cbde64e42..e269dd5343 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts @@ -1,6 +1,6 @@ import { Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; -import { SuggestionsMenuFactory } from "../../menu-tools/SuggestionsMenu/types"; +import { SuggestionsMenuFactory } from "../../shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; import { createSuggestionPlugin } from "../../shared/plugins/suggestion/SuggestionPlugin"; import defaultCommands from "./defaultCommands"; import { SlashMenuItem } from "./SlashMenuItem"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 62f1c1af0d..3c1964a545 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,8 +3,8 @@ import "./globals.css"; export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; export * from "./EditorContent"; -export * from "./menu-tools/BubbleMenu/types"; -export * from "./menu-tools/HyperlinkHoverMenu/types"; -export * from "./menu-tools/SuggestionsMenu/types"; +export * from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; +export * from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; +export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; export * from "./useEditor"; diff --git a/packages/core/src/menu-tools/types.ts b/packages/core/src/menu-tools/types.ts deleted file mode 100644 index 3b610b46e1..0000000000 --- a/packages/core/src/menu-tools/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Menu = { - element: HTMLElement | undefined; - show: (props: MenuProps) => void; - hide: () => void; - update: (newProps: MenuProps) => void; -}; - -export type MenuFactory = (props: MenuProps) => Menu; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index c6fa1e2b8c..5d45b452ba 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -7,7 +7,7 @@ import { SuggestionsMenu, SuggestionsMenuFactory, SuggestionsMenuParams, -} from "../../../menu-tools/SuggestionsMenu/types"; +} from "./SuggestionsMenuFactoryTypes"; import { SuggestionItem } from "./SuggestionItem"; export type SuggestionPluginOptions = { @@ -274,7 +274,6 @@ export function createSuggestionPlugin({ if ( transaction.getMeta(pluginKey)?.selectedItemIndexChanged !== undefined ) { - console.log("INDEX"); let newIndex = transaction.getMeta(pluginKey).selectedItemIndexChanged; @@ -335,7 +334,6 @@ export function createSuggestionPlugin({ next.decorationId = prev.decorationId; next.query = match.query; next.selectedItemIndex = 0; - console.log(match.query); } } else { next.active = false; diff --git a/packages/core/src/menu-tools/SuggestionsMenu/types.ts b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts similarity index 66% rename from packages/core/src/menu-tools/SuggestionsMenu/types.ts rename to packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts index e7bae26ae5..3fe73adc4d 100644 --- a/packages/core/src/menu-tools/SuggestionsMenu/types.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts @@ -1,10 +1,5 @@ -import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; -import { Menu, MenuFactory } from "../types"; - -export type SuggestionsMenuItem = { - name: string; - set: () => void; -}; +import { SuggestionItem } from "./SuggestionItem"; +import { Menu, MenuFactory } from "../../../MenuFactoryTypes"; export type SuggestionsMenuParams = { items: T[]; diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx index 2a1fccf799..a7e4039d6c 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/BubbleMenu/components/BubbleMenu.tsx @@ -13,9 +13,9 @@ import { RiText, RiUnderline, } from "react-icons/ri"; -import { ToolbarButton } from "../../shared/components/toolbar/ToolbarButton"; -import { ToolbarDropdown } from "../../shared/components/toolbar/ToolbarDropdown"; -import { Toolbar } from "../../shared/components/toolbar/Toolbar"; +import { ToolbarButton } from "../../SharedComponents/Toolbar/components/ToolbarButton"; +import { ToolbarDropdown } from "../../SharedComponents/Toolbar/components/ToolbarDropdown"; +import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; diff --git a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx index e21f471866..8c1d62966b 100644 --- a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx +++ b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx @@ -3,7 +3,7 @@ import { useCallback, useState } from "react"; import { ToolbarButton, ToolbarButtonProps, -} from "../../shared/components/toolbar/ToolbarButton"; +} from "../../SharedComponents/Toolbar/components/ToolbarButton"; import { EditHyperlinkMenu } from "../../HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenu"; type HyperlinkButtonProps = ToolbarButtonProps & { diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx index 9d4936307d..a026d787e2 100644 --- a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx +++ b/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx @@ -1,6 +1,6 @@ import { IconType } from "react-icons"; import Tippy from "@tippyjs/react"; -import { TooltipContent } from "../../../shared/components/tooltip/TooltipContent"; +import { TooltipContent } from "../../../SharedComponents/Tooltip/components/TooltipContent"; import { Container } from "@mantine/core"; export type EditHyperlinkMenuItemIconProps = { diff --git a/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx b/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx index acdaf8c9dc..6c9cfb77ac 100644 --- a/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx @@ -1,6 +1,6 @@ import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { Toolbar } from "../../../shared/components/toolbar/Toolbar"; -import { ToolbarButton } from "../../../shared/components/toolbar/ToolbarButton"; +import { Toolbar } from "../../../SharedComponents/Toolbar/components/Toolbar"; +import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; type HoverHyperlinkMenuProps = { url: string; diff --git a/packages/react/src/shared/components/toolbar/Toolbar.tsx b/packages/react/src/SharedComponents/Toolbar/components/Toolbar.tsx similarity index 100% rename from packages/react/src/shared/components/toolbar/Toolbar.tsx rename to packages/react/src/SharedComponents/Toolbar/components/Toolbar.tsx diff --git a/packages/react/src/shared/components/toolbar/ToolbarButton.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx similarity index 95% rename from packages/react/src/shared/components/toolbar/ToolbarButton.tsx rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx index 3c12ec8f23..7e0879e6a3 100644 --- a/packages/react/src/shared/components/toolbar/ToolbarButton.tsx +++ b/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx @@ -1,7 +1,7 @@ import { ActionIcon, Button } from "@mantine/core"; import Tippy from "@tippyjs/react"; import { forwardRef } from "react"; -import { TooltipContent } from "../tooltip/TooltipContent"; +import { TooltipContent } from "../../Tooltip/components/TooltipContent"; import { IconType } from "react-icons"; export type ToolbarButtonProps = { diff --git a/packages/react/src/shared/components/toolbar/ToolbarDropdown.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx similarity index 100% rename from packages/react/src/shared/components/toolbar/ToolbarDropdown.tsx rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdown.tsx diff --git a/packages/react/src/shared/components/toolbar/ToolbarDropdownItem.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx similarity index 100% rename from packages/react/src/shared/components/toolbar/ToolbarDropdownItem.tsx rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownItem.tsx diff --git a/packages/react/src/shared/components/toolbar/ToolbarDropdownTarget.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownTarget.tsx similarity index 100% rename from packages/react/src/shared/components/toolbar/ToolbarDropdownTarget.tsx rename to packages/react/src/SharedComponents/Toolbar/components/ToolbarDropdownTarget.tsx diff --git a/packages/react/src/shared/components/tooltip/TooltipContent.module.css b/packages/react/src/SharedComponents/Tooltip/TooltipContent.module.css similarity index 100% rename from packages/react/src/shared/components/tooltip/TooltipContent.module.css rename to packages/react/src/SharedComponents/Tooltip/TooltipContent.module.css diff --git a/packages/react/src/shared/components/tooltip/TooltipContent.tsx b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx similarity index 91% rename from packages/react/src/shared/components/tooltip/TooltipContent.tsx rename to packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx index 2a57e841b8..cdcfd0fecf 100644 --- a/packages/react/src/shared/components/tooltip/TooltipContent.tsx +++ b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx @@ -1,4 +1,4 @@ -import styles from "./TooltipContent.module.css"; +import styles from "../TooltipContent.module.css"; /** * Helper for the tooltip for inline bubble menu buttons. diff --git a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx b/packages/react/src/SuggestionsMenu/SuggestionsMenuFactory.tsx similarity index 94% rename from packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx rename to packages/react/src/SuggestionsMenu/SuggestionsMenuFactory.tsx index f578319475..81398c8b1f 100644 --- a/packages/react/src/shared/components/suggestion/SuggestionsMenuFactory.tsx +++ b/packages/react/src/SuggestionsMenu/SuggestionsMenuFactory.tsx @@ -7,8 +7,11 @@ import { } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; import Tippy from "@tippyjs/react"; -import { SuggestionList, SuggestionListProps } from "./SuggestionList"; -import { BlockNoteTheme } from "../../../BlockNoteTheme"; +import { + SuggestionList, + SuggestionListProps, +} from "./components/SuggestionList"; +import { BlockNoteTheme } from "../BlockNoteTheme"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< diff --git a/packages/react/src/shared/components/suggestion/SuggestionList.tsx b/packages/react/src/SuggestionsMenu/components/SuggestionList.tsx similarity index 100% rename from packages/react/src/shared/components/suggestion/SuggestionList.tsx rename to packages/react/src/SuggestionsMenu/components/SuggestionList.tsx diff --git a/packages/react/src/shared/components/suggestion/SuggestionListItem.tsx b/packages/react/src/SuggestionsMenu/components/SuggestionListItem.tsx similarity index 100% rename from packages/react/src/shared/components/suggestion/SuggestionListItem.tsx rename to packages/react/src/SuggestionsMenu/components/SuggestionListItem.tsx diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index f54a34b5d4..11fffc00c1 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -2,4 +2,4 @@ export * from "./BubbleMenu/BubbleMenuFactory"; export * from "./EditorContent"; export * from "./HyperlinkMenus/HyperlinkMenuFactory"; -export * from "./shared/components/suggestion/SuggestionsMenuFactory"; +export * from "./SuggestionsMenu/SuggestionsMenuFactory"; From a3617c30c218050b1371185767789e525b12ef20 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 9 Jan 2023 11:47:51 +0100 Subject: [PATCH 26/55] Minor refactor --- .../components/LinkToolbarButton.tsx | 2 +- .../react/src/{ => Editor}/EditorContent.tsx | 0 .../components/EditHyperlinkMenu.tsx | 0 .../components/EditHyperlinkMenuItem.tsx | 0 .../components/EditHyperlinkMenuItemIcon.tsx | 0 .../components/EditHyperlinkMenuItemInput.tsx | 0 .../HyperlinkMenuFactory.tsx | 0 .../components/HyperlinkMenu.tsx | 56 +++++++++++++++++++ .../components/HoverHyperlinkMenu.tsx | 37 ------------ .../components/HyperlinkMenu.tsx | 41 -------------- packages/react/src/index.ts | 4 +- packages/react/src/utils.ts | 31 ---------- 12 files changed, 59 insertions(+), 112 deletions(-) rename packages/react/src/{ => Editor}/EditorContent.tsx (100%) rename packages/react/src/{HyperlinkMenus => HyperlinkMenu}/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx (100%) rename packages/react/src/{HyperlinkMenus => HyperlinkMenu}/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx (100%) rename packages/react/src/{HyperlinkMenus => HyperlinkMenu}/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx (100%) rename packages/react/src/{HyperlinkMenus => HyperlinkMenu}/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx (100%) rename packages/react/src/{HyperlinkMenus => HyperlinkMenu}/HyperlinkMenuFactory.tsx (100%) create mode 100644 packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx delete mode 100644 packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx delete mode 100644 packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx diff --git a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx index 8c1d62966b..06a656c12a 100644 --- a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx +++ b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx @@ -4,7 +4,7 @@ import { ToolbarButton, ToolbarButtonProps, } from "../../SharedComponents/Toolbar/components/ToolbarButton"; -import { EditHyperlinkMenu } from "../../HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenu"; +import { EditHyperlinkMenu } from "../../HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenu"; type HyperlinkButtonProps = ToolbarButtonProps & { hyperlinkIsActive: boolean; diff --git a/packages/react/src/EditorContent.tsx b/packages/react/src/Editor/EditorContent.tsx similarity index 100% rename from packages/react/src/EditorContent.tsx rename to packages/react/src/Editor/EditorContent.tsx diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx b/packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx similarity index 100% rename from packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx rename to packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx b/packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx similarity index 100% rename from packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx rename to packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx b/packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx similarity index 100% rename from packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx rename to packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx diff --git a/packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx b/packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx similarity index 100% rename from packages/react/src/HyperlinkMenus/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx rename to packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx diff --git a/packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenu/HyperlinkMenuFactory.tsx similarity index 100% rename from packages/react/src/HyperlinkMenus/HyperlinkMenuFactory.tsx rename to packages/react/src/HyperlinkMenu/HyperlinkMenuFactory.tsx diff --git a/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx b/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx new file mode 100644 index 0000000000..6dc305d142 --- /dev/null +++ b/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx @@ -0,0 +1,56 @@ +import { useState } from "react"; +import { EditHyperlinkMenu } from "../EditHyperlinkMenu/components/EditHyperlinkMenu"; +import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; +import { ToolbarButton } from "../../SharedComponents/Toolbar/components/ToolbarButton"; +import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; +// import rootStyles from "../../../root.module.css"; + +export type HyperlinkMenuProps = { + url: string; + text: string; + update: (url: string, text: string) => void; + remove: () => void; +}; + +/** + * Main menu component for the hyperlink extension. + * Either renders a menu to create/edit a hyperlink, or a menu to interact with it on mouse hover. + */ +export const HyperlinkMenu = (props: HyperlinkMenuProps) => { + const [isEditing, setIsEditing] = useState(false); + + if (isEditing) { + return ( + + ); + } + + return ( + + setIsEditing(true)}> + Edit Link + + { + window.open(props.url, "_blank"); + }} + icon={RiExternalLinkFill} + /> + + + ); +}; diff --git a/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx b/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx deleted file mode 100644 index 6c9cfb77ac..0000000000 --- a/packages/react/src/HyperlinkMenus/HoverHyperlinkMenu/components/HoverHyperlinkMenu.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; -import { Toolbar } from "../../../SharedComponents/Toolbar/components/Toolbar"; -import { ToolbarButton } from "../../../SharedComponents/Toolbar/components/ToolbarButton"; - -type HoverHyperlinkMenuProps = { - url: string; - edit: () => void; - remove: () => void; -}; - -/** - * Menu which opens when hovering an existing hyperlink. - * Provides buttons for editing, opening, and removing the hyperlink. - */ -export const HoverHyperlinkMenu = (props: HoverHyperlinkMenuProps) => { - return ( - - - Edit Link - - { - window.open(props.url, "_blank"); - }} - icon={RiExternalLinkFill} - /> - - - ); -}; diff --git a/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx b/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx deleted file mode 100644 index 65f65f77f1..0000000000 --- a/packages/react/src/HyperlinkMenus/components/HyperlinkMenu.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { useState } from "react"; -import { EditHyperlinkMenu } from "../EditHyperlinkMenu/components/EditHyperlinkMenu"; -import { HoverHyperlinkMenu } from "../HoverHyperlinkMenu/components/HoverHyperlinkMenu"; -// import rootStyles from "../../../root.module.css"; - -export type HyperlinkMenuProps = { - url: string; - text: string; - update: (url: string, text: string) => void; - remove: () => void; -}; - -/** - * Main menu component for the hyperlink extension. - * Either renders a menu to create/edit a hyperlink, or a menu to interact with it on mouse hover. - */ -export const HyperlinkMenu = (props: HyperlinkMenuProps) => { - const [isEditing, setIsEditing] = useState(false); - - const editHyperlinkMenu = ( - - ); - - const hoverHyperlinkMenu = ( - setIsEditing(true)} - remove={props.remove} - /> - ); - - if (isEditing) { - return editHyperlinkMenu; - } else { - return hoverHyperlinkMenu; - } -}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 11fffc00c1..b1649a8893 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,5 +1,5 @@ // TODO: review directories export * from "./BubbleMenu/BubbleMenuFactory"; -export * from "./EditorContent"; -export * from "./HyperlinkMenus/HyperlinkMenuFactory"; +export * from "./Editor/EditorContent"; +export * from "./HyperlinkMenu/HyperlinkMenuFactory"; export * from "./SuggestionsMenu/SuggestionsMenuFactory"; diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts index f43e3d30fe..a035199358 100644 --- a/packages/react/src/utils.ts +++ b/packages/react/src/utils.ts @@ -1,6 +1,3 @@ -import { useEffect, useState } from "react"; -import { Editor } from "@tiptap/core"; - export const isAppleOS = () => /Mac/.test(navigator.platform) || (/AppleWebKit/.test(navigator.userAgent) && @@ -13,31 +10,3 @@ export function formatKeyboardShortcut(shortcut: string) { return shortcut.replace("Mod", "Ctrl"); } } - -function useForceUpdate() { - const [, setValue] = useState(0); - - return () => setValue((value) => value + 1); -} - -// This is a component that is similar to https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/useEditor.ts -// Use it to rerender a component whenever a transaction happens in the editor -export const useEditorForceUpdate = (editor: Editor) => { - const forceUpdate = useForceUpdate(); - - useEffect(() => { - const callback = () => { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - forceUpdate(); - }); - }); - }; - - editor.on("transaction", callback); - return () => { - editor.off("transaction", callback); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [editor]); -}; From 03154f720f38974aca8adf2f273087c7c3db668c Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 9 Jan 2023 12:29:02 +0100 Subject: [PATCH 27/55] Fixed hyperlink menu behaviour --- .../core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts | 1 + .../src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 9d03e2b99f..f8b9a711ee 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -283,6 +283,7 @@ export class BubbleMenuView { .insertText(text, from, to) .addMark(from, from + text.length, mark) ); + this.editor.view.focus(); }, paragraphIsActive: this.editor.state.selection.$from.node().type.name === "textContent", diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx index 7335a2dc3a..3c4fac9d93 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx @@ -105,6 +105,10 @@ class HyperlinkHoverMenuView { } update() { + if (!this.editor.view.hasFocus()) { + return; + } + // Saves the currently hovered hyperlink mark before it's updated. const prevHyperlinkMark = this.hyperlinkMark; @@ -203,6 +207,9 @@ class HyperlinkHoverMenuView { this.editor.schema.mark("link", { href: url }) ); this.editor.view.dispatch(tr); + this.editor.view.focus(); + + this.hyperlinkHoverMenu.hide(); }, deleteHyperlink: () => { this.editor.view.dispatch( @@ -214,6 +221,9 @@ class HyperlinkHoverMenuView { ) .setMeta("preventAutolink", true) ); + this.editor.view.focus(); + + this.hyperlinkHoverMenu.hide(); }, hyperlinkBoundingBox: new DOMRect(), From 7e2ef52015dc096471cbb323868d96a38990d4e3 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 11 Jan 2023 00:35:49 +0100 Subject: [PATCH 28/55] Separated drag handle UI elements to react package --- examples/editor/src/App.tsx | 6 ++ packages/core/src/EditorElement.ts | 10 +++ packages/core/src/MenuFactoryTypes.ts | 10 --- .../BubbleMenu/BubbleMenuFactoryTypes.ts | 6 +- .../DraggableBlocks/DragMenuFactoryTypes.ts | 24 ++++++ .../DraggableBlocks/components/DragHandle.tsx | 80 ------------------- .../components/DragHandleMenu.tsx | 19 ----- .../Hyperlinks/HyperlinkMenuFactoryTypes.ts | 14 ++-- packages/core/src/index.ts | 1 + packages/core/src/useEditor.ts | 19 +++-- .../AddBlockButton/AddBlockButtonFactory.tsx | 70 ++++++++++++++++ .../components/AddBlockButton.tsx | 14 ++++ .../src/DragHandle/DragHandleFactory.tsx | 58 ++++++++++++++ .../src/DragHandle/components/DragHandle.tsx | 10 +++ .../DragHandleMenu/DragHandleMenuFactory.tsx | 72 +++++++++++++++++ .../components/DragHandleMenu.tsx | 24 ++++++ packages/react/src/index.ts | 3 + 17 files changed, 316 insertions(+), 124 deletions(-) create mode 100644 packages/core/src/EditorElement.ts delete mode 100644 packages/core/src/MenuFactoryTypes.ts create mode 100644 packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts delete mode 100644 packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx delete mode 100644 packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx create mode 100644 packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx create mode 100644 packages/react/src/AddBlockButton/components/AddBlockButton.tsx create mode 100644 packages/react/src/DragHandle/DragHandleFactory.tsx create mode 100644 packages/react/src/DragHandle/components/DragHandle.tsx create mode 100644 packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx create mode 100644 packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index aa55a05d99..65fae001d7 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -4,7 +4,10 @@ import "@blocknote/core/style.css"; import { Editor } from "@tiptap/core"; import styles from "./App.module.css"; import { + ReactAddBlockButtonFactory, ReactBubbleMenuFactory, + ReactDragHandleFactory, + ReactDragHandleMenuFactory, ReactHyperlinkMenuFactory, ReactSuggestionsMenuFactory, } from "@blocknote/react"; @@ -15,7 +18,10 @@ type WindowWithProseMirror = Window & function App() { const editor = useEditor( { + addBlockButtonFactory: ReactAddBlockButtonFactory, bubbleMenuFactory: ReactBubbleMenuFactory, + dragHandleFactory: ReactDragHandleFactory, + dragHandleMenuFactory: ReactDragHandleMenuFactory, hyperlinkMenuFactory: ReactHyperlinkMenuFactory, suggestionsMenuFactory: ReactSuggestionsMenuFactory, }, diff --git a/packages/core/src/EditorElement.ts b/packages/core/src/EditorElement.ts new file mode 100644 index 0000000000..386df572c5 --- /dev/null +++ b/packages/core/src/EditorElement.ts @@ -0,0 +1,10 @@ +export type EditorElement> = { + element: HTMLElement | undefined; + show: (params: ElementParams) => void; + hide: () => void; + update: (params: ElementParams) => void; +}; + +export type ElementFactory> = ( + params: ElementParams +) => EditorElement; diff --git a/packages/core/src/MenuFactoryTypes.ts b/packages/core/src/MenuFactoryTypes.ts deleted file mode 100644 index 9192e29c29..0000000000 --- a/packages/core/src/MenuFactoryTypes.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type Menu> = { - element: HTMLElement | undefined; - show: (params: MenuParams) => void; - hide: () => void; - update: (params: MenuParams) => void; -}; - -export type MenuFactory> = ( - params: MenuParams -) => Menu; diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts index 75327f65aa..36fdbe7728 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts @@ -1,4 +1,4 @@ -import { Menu, MenuFactory } from "../../MenuFactoryTypes"; +import { EditorElement, ElementFactory } from "../../EditorElement"; export type BubbleMenuParams = { boldIsActive: boolean; @@ -27,5 +27,5 @@ export type BubbleMenuParams = { editorElement: Element; }; -export type BubbleMenu = Menu; -export type BubbleMenuFactory = MenuFactory; +export type BubbleMenu = EditorElement; +export type BubbleMenuFactory = ElementFactory; diff --git a/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts new file mode 100644 index 0000000000..715950f48c --- /dev/null +++ b/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts @@ -0,0 +1,24 @@ +import { EditorElement, ElementFactory } from "../../EditorElement"; + +export type AddBlockButtonParams = { + addBlock: () => void; + blockBoundingBox: DOMRect; +}; + +export type AddBlockButton = EditorElement; +export type AddBlockButtonFactory = ElementFactory; + +export type DragHandleParams = { + blockBoundingBox: DOMRect; +}; + +export type DragHandle = EditorElement; +export type DragHandleFactory = ElementFactory; + +export type DragHandleMenuParams = { + deleteBlock: () => void; + dragHandleBoundingBox: DOMRect; +}; + +export type DragHandleMenu = EditorElement; +export type DragHandleMenuFactory = ElementFactory; diff --git a/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx b/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx deleted file mode 100644 index c880b556b8..0000000000 --- a/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { ActionIcon, Menu } from "@mantine/core"; -import { AiOutlinePlus, MdDragIndicator } from "react-icons/all"; -import DragHandleMenu from "./DragHandleMenu"; -import { SlashMenuPluginKey } from "../../SlashMenu/SlashMenuExtension"; -import { getBlockInfoFromPos } from "../../Blocks/helpers/getBlockInfoFromPos"; - -export const DragHandle = (props: { - editor: Editor; - coords: { left: number; top: number }; - onShow?: () => void; - onHide?: () => void; - onAddClicked?: () => void; -}) => { - const onDelete = () => { - const pos = props.editor.view.posAtCoords(props.coords); - if (!pos) { - return; - } - - props.editor.commands.BNDeleteBlock(pos.pos); - }; - - const onAddClick = () => { - if (props.onAddClicked) { - props.onAddClicked(); - } - - const pos = props.editor.view.posAtCoords(props.coords); - if (!pos) { - return; - } - - const blockInfo = getBlockInfoFromPos(props.editor.state.doc, pos.pos); - if (blockInfo === undefined) { - return; - } - - const { contentNode, endPos } = blockInfo; - - // Creates a new block if current one is not empty for the suggestion menu to open in. - if (contentNode.textContent.length !== 0) { - const newBlockInsertionPos = endPos + 1; - const newBlockContentPos = newBlockInsertionPos + 2; - - props.editor - .chain() - .BNCreateBlock(newBlockInsertionPos) - .BNSetContentType(newBlockContentPos, "textContent") - .setTextSelection(newBlockContentPos) - .run(); - } - - // Focuses and activates the suggestion menu. - props.editor.view.focus(); - props.editor.view.dispatch( - props.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, { - // TODO import suggestion plugin key - activate: true, - type: "drag", - }) - ); - }; - - return ( -
- - {} - - - - - {} - - - - -
- ); -}; diff --git a/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx b/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx deleted file mode 100644 index 2ccaabfd55..0000000000 --- a/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createStyles, Menu } from "@mantine/core"; - -type Props = { - onDelete: () => void; -}; - -const DragHandleMenu = (props: Props) => { - const { classes } = createStyles({ root: {} })(undefined, { - name: "DragHandleMenu", - }); - - return ( - - Delete - - ); -}; - -export default DragHandleMenu; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts index 1645f7ceb0..efa75ed20c 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts @@ -1,14 +1,14 @@ -import { Menu, MenuFactory } from "../../MenuFactoryTypes"; +import { EditorElement, ElementFactory } from "../../EditorElement"; -export type HyperlinkHoverMenuParams = { - hyperlinkUrl: string; - hyperlinkText: string; +export type HyperlinkMenuParams = { + url: string; + text: string; editHyperlink: (url: string, text: string) => void; deleteHyperlink: () => void; - hyperlinkBoundingBox: DOMRect; + boundingBox: DOMRect; editorElement: Element; }; -export type HyperlinkHoverMenu = Menu; -export type HyperlinkHoverMenuFactory = MenuFactory; +export type HyperlinkMenu = EditorElement; +export type HyperlinkMenuFactory = ElementFactory; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3c1964a545..f8c5c1ad27 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,7 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; export * from "./EditorContent"; export * from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; +export * from "./extensions/DraggableBlocks/DragMenuFactoryTypes"; export * from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; diff --git a/packages/core/src/useEditor.ts b/packages/core/src/useEditor.ts index 4d62fc1c6f..cddb6e5219 100644 --- a/packages/core/src/useEditor.ts +++ b/packages/core/src/useEditor.ts @@ -4,7 +4,7 @@ import { DependencyList } from "react"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import rootStyles from "./root.module.css"; -import { MenuFactories } from "./BlockNoteEditor"; +import { ElementFactories } from "./BlockNoteEditor"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; @@ -23,7 +23,7 @@ const blockNoteOptions = { * Main hook for importing a BlockNote editor into a react project */ export const useEditor = ( - menuFactories: MenuFactories, + elementFactories: ElementFactories, options: Partial = {}, deps: DependencyList = [] ) => { @@ -35,19 +35,27 @@ export const useEditor = ( extensions = extensions.map((extension) => { if (extension.name === "BubbleMenuExtension") { return extension.configure({ - bubbleMenuFactory: menuFactories.bubbleMenuFactory, + bubbleMenuFactory: elementFactories.bubbleMenuFactory, }); } if (extension.name === "link") { return extension.configure({ - hyperlinkMenuFactory: menuFactories.hyperlinkMenuFactory, + hyperlinkMenuFactory: elementFactories.hyperlinkMenuFactory, }); } if (extension.name === "slash-command") { return extension.configure({ - suggestionsMenuFactory: menuFactories.suggestionsMenuFactory, + suggestionsMenuFactory: elementFactories.suggestionsMenuFactory, + }); + } + + if (extension.name === "DraggableBlocksExtension") { + return extension.configure({ + addBlockButtonFactory: elementFactories.addBlockButtonFactory, + dragHandleFactory: elementFactories.dragHandleFactory, + dragHandleMenuFactory: elementFactories.dragHandleMenuFactory, }); } @@ -72,5 +80,6 @@ export const useEditor = ( }, }, }; + return useEditorTiptap(tiptapOptions, deps); }; diff --git a/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx b/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx new file mode 100644 index 0000000000..8b3180fb7b --- /dev/null +++ b/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx @@ -0,0 +1,70 @@ +import { + AddBlockButton, + AddBlockButtonFactory, + AddBlockButtonParams, +} from "@blocknote/core"; +import { AddBlockButtonProps } from "./components/AddBlockButton"; +import { AddBlockButton as ReactAddBlockButton } from "./components/AddBlockButton"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +import { MantineProvider } from "@mantine/core"; +import { createRoot, Root } from "react-dom/client"; +import Tippy from "@tippyjs/react"; + +export const ReactAddBlockButtonFactory: AddBlockButtonFactory = ( + params: AddBlockButtonParams +): AddBlockButton => { + const addBlockButtonProps: AddBlockButtonProps = { + ...params, + }; + + function updateAddBlockButtonProps(params: AddBlockButtonParams) { + addBlockButtonProps.addBlock = params.addBlock; + } + + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.blockBoundingBox} + hideOnClick={false} + interactive={true} + offset={[0, 24]} + placement={"left"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } + + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; + + return { + element: menuRootElement, + show: (params: AddBlockButtonParams) => { + updateAddBlockButtonProps(params); + + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); + + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot?.unmount(); + + menuRootElement.remove(); + }, + update: (_params: AddBlockButtonParams) => { + updateAddBlockButtonProps(params); + + menuRoot?.render(getMenuComponent()); + }, + }; +}; diff --git a/packages/react/src/AddBlockButton/components/AddBlockButton.tsx b/packages/react/src/AddBlockButton/components/AddBlockButton.tsx new file mode 100644 index 0000000000..7a4bbe5cba --- /dev/null +++ b/packages/react/src/AddBlockButton/components/AddBlockButton.tsx @@ -0,0 +1,14 @@ +import { ActionIcon } from "@mantine/core"; +import { AiOutlinePlus } from "react-icons/all"; + +export type AddBlockButtonProps = { + addBlock: () => void; +}; + +export const AddBlockButton = (props: AddBlockButtonProps) => { + return ( + + {} + + ); +}; diff --git a/packages/react/src/DragHandle/DragHandleFactory.tsx b/packages/react/src/DragHandle/DragHandleFactory.tsx new file mode 100644 index 0000000000..a2ce905400 --- /dev/null +++ b/packages/react/src/DragHandle/DragHandleFactory.tsx @@ -0,0 +1,58 @@ +import { + DragHandle, + DragHandleFactory, + DragHandleParams, +} from "@blocknote/core"; +import { DragHandle as ReactDragHandle } from "./components/DragHandle"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +import { MantineProvider } from "@mantine/core"; +import { createRoot, Root } from "react-dom/client"; +import Tippy from "@tippyjs/react"; + +export const ReactDragHandleFactory: DragHandleFactory = ( + params: DragHandleParams +): DragHandle => { + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + menuRootElement.style.position = "absolute"; + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; + + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.blockBoundingBox} + hideOnClick={false} + interactive={true} + offset={[0, 0]} + placement={"left"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } + + return { + element: menuRootElement, + show: (_params: DragHandleParams) => { + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); + + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot?.unmount(); + + menuRootElement.remove(); + }, + update: (_params: DragHandleParams) => { + menuRoot?.render(getMenuComponent()); + }, + }; +}; diff --git a/packages/react/src/DragHandle/components/DragHandle.tsx b/packages/react/src/DragHandle/components/DragHandle.tsx new file mode 100644 index 0000000000..c5c4bea973 --- /dev/null +++ b/packages/react/src/DragHandle/components/DragHandle.tsx @@ -0,0 +1,10 @@ +import { ActionIcon } from "@mantine/core"; +import { MdDragIndicator } from "react-icons/all"; + +export const DragHandle = () => { + return ( + + {} + + ); +}; diff --git a/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx b/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx new file mode 100644 index 0000000000..605ff608a8 --- /dev/null +++ b/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx @@ -0,0 +1,72 @@ +import { MantineProvider } from "@mantine/core"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +import { createRoot, Root } from "react-dom/client"; +import { + DragHandleMenu as ReactDragHandleMenu, + DragHandleMenuProps, +} from "./components/DragHandleMenu"; +import { + DragHandleMenuFactory, + DragHandleMenuParams, + DragHandleMenu, +} from "@blocknote/core"; +import Tippy from "@tippyjs/react"; + +export const ReactDragHandleMenuFactory: DragHandleMenuFactory = ( + params: DragHandleMenuParams +): DragHandleMenu => { + const dragHandleMenuProps: DragHandleMenuProps = { + ...params, + }; + + function updateDragHandleMenuProps(params: DragHandleMenuParams) { + dragHandleMenuProps.deleteBlock = params.deleteBlock; + } + + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.dragHandleBoundingBox} + hideOnClick={false} + interactive={true} + // offset={[24, 0]} + placement={"left"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } + + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; + + return { + element: menuRootElement, + show: (params: DragHandleMenuParams) => { + updateDragHandleMenuProps(params); + + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); + + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot?.unmount(); + + menuRootElement.remove(); + }, + update: (_params: DragHandleMenuParams) => { + updateDragHandleMenuProps(params); + + menuRoot?.render(getMenuComponent()); + }, + }; +}; diff --git a/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx b/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx new file mode 100644 index 0000000000..b8f8a9bc52 --- /dev/null +++ b/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx @@ -0,0 +1,24 @@ +import { createStyles, Menu } from "@mantine/core"; + +export type DragHandleMenuProps = { + deleteBlock: () => void; +}; + +export const DragHandleMenu = (props: DragHandleMenuProps) => { + const { classes } = createStyles({ root: {} })(undefined, { + name: "DragHandleMenu", + }); + + return ( +
+ + +
+
+ + Delete + +
+
+ ); +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b1649a8893..dc4253e378 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,5 +1,8 @@ // TODO: review directories +export * from "./AddBlockButton/AddBlockButtonFactory"; export * from "./BubbleMenu/BubbleMenuFactory"; +export * from "./DragHandle/DragHandleFactory"; +export * from "./DragHandleMenu/DragHandleMenuFactory"; export * from "./Editor/EditorContent"; export * from "./HyperlinkMenu/HyperlinkMenuFactory"; export * from "./SuggestionsMenu/SuggestionsMenuFactory"; From baea3e2c47860dc958169a47639f531c56ad998c Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 11 Jan 2023 00:35:49 +0100 Subject: [PATCH 29/55] Separated drag handle UI elements to react package --- examples/editor/src/App.tsx | 6 + packages/core/src/EditorElement.ts | 10 + packages/core/src/MenuFactoryTypes.ts | 10 - .../BubbleMenu/BubbleMenuFactoryTypes.ts | 6 +- .../DraggableBlocks/DragMenuFactoryTypes.ts | 24 ++ .../DraggableBlocksExtension.ts | 36 ++- ...cksPlugin.tsx => DraggableBlocksPlugin.ts} | 260 ++++++++++++------ .../DraggableBlocks/components/DragHandle.tsx | 80 ------ .../components/DragHandleMenu.tsx | 19 -- .../Hyperlinks/HyperlinkMenuFactoryTypes.ts | 14 +- packages/core/src/index.ts | 1 + packages/core/src/useEditor.ts | 19 +- .../AddBlockButton/AddBlockButtonFactory.tsx | 70 +++++ .../components/AddBlockButton.tsx | 14 + .../src/DragHandle/DragHandleFactory.tsx | 58 ++++ .../src/DragHandle/components/DragHandle.tsx | 10 + .../DragHandleMenu/DragHandleMenuFactory.tsx | 72 +++++ .../components/DragHandleMenu.tsx | 24 ++ packages/react/src/index.ts | 3 + 19 files changed, 519 insertions(+), 217 deletions(-) create mode 100644 packages/core/src/EditorElement.ts delete mode 100644 packages/core/src/MenuFactoryTypes.ts create mode 100644 packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts rename packages/core/src/extensions/DraggableBlocks/{DraggableBlocksPlugin.tsx => DraggableBlocksPlugin.ts} (60%) delete mode 100644 packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx delete mode 100644 packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx create mode 100644 packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx create mode 100644 packages/react/src/AddBlockButton/components/AddBlockButton.tsx create mode 100644 packages/react/src/DragHandle/DragHandleFactory.tsx create mode 100644 packages/react/src/DragHandle/components/DragHandle.tsx create mode 100644 packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx create mode 100644 packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index aa55a05d99..65fae001d7 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -4,7 +4,10 @@ import "@blocknote/core/style.css"; import { Editor } from "@tiptap/core"; import styles from "./App.module.css"; import { + ReactAddBlockButtonFactory, ReactBubbleMenuFactory, + ReactDragHandleFactory, + ReactDragHandleMenuFactory, ReactHyperlinkMenuFactory, ReactSuggestionsMenuFactory, } from "@blocknote/react"; @@ -15,7 +18,10 @@ type WindowWithProseMirror = Window & function App() { const editor = useEditor( { + addBlockButtonFactory: ReactAddBlockButtonFactory, bubbleMenuFactory: ReactBubbleMenuFactory, + dragHandleFactory: ReactDragHandleFactory, + dragHandleMenuFactory: ReactDragHandleMenuFactory, hyperlinkMenuFactory: ReactHyperlinkMenuFactory, suggestionsMenuFactory: ReactSuggestionsMenuFactory, }, diff --git a/packages/core/src/EditorElement.ts b/packages/core/src/EditorElement.ts new file mode 100644 index 0000000000..386df572c5 --- /dev/null +++ b/packages/core/src/EditorElement.ts @@ -0,0 +1,10 @@ +export type EditorElement> = { + element: HTMLElement | undefined; + show: (params: ElementParams) => void; + hide: () => void; + update: (params: ElementParams) => void; +}; + +export type ElementFactory> = ( + params: ElementParams +) => EditorElement; diff --git a/packages/core/src/MenuFactoryTypes.ts b/packages/core/src/MenuFactoryTypes.ts deleted file mode 100644 index 9192e29c29..0000000000 --- a/packages/core/src/MenuFactoryTypes.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type Menu> = { - element: HTMLElement | undefined; - show: (params: MenuParams) => void; - hide: () => void; - update: (params: MenuParams) => void; -}; - -export type MenuFactory> = ( - params: MenuParams -) => Menu; diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts index 75327f65aa..36fdbe7728 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts @@ -1,4 +1,4 @@ -import { Menu, MenuFactory } from "../../MenuFactoryTypes"; +import { EditorElement, ElementFactory } from "../../EditorElement"; export type BubbleMenuParams = { boldIsActive: boolean; @@ -27,5 +27,5 @@ export type BubbleMenuParams = { editorElement: Element; }; -export type BubbleMenu = Menu; -export type BubbleMenuFactory = MenuFactory; +export type BubbleMenu = EditorElement; +export type BubbleMenuFactory = ElementFactory; diff --git a/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts new file mode 100644 index 0000000000..715950f48c --- /dev/null +++ b/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts @@ -0,0 +1,24 @@ +import { EditorElement, ElementFactory } from "../../EditorElement"; + +export type AddBlockButtonParams = { + addBlock: () => void; + blockBoundingBox: DOMRect; +}; + +export type AddBlockButton = EditorElement; +export type AddBlockButtonFactory = ElementFactory; + +export type DragHandleParams = { + blockBoundingBox: DOMRect; +}; + +export type DragHandle = EditorElement; +export type DragHandleFactory = ElementFactory; + +export type DragHandleMenuParams = { + deleteBlock: () => void; + dragHandleBoundingBox: DOMRect; +}; + +export type DragHandleMenu = EditorElement; +export type DragHandleMenuFactory = ElementFactory; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts index e6042402ca..e02157a156 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts @@ -1,15 +1,35 @@ -import { Extension } from "@tiptap/core"; +import { Editor, Extension } from "@tiptap/core"; import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin"; +import { + AddBlockButtonFactory, + DragHandleFactory, + DragHandleMenuFactory, +} from "./DragMenuFactoryTypes"; + +export type DraggableBlocksOptions = { + editor: Editor; + addBlockButtonFactory: AddBlockButtonFactory; + dragHandleFactory: DragHandleFactory; + dragHandleMenuFactory: DragHandleMenuFactory; +}; /** * This extension adds a drag handle in front of all nodes with a "data-id" attribute * * code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799 */ -export const DraggableBlocksExtension = Extension.create<{}>({ - name: "DraggableBlocksExtension", - priority: 1000, // Need to be high, in order to hide draghandle when typing slash - addProseMirrorPlugins() { - return [createDraggableBlocksPlugin(this.editor)]; - }, -}); +export const DraggableBlocksExtension = + Extension.create({ + name: "DraggableBlocksExtension", + priority: 1000, // Need to be high, in order to hide draghandle when typing slash + addProseMirrorPlugins() { + return [ + createDraggableBlocksPlugin({ + editor: this.editor, + addBlockButtonFactory: this.options.addBlockButtonFactory, + dragHandleFactory: this.options.dragHandleFactory, + dragHandleMenuFactory: this.options.dragHandleMenuFactory, + }), + ]; + }, + }); diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts similarity index 60% rename from packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx rename to packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index f0f5011239..94c8535e8f 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.tsx +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -1,12 +1,17 @@ -import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state"; import * as pv from "prosemirror-view"; import { EditorView } from "prosemirror-view"; -import { createRoot, Root } from "react-dom/client"; // import { BlockNoteTheme } from "../../BlockNoteTheme"; import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection"; -import { DragHandle } from "./components/DragHandle"; +import { DraggableBlocksOptions } from "./DraggableBlocksExtension"; +import { + AddBlockButtonParams, + DragHandleMenuParams, + DragHandleParams, +} from "./DragMenuFactoryTypes"; +import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; +import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension"; const serializeForClipboard = (pv as any).__serializeForClipboard; // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799 @@ -217,52 +222,131 @@ function dragStart(e: DragEvent, view: EditorView) { } } -export const createDraggableBlocksPlugin = (editor: Editor) => { - let dropElement: HTMLElement | undefined; - let dropElementRoot: Root | undefined; - - const WIDTH = 48; - +export const createDraggableBlocksPlugin = ( + options: DraggableBlocksOptions +) => { // When true, the drag handle with be anchored at the same level as root elements // When false, the drag handle with be just to the left of the element const horizontalPosAnchoredAtRoot = true; + // Determines if the drag handle and add block buttons should be visible. Gets set to true on mouse move events. Gets + // set to false on mouse click and key down events. + let blockButtonsVisible = false; + // Determines if the drag handle and add block buttons should be frozen, i.e. should not update on mouse move. Gets + // set to true when clicking the add block button. Gets set to false on mouse click and key down events. + let blockButtonsFrozen = false; + + // Declares callback functions for use for params in drag handle, drag handle menu, and add block button factories. + function addBlock(coords: { left: number; top: number }) { + blockButtonsFrozen = true; + + const pos = options.editor.view.posAtCoords(coords); + if (!pos) { + return; + } + + const blockInfo = getBlockInfoFromPos(options.editor.state.doc, pos.pos); + if (blockInfo === undefined) { + return; + } - let menuShown = false; - let addClicked = false; + const { contentNode, endPos } = blockInfo; + + // Creates a new block if current one is not empty for the suggestion menu to open in. + if (contentNode.textContent.length !== 0) { + const newBlockInsertionPos = endPos + 1; + const newBlockContentPos = newBlockInsertionPos + 2; + + options.editor + .chain() + .BNCreateBlock(newBlockInsertionPos) + .BNSetContentType(newBlockContentPos, "textContent") + .setTextSelection(newBlockContentPos) + .run(); + } + + // Focuses and activates the suggestion menu. + options.editor.view.focus(); + options.editor.view.dispatch( + options.editor.view.state.tr + .scrollIntoView() + .setMeta(SlashMenuPluginKey, { + // TODO import suggestion plugin key + activate: true, + type: "drag", + }) + ); + } - const onShow = () => { - menuShown = true; + function deleteBlock(coords: { left: number; top: number }) { + dragHandleMenu.hide(); + + const pos = options.editor.view.posAtCoords(coords); + if (!pos) { + return; + } + + options.editor.commands.BNDeleteBlock(pos.pos); + } + + // Initializes params for use in drag handle, drag handle menu, and add block button factories. + const addBlockButtonParams: AddBlockButtonParams = { + addBlock: () => addBlock({ left: 0, top: 0 }), + blockBoundingBox: new DOMRect(), }; - const onHide = () => { - menuShown = false; + const dragHandleParams: DragHandleParams = { + blockBoundingBox: new DOMRect(), }; - const onAddClicked = () => { - addClicked = true; + const dragHandleMenuParams: DragHandleMenuParams = { + deleteBlock: () => deleteBlock({ left: 0, top: 0 }), + dragHandleBoundingBox: new DOMRect(), }; + // Creates drag handle, drag handle menu, and add block button editor elements. + const addBlockButton = options.addBlockButtonFactory(addBlockButtonParams); + const dragHandle = options.dragHandleFactory(dragHandleParams); + const dragHandleMenu = options.dragHandleMenuFactory(dragHandleMenuParams); + + // Declares additional listeners to attach to the drag handle element for drag/drop & menu opening. + const dragStartCallback = (event: DragEvent) => + dragStart(event, options.editor.view); + const dragEndCallback = (_event: DragEvent) => unsetDragImage(); + const clickCallback = (_event: MouseEvent) => { + dragHandleMenu.show(dragHandleMenuParams); + blockButtonsFrozen = true; + }; + + function addDragHandleListeners() { + dragHandle.element!.addEventListener("dragstart", dragStartCallback); + dragHandle.element!.addEventListener("dragend", dragEndCallback); + dragHandle.element!.addEventListener("click", clickCallback); + } + + function removeDragHandleListeners() { + dragHandle.element!.removeEventListener("dragstart", dragStartCallback); + dragHandle.element!.removeEventListener("dragend", dragEndCallback); + dragHandle.element!.removeEventListener("click", clickCallback); + } + + // Hides drag handle, drag handle menu, and add block button when scrolling. + window.addEventListener("scroll", () => { + blockButtonsVisible = false; + blockButtonsFrozen = false; + + addBlockButton.hide(); + dragHandle.hide(); + removeDragHandleListeners(); + dragHandleMenu.hide(); + }); + return new Plugin({ key: new PluginKey("DraggableBlocksPlugin"), - view(editorView) { - dropElement = document.createElement("div"); - dropElement.setAttribute("draggable", "true"); - dropElement.style.position = "absolute"; - dropElement.style.height = "24px"; // default height - document.body.append(dropElement); - - dropElement.addEventListener("dragstart", (e) => - dragStart(e, editorView) - ); - dropElement.addEventListener("dragend", () => unsetDragImage()); - dropElementRoot = createRoot(dropElement); + view() { return { - // update(view, prevState) {}, destroy() { - if (!dropElement) { - throw new Error("unexpected"); - } - dropElement.parentNode!.removeChild(dropElement); - dropElement = undefined; - dropElementRoot = undefined; + addBlockButton.hide(); + dragHandle.hide(); + removeDragHandleListeners(); + dragHandleMenu.hide(); }, }; }, @@ -281,12 +365,13 @@ export const createDraggableBlocksPlugin = (editor: Editor) => { // return true; // }, handleKeyDown(_view, _event) { - if (!dropElementRoot) { - throw new Error("unexpected"); - } - menuShown = false; - addClicked = false; - dropElementRoot.render(<>); + blockButtonsVisible = false; + blockButtonsFrozen = false; + + addBlockButton.hide(); + dragHandle.hide(); + removeDragHandleListeners(); + return false; }, handleDOMEvents: { @@ -295,32 +380,27 @@ export const createDraggableBlocksPlugin = (editor: Editor) => { // return false; // }, mouseleave(_view, _event: any) { - if (!dropElement) { - throw new Error("unexpected"); - } // TODO // dropElement.style.display = "none"; return true; }, mousedown(_view, _event: any) { - if (!dropElementRoot) { - throw new Error("unexpected"); - } - menuShown = false; - addClicked = false; - dropElementRoot.render(<>); + blockButtonsVisible = false; + blockButtonsFrozen = false; + + addBlockButton.hide(); + dragHandle.hide(); + removeDragHandleListeners(); + dragHandleMenu.hide(); + return false; }, mousemove(view, event: any) { - if (!dropElementRoot || !dropElement) { - throw new Error("unexpected"); - } - - if (menuShown || addClicked) { - // The submenu is open, don't move draghandle - // Or if the user clicked the add button + if (blockButtonsFrozen) { return true; } + + // Gets block at mouse Y coordinate. const coords = { left: view.dom.clientWidth / 2, // take middle of editor top: event.clientY, @@ -341,33 +421,43 @@ export const createDraggableBlocksPlugin = (editor: Editor) => { return true; } - const rect = absoluteRect(blockContent); - const win = block.node.ownerDocument.defaultView!; - const dropElementRect = dropElement.getBoundingClientRect(); - const left = - (horizontalPosAnchoredAtRoot ? getHorizontalAnchor() : rect.left) - - WIDTH + - win.pageXOffset; - rect.top += - rect.height / 2 - dropElementRect.height / 2 + win.pageYOffset; - - dropElement.style.left = left + "px"; - dropElement.style.top = rect.top + "px"; - - // MantineProvider has been commented out because I removed - // BlockNoteTheme. I know this will be part of the DraggableBlocks rewrite anyway - dropElementRoot.render( - // - - // - ); + // Gets bounding box of relevant block. + const blockBoundingBox = blockContent.getBoundingClientRect(); + blockBoundingBox.x = horizontalPosAnchoredAtRoot + ? getHorizontalAnchor() + : blockBoundingBox.left; + + // Updates element params. + addBlockButtonParams.addBlock = () => + addBlock({ + left: blockBoundingBox.left, + top: blockBoundingBox.top, + }); + addBlockButtonParams.blockBoundingBox = blockBoundingBox; + + dragHandleParams.blockBoundingBox = blockBoundingBox; + + dragHandleMenuParams.deleteBlock = () => + deleteBlock({ + left: blockBoundingBox.left, + top: blockBoundingBox.top, + }); + dragHandleMenuParams.dragHandleBoundingBox = blockBoundingBox; + + // Shows or updates elements. + if (!blockButtonsVisible) { + blockButtonsVisible = true; + + dragHandle.show(dragHandleParams); + addBlockButton.show(addBlockButtonParams); + + dragHandle.element!.setAttribute("draggable", "true"); + addDragHandleListeners(); + } else { + dragHandle.update(dragHandleParams); + addBlockButton.update(addBlockButtonParams); + } + return true; }, }, diff --git a/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx b/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx deleted file mode 100644 index c880b556b8..0000000000 --- a/packages/core/src/extensions/DraggableBlocks/components/DragHandle.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Editor } from "@tiptap/core"; -import { ActionIcon, Menu } from "@mantine/core"; -import { AiOutlinePlus, MdDragIndicator } from "react-icons/all"; -import DragHandleMenu from "./DragHandleMenu"; -import { SlashMenuPluginKey } from "../../SlashMenu/SlashMenuExtension"; -import { getBlockInfoFromPos } from "../../Blocks/helpers/getBlockInfoFromPos"; - -export const DragHandle = (props: { - editor: Editor; - coords: { left: number; top: number }; - onShow?: () => void; - onHide?: () => void; - onAddClicked?: () => void; -}) => { - const onDelete = () => { - const pos = props.editor.view.posAtCoords(props.coords); - if (!pos) { - return; - } - - props.editor.commands.BNDeleteBlock(pos.pos); - }; - - const onAddClick = () => { - if (props.onAddClicked) { - props.onAddClicked(); - } - - const pos = props.editor.view.posAtCoords(props.coords); - if (!pos) { - return; - } - - const blockInfo = getBlockInfoFromPos(props.editor.state.doc, pos.pos); - if (blockInfo === undefined) { - return; - } - - const { contentNode, endPos } = blockInfo; - - // Creates a new block if current one is not empty for the suggestion menu to open in. - if (contentNode.textContent.length !== 0) { - const newBlockInsertionPos = endPos + 1; - const newBlockContentPos = newBlockInsertionPos + 2; - - props.editor - .chain() - .BNCreateBlock(newBlockInsertionPos) - .BNSetContentType(newBlockContentPos, "textContent") - .setTextSelection(newBlockContentPos) - .run(); - } - - // Focuses and activates the suggestion menu. - props.editor.view.focus(); - props.editor.view.dispatch( - props.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, { - // TODO import suggestion plugin key - activate: true, - type: "drag", - }) - ); - }; - - return ( -
- - {} - - - - - {} - - - - -
- ); -}; diff --git a/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx b/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx deleted file mode 100644 index 2ccaabfd55..0000000000 --- a/packages/core/src/extensions/DraggableBlocks/components/DragHandleMenu.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createStyles, Menu } from "@mantine/core"; - -type Props = { - onDelete: () => void; -}; - -const DragHandleMenu = (props: Props) => { - const { classes } = createStyles({ root: {} })(undefined, { - name: "DragHandleMenu", - }); - - return ( - - Delete - - ); -}; - -export default DragHandleMenu; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts index 1645f7ceb0..efa75ed20c 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts @@ -1,14 +1,14 @@ -import { Menu, MenuFactory } from "../../MenuFactoryTypes"; +import { EditorElement, ElementFactory } from "../../EditorElement"; -export type HyperlinkHoverMenuParams = { - hyperlinkUrl: string; - hyperlinkText: string; +export type HyperlinkMenuParams = { + url: string; + text: string; editHyperlink: (url: string, text: string) => void; deleteHyperlink: () => void; - hyperlinkBoundingBox: DOMRect; + boundingBox: DOMRect; editorElement: Element; }; -export type HyperlinkHoverMenu = Menu; -export type HyperlinkHoverMenuFactory = MenuFactory; +export type HyperlinkMenu = EditorElement; +export type HyperlinkMenuFactory = ElementFactory; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3c1964a545..f8c5c1ad27 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,7 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; export * from "./EditorContent"; export * from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; +export * from "./extensions/DraggableBlocks/DragMenuFactoryTypes"; export * from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; diff --git a/packages/core/src/useEditor.ts b/packages/core/src/useEditor.ts index 4d62fc1c6f..cddb6e5219 100644 --- a/packages/core/src/useEditor.ts +++ b/packages/core/src/useEditor.ts @@ -4,7 +4,7 @@ import { DependencyList } from "react"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import rootStyles from "./root.module.css"; -import { MenuFactories } from "./BlockNoteEditor"; +import { ElementFactories } from "./BlockNoteEditor"; type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; @@ -23,7 +23,7 @@ const blockNoteOptions = { * Main hook for importing a BlockNote editor into a react project */ export const useEditor = ( - menuFactories: MenuFactories, + elementFactories: ElementFactories, options: Partial = {}, deps: DependencyList = [] ) => { @@ -35,19 +35,27 @@ export const useEditor = ( extensions = extensions.map((extension) => { if (extension.name === "BubbleMenuExtension") { return extension.configure({ - bubbleMenuFactory: menuFactories.bubbleMenuFactory, + bubbleMenuFactory: elementFactories.bubbleMenuFactory, }); } if (extension.name === "link") { return extension.configure({ - hyperlinkMenuFactory: menuFactories.hyperlinkMenuFactory, + hyperlinkMenuFactory: elementFactories.hyperlinkMenuFactory, }); } if (extension.name === "slash-command") { return extension.configure({ - suggestionsMenuFactory: menuFactories.suggestionsMenuFactory, + suggestionsMenuFactory: elementFactories.suggestionsMenuFactory, + }); + } + + if (extension.name === "DraggableBlocksExtension") { + return extension.configure({ + addBlockButtonFactory: elementFactories.addBlockButtonFactory, + dragHandleFactory: elementFactories.dragHandleFactory, + dragHandleMenuFactory: elementFactories.dragHandleMenuFactory, }); } @@ -72,5 +80,6 @@ export const useEditor = ( }, }, }; + return useEditorTiptap(tiptapOptions, deps); }; diff --git a/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx b/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx new file mode 100644 index 0000000000..8b3180fb7b --- /dev/null +++ b/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx @@ -0,0 +1,70 @@ +import { + AddBlockButton, + AddBlockButtonFactory, + AddBlockButtonParams, +} from "@blocknote/core"; +import { AddBlockButtonProps } from "./components/AddBlockButton"; +import { AddBlockButton as ReactAddBlockButton } from "./components/AddBlockButton"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +import { MantineProvider } from "@mantine/core"; +import { createRoot, Root } from "react-dom/client"; +import Tippy from "@tippyjs/react"; + +export const ReactAddBlockButtonFactory: AddBlockButtonFactory = ( + params: AddBlockButtonParams +): AddBlockButton => { + const addBlockButtonProps: AddBlockButtonProps = { + ...params, + }; + + function updateAddBlockButtonProps(params: AddBlockButtonParams) { + addBlockButtonProps.addBlock = params.addBlock; + } + + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.blockBoundingBox} + hideOnClick={false} + interactive={true} + offset={[0, 24]} + placement={"left"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } + + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; + + return { + element: menuRootElement, + show: (params: AddBlockButtonParams) => { + updateAddBlockButtonProps(params); + + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); + + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot?.unmount(); + + menuRootElement.remove(); + }, + update: (_params: AddBlockButtonParams) => { + updateAddBlockButtonProps(params); + + menuRoot?.render(getMenuComponent()); + }, + }; +}; diff --git a/packages/react/src/AddBlockButton/components/AddBlockButton.tsx b/packages/react/src/AddBlockButton/components/AddBlockButton.tsx new file mode 100644 index 0000000000..7a4bbe5cba --- /dev/null +++ b/packages/react/src/AddBlockButton/components/AddBlockButton.tsx @@ -0,0 +1,14 @@ +import { ActionIcon } from "@mantine/core"; +import { AiOutlinePlus } from "react-icons/all"; + +export type AddBlockButtonProps = { + addBlock: () => void; +}; + +export const AddBlockButton = (props: AddBlockButtonProps) => { + return ( + + {} + + ); +}; diff --git a/packages/react/src/DragHandle/DragHandleFactory.tsx b/packages/react/src/DragHandle/DragHandleFactory.tsx new file mode 100644 index 0000000000..a2ce905400 --- /dev/null +++ b/packages/react/src/DragHandle/DragHandleFactory.tsx @@ -0,0 +1,58 @@ +import { + DragHandle, + DragHandleFactory, + DragHandleParams, +} from "@blocknote/core"; +import { DragHandle as ReactDragHandle } from "./components/DragHandle"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +import { MantineProvider } from "@mantine/core"; +import { createRoot, Root } from "react-dom/client"; +import Tippy from "@tippyjs/react"; + +export const ReactDragHandleFactory: DragHandleFactory = ( + params: DragHandleParams +): DragHandle => { + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + menuRootElement.style.position = "absolute"; + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; + + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.blockBoundingBox} + hideOnClick={false} + interactive={true} + offset={[0, 0]} + placement={"left"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } + + return { + element: menuRootElement, + show: (_params: DragHandleParams) => { + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); + + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot?.unmount(); + + menuRootElement.remove(); + }, + update: (_params: DragHandleParams) => { + menuRoot?.render(getMenuComponent()); + }, + }; +}; diff --git a/packages/react/src/DragHandle/components/DragHandle.tsx b/packages/react/src/DragHandle/components/DragHandle.tsx new file mode 100644 index 0000000000..c5c4bea973 --- /dev/null +++ b/packages/react/src/DragHandle/components/DragHandle.tsx @@ -0,0 +1,10 @@ +import { ActionIcon } from "@mantine/core"; +import { MdDragIndicator } from "react-icons/all"; + +export const DragHandle = () => { + return ( + + {} + + ); +}; diff --git a/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx b/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx new file mode 100644 index 0000000000..605ff608a8 --- /dev/null +++ b/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx @@ -0,0 +1,72 @@ +import { MantineProvider } from "@mantine/core"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +import { createRoot, Root } from "react-dom/client"; +import { + DragHandleMenu as ReactDragHandleMenu, + DragHandleMenuProps, +} from "./components/DragHandleMenu"; +import { + DragHandleMenuFactory, + DragHandleMenuParams, + DragHandleMenu, +} from "@blocknote/core"; +import Tippy from "@tippyjs/react"; + +export const ReactDragHandleMenuFactory: DragHandleMenuFactory = ( + params: DragHandleMenuParams +): DragHandleMenu => { + const dragHandleMenuProps: DragHandleMenuProps = { + ...params, + }; + + function updateDragHandleMenuProps(params: DragHandleMenuParams) { + dragHandleMenuProps.deleteBlock = params.deleteBlock; + } + + function getMenuComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.dragHandleBoundingBox} + hideOnClick={false} + interactive={true} + // offset={[24, 0]} + placement={"left"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } + + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other menu factories do the same. + const menuRootElement = document.createElement("div"); + // menuRootElement.className = rootStyles.bnRoot; + let menuRoot: Root | undefined; + + return { + element: menuRootElement, + show: (params: DragHandleMenuParams) => { + updateDragHandleMenuProps(params); + + document.body.appendChild(menuRootElement); + menuRoot = createRoot(menuRootElement); + + menuRoot.render(getMenuComponent()); + }, + hide: () => { + menuRoot?.unmount(); + + menuRootElement.remove(); + }, + update: (_params: DragHandleMenuParams) => { + updateDragHandleMenuProps(params); + + menuRoot?.render(getMenuComponent()); + }, + }; +}; diff --git a/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx b/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx new file mode 100644 index 0000000000..b8f8a9bc52 --- /dev/null +++ b/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx @@ -0,0 +1,24 @@ +import { createStyles, Menu } from "@mantine/core"; + +export type DragHandleMenuProps = { + deleteBlock: () => void; +}; + +export const DragHandleMenu = (props: DragHandleMenuProps) => { + const { classes } = createStyles({ root: {} })(undefined, { + name: "DragHandleMenu", + }); + + return ( +
+ + +
+
+ + Delete + +
+
+ ); +}; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b1649a8893..dc4253e378 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,5 +1,8 @@ // TODO: review directories +export * from "./AddBlockButton/AddBlockButtonFactory"; export * from "./BubbleMenu/BubbleMenuFactory"; +export * from "./DragHandle/DragHandleFactory"; +export * from "./DragHandleMenu/DragHandleMenuFactory"; export * from "./Editor/EditorContent"; export * from "./HyperlinkMenu/HyperlinkMenuFactory"; export * from "./SuggestionsMenu/SuggestionsMenuFactory"; From 4853a5c8434815741641721757fd1af2add5abd9 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 11 Jan 2023 00:40:11 +0100 Subject: [PATCH 30/55] Minor improvements --- ...nuExtension.tsx => BubbleMenuExtension.ts} | 0 .../extensions/BubbleMenu/BubbleMenuPlugin.ts | 2 +- .../{HyperlinkMark.tsx => HyperlinkMark.ts} | 0 ...kMenuPlugin.tsx => HyperlinkMenuPlugin.ts} | 29 +++++++------- .../plugins/suggestion/SuggestionPlugin.ts | 2 +- .../suggestion/SuggestionsMenuFactoryTypes.ts | 6 +-- .../HyperlinkMenu/HyperlinkMenuFactory.tsx | 38 +++++++++---------- .../components/HyperlinkMenu.tsx | 8 ++-- 8 files changed, 41 insertions(+), 44 deletions(-) rename packages/core/src/extensions/BubbleMenu/{BubbleMenuExtension.tsx => BubbleMenuExtension.ts} (100%) rename packages/core/src/extensions/Hyperlinks/{HyperlinkMark.tsx => HyperlinkMark.ts} (100%) rename packages/core/src/extensions/Hyperlinks/{HyperlinkMenuPlugin.tsx => HyperlinkMenuPlugin.ts} (91%) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts similarity index 100% rename from packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.tsx rename to packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index f8b9a711ee..f8772e5617 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -237,7 +237,7 @@ export class BubbleMenuView { return posToDOMRect(this.editor.view, from, to); } - initBubbleMenuParams() { + initBubbleMenuParams(): BubbleMenuParams { return { boldIsActive: this.editor.isActive("bold"), toggleBold: () => { diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts similarity index 100% rename from packages/core/src/extensions/Hyperlinks/HyperlinkMark.tsx rename to packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts similarity index 91% rename from packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx rename to packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts index 3c4fac9d93..8a234a7ce1 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.tsx +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts @@ -2,26 +2,26 @@ import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; import { - HyperlinkHoverMenu, - HyperlinkHoverMenuFactory, - HyperlinkHoverMenuParams, + HyperlinkMenu, + HyperlinkMenuFactory, + HyperlinkMenuParams, } from "./HyperlinkMenuFactoryTypes"; const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); export type HyperlinkMenuPluginProps = { - hyperlinkMenuFactory: HyperlinkHoverMenuFactory; + hyperlinkMenuFactory: HyperlinkMenuFactory; }; export type HyperlinkHoverMenuViewProps = { editor: Editor; - hyperlinkHoverMenuFactory: HyperlinkHoverMenuFactory; + hyperlinkHoverMenuFactory: HyperlinkMenuFactory; }; class HyperlinkHoverMenuView { editor: Editor; - hyperlinkHoverMenuParams: HyperlinkHoverMenuParams; - hyperlinkHoverMenu: HyperlinkHoverMenu; + hyperlinkHoverMenuParams: HyperlinkMenuParams; + hyperlinkHoverMenu: HyperlinkMenu; menuUpdateTimer: NodeJS.Timeout | undefined; startMenuUpdateTimer: () => void; @@ -191,10 +191,10 @@ class HyperlinkHoverMenuView { } } - initHyperlinkHoverMenuParams() { + initHyperlinkHoverMenuParams(): HyperlinkMenuParams { return { - hyperlinkUrl: "", - hyperlinkText: "", + url: "", + text: "", editHyperlink: (url: string, text: string) => { const tr = this.editor.view.state.tr.insertText( text, @@ -226,16 +226,15 @@ class HyperlinkHoverMenuView { this.hyperlinkHoverMenu.hide(); }, - hyperlinkBoundingBox: new DOMRect(), + boundingBox: new DOMRect(), editorElement: this.editor.options.element, }; } updateHyperlinkHoverMenuParams() { if (this.hyperlinkMark) { - this.hyperlinkHoverMenuParams.hyperlinkUrl = - this.hyperlinkMark.attrs.href; - this.hyperlinkHoverMenuParams.hyperlinkText = + this.hyperlinkHoverMenuParams.url = this.hyperlinkMark.attrs.href; + this.hyperlinkHoverMenuParams.text = this.editor.view.state.doc.textBetween( this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to @@ -243,7 +242,7 @@ class HyperlinkHoverMenuView { } if (this.hyperlinkMarkRange) { - this.hyperlinkHoverMenuParams.hyperlinkBoundingBox = posToDOMRect( + this.hyperlinkHoverMenuParams.boundingBox = posToDOMRect( this.editor.view, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 5d45b452ba..aaa26e2a0c 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -162,7 +162,7 @@ class SuggestionPluginView { } } - initSuggestionsMenuParams() { + initSuggestionsMenuParams(): SuggestionsMenuParams { return { items: [], selectedItemIndex: 0, diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts index 3fe73adc4d..2a7e736f64 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts @@ -1,5 +1,5 @@ import { SuggestionItem } from "./SuggestionItem"; -import { Menu, MenuFactory } from "../../../MenuFactoryTypes"; +import { EditorElement, ElementFactory } from "../../../EditorElement"; export type SuggestionsMenuParams = { items: T[]; @@ -10,9 +10,9 @@ export type SuggestionsMenuParams = { editorElement: Element; }; -export type SuggestionsMenu = Menu< +export type SuggestionsMenu = EditorElement< SuggestionsMenuParams >; -export type SuggestionsMenuFactory = MenuFactory< +export type SuggestionsMenuFactory = ElementFactory< SuggestionsMenuParams >; diff --git a/packages/react/src/HyperlinkMenu/HyperlinkMenuFactory.tsx b/packages/react/src/HyperlinkMenu/HyperlinkMenuFactory.tsx index 396bd02401..5596ebefea 100644 --- a/packages/react/src/HyperlinkMenu/HyperlinkMenuFactory.tsx +++ b/packages/react/src/HyperlinkMenu/HyperlinkMenuFactory.tsx @@ -1,28 +1,26 @@ import { createRoot, Root } from "react-dom/client"; import { - HyperlinkHoverMenu, - HyperlinkHoverMenuFactory, - HyperlinkHoverMenuParams, + HyperlinkMenu, + HyperlinkMenuFactory, + HyperlinkMenuParams, } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; import Tippy from "@tippyjs/react"; -import { HyperlinkMenu, HyperlinkMenuProps } from "./components/HyperlinkMenu"; +import { + HyperlinkMenu as ReactHyperlinkMenu, + HyperlinkMenuProps, +} from "./components/HyperlinkMenu"; import { BlockNoteTheme } from "../BlockNoteTheme"; // import rootStyles from "../../../core/src/root.module.css"; -export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( - params: HyperlinkHoverMenuParams -): HyperlinkHoverMenu => { - const hyperlinkMenuProps: HyperlinkMenuProps = { - url: params.hyperlinkUrl, - text: params.hyperlinkText, - update: params.editHyperlink, - remove: params.deleteHyperlink, - }; +export const ReactHyperlinkMenuFactory: HyperlinkMenuFactory = ( + params: HyperlinkMenuParams +): HyperlinkMenu => { + const hyperlinkMenuProps: HyperlinkMenuProps = { ...params }; - function updateHyperlinkMenuProps(params: HyperlinkHoverMenuParams) { - hyperlinkMenuProps.url = params.hyperlinkUrl; - hyperlinkMenuProps.text = params.hyperlinkText; + function updateHyperlinkMenuProps(params: HyperlinkMenuParams) { + hyperlinkMenuProps.url = params.url; + hyperlinkMenuProps.text = params.text; } // We don't use the document body as a root as it would cause multiple React roots to be created on a single element @@ -36,9 +34,9 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( } + content={} duration={0} - getReferenceClientRect={() => params.hyperlinkBoundingBox} + getReferenceClientRect={() => params.boundingBox} hideOnClick={false} interactive={true} placement={"top"} @@ -51,7 +49,7 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( return { element: menuRootElement, - show: (params: HyperlinkHoverMenuParams) => { + show: (params: HyperlinkMenuParams) => { updateHyperlinkMenuProps(params); document.body.appendChild(menuRootElement); @@ -64,7 +62,7 @@ export const ReactHyperlinkMenuFactory: HyperlinkHoverMenuFactory = ( menuRootElement.remove(); }, - update: (params: HyperlinkHoverMenuParams) => { + update: (params: HyperlinkMenuParams) => { updateHyperlinkMenuProps(params); menuRoot!.render(getMenuComponent()); diff --git a/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx b/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx index 6dc305d142..67b32ce0f4 100644 --- a/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx @@ -8,8 +8,8 @@ import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; export type HyperlinkMenuProps = { url: string; text: string; - update: (url: string, text: string) => void; - remove: () => void; + editHyperlink: (url: string, text: string) => void; + deleteHyperlink: () => void; }; /** @@ -24,7 +24,7 @@ export const HyperlinkMenu = (props: HyperlinkMenuProps) => { ); } @@ -48,7 +48,7 @@ export const HyperlinkMenu = (props: HyperlinkMenuProps) => { From 496c020effe98c2c892a2892245848a3e04c6140 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 11 Jan 2023 10:03:15 +0100 Subject: [PATCH 31/55] fix lint --- package-lock.json | 1 + packages/core/package.json | 2 -- packages/react/package.json | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 47637d1ec5..d4880954af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13729,6 +13729,7 @@ } }, "packages/react": { + "name": "@blocknote/react", "version": "0.1.2", "dependencies": { "@blocknote/core": "^0.1.2", diff --git a/packages/core/package.json b/packages/core/package.json index 8e572620d0..260e1175d2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -74,8 +74,6 @@ }, "devDependencies": { "@types/lodash": "^4.14.179", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.9", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", diff --git a/packages/react/package.json b/packages/react/package.json index 9f8a787c98..a7606610df 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -23,7 +23,7 @@ "build": "tsc && vite build", "build-bundled": "tsc && vite build --config vite.config.bundled.ts && git checkout tmp-releases && rm -rf ../../release && mv ../../release-tmp ../../release", "preview": "vite preview", - "lint": "eslint ../menus/src --max-warnings 0" + "lint": "eslint src --max-warnings 0" }, "dependencies": { "@mantine/core": "^5.6.1", From 3f88072a5fbb8b4d36c5deed7afd9acf59f14f32 Mon Sep 17 00:00:00 2001 From: Yousef Date: Wed, 11 Jan 2023 14:05:17 +0100 Subject: [PATCH 32/55] refactor entrypoints (#77) * refactor entrypoints * fix packages * fix build * move some more files / remove root.module.css --- examples/editor/src/App.tsx | 42 ++---- package-lock.json | 22 ++- packages/core/package.json | 1 - packages/core/src/BlockNoteEditor.ts | 139 +++++++++++------- packages/core/src/EditorContent.tsx | 2 - packages/core/src/assets/fonts-inter.css | 92 ++++++++++++ packages/core/src/editor.module.css | 20 +++ .../BubbleMenu/BubbleMenuFactoryTypes.ts | 2 +- .../DraggableBlocks/DragMenuFactoryTypes.ts | 2 +- .../Hyperlinks/HyperlinkMenuFactoryTypes.ts | 2 +- .../src/extensions/SlashMenu/SlashMenuItem.ts | 2 - .../extensions/SlashMenu/defaultCommands.tsx | 16 +- packages/core/src/fonts-inter.css | 94 ------------ packages/core/src/globals.css | 28 ---- packages/core/src/index.ts | 6 +- packages/core/src/root.module.css | 19 --- .../core/src/{ => shared}/EditorElement.ts | 0 .../plugins/suggestion/SuggestionItem.ts | 7 - .../suggestion/SuggestionsMenuFactoryTypes.ts | 2 +- packages/core/src/{ => shared}/utils.ts | 0 packages/core/src/useEditor.ts | 85 ----------- packages/core/vite.config.bundled.ts | 4 +- packages/react/package.json | 3 +- packages/react/src/BlockNoteView.tsx | 6 + packages/react/src/Editor/useEditor.ts | 51 ------- .../components/SuggestionList.tsx | 8 +- .../components/SuggestionListItem.tsx | 36 ++++- packages/react/src/hooks/useBlockNote.ts | 68 +++++++++ .../src}/hooks/useEditorForceUpdate.tsx | 0 packages/react/src/index.ts | 4 +- 30 files changed, 341 insertions(+), 422 deletions(-) delete mode 100644 packages/core/src/EditorContent.tsx create mode 100644 packages/core/src/assets/fonts-inter.css delete mode 100644 packages/core/src/fonts-inter.css delete mode 100644 packages/core/src/globals.css delete mode 100644 packages/core/src/root.module.css rename packages/core/src/{ => shared}/EditorElement.ts (100%) rename packages/core/src/{ => shared}/utils.ts (100%) delete mode 100644 packages/core/src/useEditor.ts create mode 100644 packages/react/src/BlockNoteView.tsx delete mode 100644 packages/react/src/Editor/useEditor.ts create mode 100644 packages/react/src/hooks/useBlockNote.ts rename packages/{core/src/shared => react/src}/hooks/useEditorForceUpdate.tsx (100%) diff --git a/examples/editor/src/App.tsx b/examples/editor/src/App.tsx index 65fae001d7..1c6c5c7f4e 100644 --- a/examples/editor/src/App.tsx +++ b/examples/editor/src/App.tsx @@ -1,45 +1,27 @@ // import logo from './logo.svg' -import { EditorContent, useEditor } from "@blocknote/core"; import "@blocknote/core/style.css"; +import { BlockNoteView, useBlockNote } from "@blocknote/react"; import { Editor } from "@tiptap/core"; import styles from "./App.module.css"; -import { - ReactAddBlockButtonFactory, - ReactBubbleMenuFactory, - ReactDragHandleFactory, - ReactDragHandleMenuFactory, - ReactHyperlinkMenuFactory, - ReactSuggestionsMenuFactory, -} from "@blocknote/react"; type WindowWithProseMirror = Window & typeof globalThis & { ProseMirror: Editor }; function App() { - const editor = useEditor( - { - addBlockButtonFactory: ReactAddBlockButtonFactory, - bubbleMenuFactory: ReactBubbleMenuFactory, - dragHandleFactory: ReactDragHandleFactory, - dragHandleMenuFactory: ReactDragHandleMenuFactory, - hyperlinkMenuFactory: ReactHyperlinkMenuFactory, - suggestionsMenuFactory: ReactSuggestionsMenuFactory, + const editor = useBlockNote({ + onUpdate: ({ editor }) => { + console.log(editor.getJSON()); + (window as WindowWithProseMirror).ProseMirror = editor; // Give tests a way to get editor instance }, - { - onUpdate: ({ editor }) => { - console.log(editor.getJSON()); - (window as WindowWithProseMirror).ProseMirror = editor; // Give tests a way to get editor instance + editorProps: { + attributes: { + class: styles.editor, + "data-test": "editor", }, - editorProps: { - attributes: { - class: styles.editor, - "data-test": "editor", - }, - }, - } - ); + }, + }); - return ; + return ; } export default App; diff --git a/package-lock.json b/package-lock.json index d4880954af..2af04677d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11669,9 +11669,9 @@ } }, "node_modules/react-icons": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz", - "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz", + "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==", "peerDependencies": { "react": "*" } @@ -13712,13 +13712,10 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react-icons": "^4.3.1", "uuid": "^8.3.2" }, "devDependencies": { "@types/lodash": "^4.14.179", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.9", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", @@ -13735,7 +13732,8 @@ "@blocknote/core": "^0.1.2", "@mantine/core": "^5.6.1", "@tippyjs/react": "^4.2.6", - "@tiptap/react": "^2.0.0-beta.207" + "@tiptap/react": "^2.0.0-beta.207", + "react-icons": "^4.3.1" }, "devDependencies": { "@types/react": "^18.0.25", @@ -15172,8 +15170,6 @@ "@tiptap/extension-text": "^2.0.0-beta.17", "@tiptap/extension-underline": "^2.0.0-beta.25", "@types/lodash": "^4.14.179", - "@types/react": "^18.0.25", - "@types/react-dom": "^18.0.9", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", @@ -15182,7 +15178,6 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react-icons": "^4.3.1", "typescript": "^4.5.4", "uuid": "^8.3.2", "vite": "^3.0.5", @@ -15219,6 +15214,7 @@ "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", "prettier": "^2.7.1", + "react-icons": "^4.3.1", "typescript": "^4.5.4", "vite": "^3.0.5", "vite-plugin-eslint": "^1.7.0" @@ -22391,9 +22387,9 @@ } }, "react-icons": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz", - "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.7.1.tgz", + "integrity": "sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw==", "requires": {} }, "react-refresh": { diff --git a/packages/core/package.json b/packages/core/package.json index 260e1175d2..df48c0880b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -69,7 +69,6 @@ "prosemirror-model": "1.18.1", "prosemirror-state": "1.4.1", "prosemirror-view": "1.26.2", - "react-icons": "^4.3.1", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 73d4bc034c..a427625f94 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -3,20 +3,26 @@ import { Editor, EditorOptions } from "@tiptap/core"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import { BubbleMenuFactory } from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; -import { HyperlinkHoverMenuFactory } from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; -import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; -import rootStyles from "./root.module.css"; +import { + AddBlockButtonFactory, + DragHandleFactory, + DragHandleMenuFactory, +} from "./extensions/DraggableBlocks/DragMenuFactoryTypes"; +import { HyperlinkMenuFactory } from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; +import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; -type BlockNoteEditorOptions = EditorOptions & { +export type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; disableHistoryExtension: boolean; -}; - -export type MenuFactories = { - bubbleMenuFactory: BubbleMenuFactory; - hyperlinkMenuFactory: HyperlinkHoverMenuFactory; - suggestionsMenuFactory: SuggestionsMenuFactory; + uiFactories: { + bubbleMenuFactory: BubbleMenuFactory; + hyperlinkMenuFactory: HyperlinkMenuFactory; + suggestionsMenuFactory: SuggestionsMenuFactory; + addBlockButtonFactory: AddBlockButtonFactory; + dragHandleFactory: DragHandleFactory; + dragHandleMenuFactory: DragHandleMenuFactory; + }; }; const blockNoteExtensions = getBlockNoteExtensions(); @@ -27,55 +33,78 @@ const blockNoteOptions = { enableCoreExtensions: false, }; -export const mountBlockNoteEditor = ( - menuFactories: MenuFactories, - options: Partial = {} -) => { - let extensions = options.disableHistoryExtension - ? blockNoteExtensions.filter((e) => e.name !== "history") - : blockNoteExtensions; +export class BlockNoteEditor { + public readonly tiptapEditor: Editor & { contentComponent: any }; - // TODO: review - extensions = extensions.map((extension) => { - if (extension.name === "BubbleMenuExtension") { - return extension.configure({ - bubbleMenuFactory: menuFactories.bubbleMenuFactory, - }); - } + constructor(options: Partial = {}) { + let extensions = options.disableHistoryExtension + ? blockNoteExtensions.filter((e) => e.name !== "history") + : blockNoteExtensions; - if (extension.name === "link") { - return extension.configure({ - hyperlinkMenuFactory: menuFactories.hyperlinkMenuFactory, - }); - } + // TODO: review + extensions = extensions.map((extension) => { + if ( + extension.name === "BubbleMenuExtension" && + options.uiFactories?.bubbleMenuFactory + ) { + return extension.configure({ + bubbleMenuFactory: options.uiFactories.bubbleMenuFactory, + }); + } - if (extension.name === "slash-command") { - return extension.configure({ - suggestionsMenuFactory: menuFactories.suggestionsMenuFactory, - }); - } + if ( + extension.name === "link" && + options.uiFactories?.hyperlinkMenuFactory + ) { + return extension.configure({ + hyperlinkMenuFactory: options.uiFactories.hyperlinkMenuFactory, + }); + } - return extension; - }); + if ( + extension.name === "slash-command" && + options.uiFactories?.suggestionsMenuFactory + ) { + return extension.configure({ + suggestionsMenuFactory: options.uiFactories.suggestionsMenuFactory, + }); + } - const tiptapOptions = { - ...blockNoteOptions, - ...options, - extensions: - options.enableBlockNoteExtensions === false - ? options.extensions - : [...(options.extensions || []), ...extensions], - editorProps: { - attributes: { - ...(options.editorProps?.attributes || {}), - class: [ - styles.bnEditor, - rootStyles.bnRoot, - (options.editorProps?.attributes as any)?.class || "", - ].join(" "), + if ( + extension.name === "DraggableBlocksExtension" && + options.uiFactories + ) { + return extension.configure({ + addBlockButtonFactory: options.uiFactories.addBlockButtonFactory, + dragHandleFactory: options.uiFactories.dragHandleFactory, + dragHandleMenuFactory: options.uiFactories.dragHandleMenuFactory, + }); + } + + return extension; + }); + + const tiptapOptions = { + ...blockNoteOptions, + ...options, + extensions: + options.enableBlockNoteExtensions === false + ? options.extensions + : [...(options.extensions || []), ...extensions], + editorProps: { + attributes: { + ...(options.editorProps?.attributes || {}), + class: [ + styles.bnEditor, + styles.bnRoot, + (options.editorProps?.attributes as any)?.class || "", + ].join(" "), + }, }, - }, - }; + }; - return new Editor(tiptapOptions); -}; + this.tiptapEditor = new Editor(tiptapOptions) as Editor & { + contentComponent: any; + }; + } +} diff --git a/packages/core/src/EditorContent.tsx b/packages/core/src/EditorContent.tsx deleted file mode 100644 index 7bcccbc6cd..0000000000 --- a/packages/core/src/EditorContent.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// BlockNote uses a similar pattern as Tiptap, so for now we can just export that -export { EditorContent } from "@tiptap/react"; diff --git a/packages/core/src/assets/fonts-inter.css b/packages/core/src/assets/fonts-inter.css new file mode 100644 index 0000000000..a074dfe49d --- /dev/null +++ b/packages/core/src/assets/fonts-inter.css @@ -0,0 +1,92 @@ +/* Generated using https://google-webfonts-helper.herokuapp.com/fonts/inter?subsets=latin */ + +/* inter-100 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 100; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-100.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-100.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-200 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 200; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-200.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-200.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-300 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 300; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-300.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-300.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-regular - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-regular.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-regular.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-500 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 500; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-500.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-600 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 600; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-600.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-700 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 700; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-700.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-800 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 800; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-800.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-800.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} +/* inter-900 - latin */ +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 900; + src: local(""), + url("./inter-v12-latin/inter-v12-latin-900.woff2") format("woff2"), + /* Chrome 26+, Opera 23+, Firefox 39+ */ + url("./inter-v12-latin/inter-v12-latin-900.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ +} diff --git a/packages/core/src/editor.module.css b/packages/core/src/editor.module.css index 78fa284a5a..47296e8811 100644 --- a/packages/core/src/editor.module.css +++ b/packages/core/src/editor.module.css @@ -1,3 +1,23 @@ .bnEditor { outline: none; } + +/* +bnRoot should be applied to all top-level elements + +This includes the Prosemirror editor, but also
element such as +Tippy popups that are appended to document.body directly +*/ +.bnRoot { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +.bnRoot *, +.bnRoot *::before, +.bnRoot *::after { + -webkit-box-sizing: inherit; + -moz-box-sizing: inherit; + box-sizing: inherit; +} diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts index 36fdbe7728..3fb9bbec8b 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts @@ -1,4 +1,4 @@ -import { EditorElement, ElementFactory } from "../../EditorElement"; +import { EditorElement, ElementFactory } from "../../shared/EditorElement"; export type BubbleMenuParams = { boldIsActive: boolean; diff --git a/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts index 715950f48c..96668e35eb 100644 --- a/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts +++ b/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts @@ -1,4 +1,4 @@ -import { EditorElement, ElementFactory } from "../../EditorElement"; +import { EditorElement, ElementFactory } from "../../shared/EditorElement"; export type AddBlockButtonParams = { addBlock: () => void; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts index efa75ed20c..3d45862a67 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts @@ -1,4 +1,4 @@ -import { EditorElement, ElementFactory } from "../../EditorElement"; +import { EditorElement, ElementFactory } from "../../shared/EditorElement"; export type HyperlinkMenuParams = { url: string; diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts b/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts index 76e76da6ce..576038b894 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuItem.ts @@ -1,5 +1,4 @@ import { Editor, Range } from "@tiptap/core"; -import { IconType } from "react-icons"; import { SuggestionItem } from "../../shared/plugins/suggestion/SuggestionItem"; export type SlashMenuCallback = (editor: Editor, range: Range) => boolean; @@ -40,7 +39,6 @@ export class SlashMenuItem implements SuggestionItem { public readonly group: SlashMenuGroups, public readonly execute: SlashMenuCallback, public readonly aliases: string[] = [], - public readonly icon?: IconType, public readonly hint?: string, public readonly shortcut?: string ) { diff --git a/packages/core/src/extensions/SlashMenu/defaultCommands.tsx b/packages/core/src/extensions/SlashMenu/defaultCommands.tsx index f5024a56cb..cbd4c609e4 100644 --- a/packages/core/src/extensions/SlashMenu/defaultCommands.tsx +++ b/packages/core/src/extensions/SlashMenu/defaultCommands.tsx @@ -1,12 +1,4 @@ -import { - RiH1, - RiH2, - RiH3, - RiListOrdered, - RiListUnordered, - RiText, -} from "react-icons/ri"; -import { formatKeyboardShortcut } from "../../utils"; +import { formatKeyboardShortcut } from "../../shared/utils"; import { SlashMenuGroups, SlashMenuItem } from "./SlashMenuItem"; /** @@ -28,7 +20,6 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .run(); }, ["h", "heading1", "h1"], - RiH1, "Used for a top-level heading", formatKeyboardShortcut("Mod-Alt-1") ), @@ -48,7 +39,6 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .run(); }, ["h2", "heading2", "subheading"], - RiH2, "Used for key sections", formatKeyboardShortcut("Mod-Alt-2") ), @@ -68,7 +58,6 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .run(); }, ["h3", "heading3", "subheading"], - RiH3, "Used for subsections and group headings", formatKeyboardShortcut("Mod-Alt-3") ), @@ -88,7 +77,6 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .run(); }, ["li", "list", "numberedlist", "numbered list"], - RiListOrdered, "Used to display a numbered list", formatKeyboardShortcut("Mod-Shift-7") ), @@ -108,7 +96,6 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .run(); }, ["ul", "list", "bulletlist", "bullet list"], - RiListUnordered, "Used to display an unordered list", formatKeyboardShortcut("Mod-Shift-8") ), @@ -126,7 +113,6 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .run(); }, ["p"], - RiText, "Used for the body of your document", formatKeyboardShortcut("Mod-Alt-0") ), diff --git a/packages/core/src/fonts-inter.css b/packages/core/src/fonts-inter.css deleted file mode 100644 index 8a2be5de51..0000000000 --- a/packages/core/src/fonts-inter.css +++ /dev/null @@ -1,94 +0,0 @@ -/* Generated using https://google-webfonts-helper.herokuapp.com/fonts/inter?subsets=latin */ - -/* inter-100 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 100; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-100.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-100.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-200 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 200; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-200.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-200.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-300 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 300; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-300.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-300.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-regular - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 400; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-regular.woff2") - format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-regular.woff") - format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-500 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 500; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-500.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-500.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-600 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 600; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-600.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-600.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-700 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 700; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-700.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-700.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-800 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 800; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-800.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-800.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} -/* inter-900 - latin */ -@font-face { - font-family: "Inter"; - font-style: normal; - font-weight: 900; - src: local(""), - url("./assets/inter-v12-latin/inter-v12-latin-900.woff2") format("woff2"), - /* Chrome 26+, Opera 23+, Firefox 39+ */ - url("./assets/inter-v12-latin/inter-v12-latin-900.woff") format("woff"); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} diff --git a/packages/core/src/globals.css b/packages/core/src/globals.css deleted file mode 100644 index 636c561a70..0000000000 --- a/packages/core/src/globals.css +++ /dev/null @@ -1,28 +0,0 @@ -@import url("fonts-inter.css"); - -/* TODO: should not be on root as this changes entire consuming application */ - -:root { - /* Define a set of colors to be used throughout the app for consistency - see https://atlassian.design/foundations/color for more info */ - --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */ - --N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */ - - font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, - "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", - "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - color: rgb(60, 65, 73); -} - -button { - font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, - "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", - "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - - color: rgb(60, 65, 73); -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f8c5c1ad27..1d9d832763 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,11 +1,7 @@ -import "./globals.css"; - export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; -export * from "./EditorContent"; export * from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; export * from "./extensions/DraggableBlocks/DragMenuFactoryTypes"; export * from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; -export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; -export * from "./useEditor"; +export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; diff --git a/packages/core/src/root.module.css b/packages/core/src/root.module.css deleted file mode 100644 index e2f9877bad..0000000000 --- a/packages/core/src/root.module.css +++ /dev/null @@ -1,19 +0,0 @@ -/* -bnRoot should be applied to all top-level elements - -This includes the Prosemirror editor, but also
element such as -Tippy popups that are appended to document.body directly -*/ -.bnRoot { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.bnRoot *, -.bnRoot *::before, -.bnRoot *::after { - -webkit-box-sizing: inherit; - -moz-box-sizing: inherit; - box-sizing: inherit; -} diff --git a/packages/core/src/EditorElement.ts b/packages/core/src/shared/EditorElement.ts similarity index 100% rename from packages/core/src/EditorElement.ts rename to packages/core/src/shared/EditorElement.ts diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts b/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts index 688aaea88e..837089e32b 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionItem.ts @@ -1,5 +1,3 @@ -import { IconType } from "react-icons"; - /** * A generic interface used in all suggestion menus (slash menu, mentions, etc) */ @@ -14,11 +12,6 @@ export interface SuggestionItem { */ groupName: string; - /** - * The react icon - */ - icon?: IconType; - hint?: string; shortcut?: string; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts index 2a7e736f64..dd8b1399ef 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts @@ -1,5 +1,5 @@ +import { EditorElement, ElementFactory } from "../../EditorElement"; import { SuggestionItem } from "./SuggestionItem"; -import { EditorElement, ElementFactory } from "../../../EditorElement"; export type SuggestionsMenuParams = { items: T[]; diff --git a/packages/core/src/utils.ts b/packages/core/src/shared/utils.ts similarity index 100% rename from packages/core/src/utils.ts rename to packages/core/src/shared/utils.ts diff --git a/packages/core/src/useEditor.ts b/packages/core/src/useEditor.ts deleted file mode 100644 index cddb6e5219..0000000000 --- a/packages/core/src/useEditor.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react"; - -import { DependencyList } from "react"; -import { getBlockNoteExtensions } from "./BlockNoteExtensions"; -import styles from "./editor.module.css"; -import rootStyles from "./root.module.css"; -import { ElementFactories } from "./BlockNoteEditor"; - -type BlockNoteEditorOptions = EditorOptions & { - enableBlockNoteExtensions: boolean; - disableHistoryExtension: boolean; -}; - -const blockNoteExtensions = getBlockNoteExtensions(); - -const blockNoteOptions = { - enableInputRules: true, - enablePasteRules: true, - enableCoreExtensions: false, -}; - -/** - * Main hook for importing a BlockNote editor into a react project - */ -export const useEditor = ( - elementFactories: ElementFactories, - options: Partial = {}, - deps: DependencyList = [] -) => { - let extensions = options.disableHistoryExtension - ? blockNoteExtensions.filter((e) => e.name !== "history") - : blockNoteExtensions; - - // TODO: review - extensions = extensions.map((extension) => { - if (extension.name === "BubbleMenuExtension") { - return extension.configure({ - bubbleMenuFactory: elementFactories.bubbleMenuFactory, - }); - } - - if (extension.name === "link") { - return extension.configure({ - hyperlinkMenuFactory: elementFactories.hyperlinkMenuFactory, - }); - } - - if (extension.name === "slash-command") { - return extension.configure({ - suggestionsMenuFactory: elementFactories.suggestionsMenuFactory, - }); - } - - if (extension.name === "DraggableBlocksExtension") { - return extension.configure({ - addBlockButtonFactory: elementFactories.addBlockButtonFactory, - dragHandleFactory: elementFactories.dragHandleFactory, - dragHandleMenuFactory: elementFactories.dragHandleMenuFactory, - }); - } - - return extension; - }); - - const tiptapOptions = { - ...blockNoteOptions, - ...options, - extensions: - options.enableBlockNoteExtensions === false - ? options.extensions - : [...(options.extensions || []), ...extensions], - editorProps: { - attributes: { - ...(options.editorProps?.attributes || {}), - class: [ - styles.bnEditor, - rootStyles.bnRoot, - (options.editorProps?.attributes as any)?.class || "", - ].join(" "), - }, - }, - }; - - return useEditorTiptap(tiptapOptions, deps); -}; diff --git a/packages/core/vite.config.bundled.ts b/packages/core/vite.config.bundled.ts index 9a6ebab09c..7ca00d3dd8 100644 --- a/packages/core/vite.config.bundled.ts +++ b/packages/core/vite.config.bundled.ts @@ -21,8 +21,8 @@ export default defineConfig({ // Provide global variables to use in the UMD build // for externalized deps globals: { - react: "React", - "react-dom": "ReactDOM", + // react: "React", + // "react-dom": "ReactDOM", }, }, }, diff --git a/packages/react/package.json b/packages/react/package.json index a7606610df..8f22317a2b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -29,7 +29,8 @@ "@mantine/core": "^5.6.1", "@blocknote/core": "^0.1.2", "@tippyjs/react": "^4.2.6", - "@tiptap/react": "^2.0.0-beta.207" + "@tiptap/react": "^2.0.0-beta.207", + "react-icons": "^4.3.1" }, "devDependencies": { "@types/react": "^18.0.25", diff --git a/packages/react/src/BlockNoteView.tsx b/packages/react/src/BlockNoteView.tsx new file mode 100644 index 0000000000..0c7a5e176b --- /dev/null +++ b/packages/react/src/BlockNoteView.tsx @@ -0,0 +1,6 @@ +import { BlockNoteEditor } from "@blocknote/core"; +import { EditorContent } from "@tiptap/react"; + +export function BlockNoteView(props: { editor: BlockNoteEditor | null }) { + return ; +} diff --git a/packages/react/src/Editor/useEditor.ts b/packages/react/src/Editor/useEditor.ts deleted file mode 100644 index 9dc75db22b..0000000000 --- a/packages/react/src/Editor/useEditor.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { EditorOptions, useEditor as useEditorTiptap } from "@tiptap/react"; - -import { getBlockNoteExtensions } from "@blocknote/core"; -import { DependencyList } from "react"; -// import styles from "../../../core/src/editor.module.css"; -// import rootStyles from "../../../core/src/root.module.css"; - -type BlockNoteEditorOptions = EditorOptions & { - enableBlockNoteExtensions: boolean; - disableHistoryExtension: boolean; -}; - -const blockNoteExtensions = getBlockNoteExtensions(); - -const blockNoteOptions = { - enableInputRules: true, - enablePasteRules: true, - enableCoreExtensions: false, -}; - -/** - * Main hook for importing a BlockNote editor into a react project - */ -export const useEditor = ( - options: Partial = {}, - deps: DependencyList = [] -) => { - const extensions = options.disableHistoryExtension - ? blockNoteExtensions.filter((e) => e.name !== "history") - : blockNoteExtensions; - - const tiptapOptions = { - ...blockNoteOptions, - ...options, - extensions: - options.enableBlockNoteExtensions === false - ? options.extensions - : [...(options.extensions || []), ...extensions], - editorProps: { - attributes: { - ...(options.editorProps?.attributes || {}), - class: [ - // styles.bnEditor, - // rootStyles.bnRoot, - (options.editorProps?.attributes as any)?.class || "", - ].join(" "), - }, - }, - }; - return useEditorTiptap(tiptapOptions, deps); -}; diff --git a/packages/react/src/SuggestionsMenu/components/SuggestionList.tsx b/packages/react/src/SuggestionsMenu/components/SuggestionList.tsx index 0e701b5db5..e4fd8e04dd 100644 --- a/packages/react/src/SuggestionsMenu/components/SuggestionList.tsx +++ b/packages/react/src/SuggestionsMenu/components/SuggestionList.tsx @@ -15,8 +15,8 @@ export function SuggestionList( name: "SuggestionList", }); - const headingGroup = []; - const basicBlockGroup = []; + const headingGroup: T[] = []; + const basicBlockGroup: T[] = []; for (const item of props.items) { if (item.name === "Heading") { @@ -44,7 +44,7 @@ export function SuggestionList( } } - const renderedItems = []; + const renderedItems: any[] = []; let index = 0; if (headingGroup.length > 0) { @@ -58,7 +58,6 @@ export function SuggestionList( key={item.name} name={item.name} hint={item.hint} - icon={item.icon} isSelected={props.selectedItemIndex === index} set={() => props.itemCallback(item)} /> @@ -78,7 +77,6 @@ export function SuggestionList( key={item.name} name={item.name} hint={item.hint} - icon={item.icon} isSelected={props.selectedItemIndex === index} set={() => props.itemCallback(item)} /> diff --git a/packages/react/src/SuggestionsMenu/components/SuggestionListItem.tsx b/packages/react/src/SuggestionsMenu/components/SuggestionListItem.tsx index 31a5cbf61c..7a7520d492 100644 --- a/packages/react/src/SuggestionsMenu/components/SuggestionListItem.tsx +++ b/packages/react/src/SuggestionsMenu/components/SuggestionListItem.tsx @@ -3,12 +3,20 @@ import { Badge, createStyles, Menu, Stack, Text } from "@mantine/core"; import { useEffect, useRef } from "react"; import { IconType } from "react-icons"; +import { + RiH1, + RiH2, + RiH3, + RiListOrdered, + RiListUnordered, + RiText, +} from "react-icons/ri"; + const MIN_LEFT_MARGIN = 5; export type SuggestionGroupItemProps = { name: string; hint: string | undefined; - icon: IconType | undefined; shortcut?: string; isSelected: boolean; set: () => void; @@ -54,7 +62,31 @@ export function SuggestionListItem(props: SuggestionGroupItemProps) { } }); - const Icon = props.icon; + // TODO: rearchitect, this is hacky + let Icon: IconType | undefined; + switch (props.name) { + case "Heading": + Icon = RiH1; + break; + case "Heading 2": + Icon = RiH2; + break; + + case "Heading 3": + Icon = RiH3; + break; + case "Numbered List": + Icon = RiListOrdered; + break; + case "Bullet List": + Icon = RiListUnordered; + break; + case "Paragraph": + Icon = RiText; + break; + default: + break; + } return ( setValue((value) => value + 1); +} + +/** + * Main hook for importing a BlockNote editor into a react project + */ +export const useBlockNote = ( + options: Partial = {}, + deps: DependencyList = [] +) => { + const [editor, setEditor] = useState(null); + const forceUpdate = useForceUpdate(); + // useEditorForceUpdate(editor.tiptapEditor); + + useEffect(() => { + let isMounted = true; + let newOptions = { ...options }; + if (!newOptions.uiFactories) { + newOptions = { + ...newOptions, + uiFactories: { + bubbleMenuFactory: ReactBubbleMenuFactory, + hyperlinkMenuFactory: ReactHyperlinkMenuFactory, + suggestionsMenuFactory: ReactSuggestionsMenuFactory, + addBlockButtonFactory: ReactAddBlockButtonFactory, + dragHandleFactory: ReactDragHandleFactory, + dragHandleMenuFactory: ReactDragHandleMenuFactory, + }, + }; + } + console.log("create new blocknote instance"); + const instance = new BlockNoteEditor(newOptions); + + setEditor(instance); + + instance.tiptapEditor.on("transaction", () => { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + if (isMounted) { + forceUpdate(); + } + }); + }); + }); + + return () => { + instance.tiptapEditor.destroy(); + isMounted = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); + + return editor; +}; diff --git a/packages/core/src/shared/hooks/useEditorForceUpdate.tsx b/packages/react/src/hooks/useEditorForceUpdate.tsx similarity index 100% rename from packages/core/src/shared/hooks/useEditorForceUpdate.tsx rename to packages/react/src/hooks/useEditorForceUpdate.tsx diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index dc4253e378..444df8d17c 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,8 +1,10 @@ // TODO: review directories export * from "./AddBlockButton/AddBlockButtonFactory"; +export * from "./BlockNoteView"; export * from "./BubbleMenu/BubbleMenuFactory"; export * from "./DragHandle/DragHandleFactory"; export * from "./DragHandleMenu/DragHandleMenuFactory"; -export * from "./Editor/EditorContent"; +export * from "./hooks/useBlockNote"; +export * from "./hooks/useEditorForceUpdate"; export * from "./HyperlinkMenu/HyperlinkMenuFactory"; export * from "./SuggestionsMenu/SuggestionsMenuFactory"; From f6a63643ceeb81e1c8683f105cb40667b33af26b Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 11 Jan 2023 14:17:29 +0100 Subject: [PATCH 33/55] Fixed hyperlink edit menu bugs --- .../react/src/BubbleMenu/components/LinkToolbarButton.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx index 06a656c12a..2d18bac65c 100644 --- a/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx +++ b/packages/react/src/BubbleMenu/components/LinkToolbarButton.tsx @@ -24,8 +24,8 @@ export const LinkToolbarButton = (props: HyperlinkButtonProps) => { setCreationMenu( ); @@ -33,6 +33,7 @@ export const LinkToolbarButton = (props: HyperlinkButtonProps) => { return ( { From 82be2867ebdce5e74e2cb2ee0d1b1f7e10e2a216 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 11 Jan 2023 14:43:21 +0100 Subject: [PATCH 34/55] re-add font --- packages/core/src/BlockNoteEditor.ts | 1 + packages/core/src/editor.module.css | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index a427625f94..7810c49bc9 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -1,5 +1,6 @@ import { Editor, EditorOptions } from "@tiptap/core"; +// import "./blocknote.css"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import { BubbleMenuFactory } from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; diff --git a/packages/core/src/editor.module.css b/packages/core/src/editor.module.css index 47296e8811..2976478420 100644 --- a/packages/core/src/editor.module.css +++ b/packages/core/src/editor.module.css @@ -1,3 +1,5 @@ +@import url("./assets/fonts-inter.css"); + .bnEditor { outline: none; } @@ -21,3 +23,18 @@ Tippy popups that are appended to document.body directly -moz-box-sizing: inherit; box-sizing: inherit; } + +.bnEditor { + /* Define a set of colors to be used throughout the app for consistency + see https://atlassian.design/foundations/color for more info */ + --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */ + --N40: #dfe1e6; /* Light neutral used for subtle borders and text on dark background */ + + font-family: "Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, + "Open Sans", "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", + "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + color: rgb(60, 65, 73); +} From 94466376dd971250eeaa668d235481bae4d42771 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 11 Jan 2023 16:03:12 +0100 Subject: [PATCH 35/55] Removed bubble menu animation update delay hack --- .../src/extensions/BubbleMenu/BubbleMenuPlugin.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index f8772e5617..96994d81fe 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -177,15 +177,8 @@ export class BubbleMenuView { !this.preventShow && (shouldShow || this.preventHide) ) { - // Hacky fix to account for animations. Since the bounding boxes/DOMRects of elements are calculated based on how - // they are displayed on the screen, we need to wait until a given animation is completed to get the correct - // values for the selectionBoundingBox param. - // TODO: Find a better solution. The delay can cause menu updates to occur while the menu is hidden, which may - // cause issues depending on the menu factory implementation. - setTimeout(() => { - this.updateBubbleMenuParams(); - this.bubbleMenu.update(this.bubbleMenuParams); - }, 400); + this.updateBubbleMenuParams(); + this.bubbleMenu.update(this.bubbleMenuParams); return; } From 07c38f1ceceb9a67288fa095f639fdc6998372a2 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 11 Jan 2023 18:27:37 +0100 Subject: [PATCH 36/55] Changed bubble menu placement to be at the start of the selection bounding box --- packages/react/src/BubbleMenu/BubbleMenuFactory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx index 6fd78043d2..572341f222 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx @@ -73,7 +73,7 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( getReferenceClientRect={() => params.selectionBoundingBox} hideOnClick={false} interactive={true} - placement={"top"} + placement={"top-start"} showOnCreate={true} trigger={"manual"} /> From 89897f34faebd55762d5dd1b8fb64545f970c8ce Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 11 Jan 2023 19:17:42 +0100 Subject: [PATCH 37/55] Fixed hyperlink menu naming inconsistencies --- .../Hyperlinks/HyperlinkMenuPlugin.ts | 62 +++++++++---------- 1 file changed, 28 insertions(+), 34 deletions(-) diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts index 8a234a7ce1..ee00ffdf99 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts @@ -12,16 +12,16 @@ export type HyperlinkMenuPluginProps = { hyperlinkMenuFactory: HyperlinkMenuFactory; }; -export type HyperlinkHoverMenuViewProps = { +export type HyperlinkMenuViewProps = { editor: Editor; - hyperlinkHoverMenuFactory: HyperlinkMenuFactory; + hyperlinkMenuFactory: HyperlinkMenuFactory; }; -class HyperlinkHoverMenuView { +class HyperlinkMenuView { editor: Editor; - hyperlinkHoverMenuParams: HyperlinkMenuParams; - hyperlinkHoverMenu: HyperlinkMenu; + hyperlinkMenuParams: HyperlinkMenuParams; + hyperlinkMenu: HyperlinkMenu; menuUpdateTimer: NodeJS.Timeout | undefined; startMenuUpdateTimer: () => void; @@ -36,16 +36,11 @@ class HyperlinkHoverMenuView { hyperlinkMark: Mark | undefined; hyperlinkMarkRange: Range | undefined; - constructor({ - editor, - hyperlinkHoverMenuFactory, - }: HyperlinkHoverMenuViewProps) { + constructor({ editor, hyperlinkMenuFactory }: HyperlinkMenuViewProps) { this.editor = editor; - this.hyperlinkHoverMenuParams = this.initHyperlinkHoverMenuParams(); - this.hyperlinkHoverMenu = hyperlinkHoverMenuFactory( - this.hyperlinkHoverMenuParams - ); + this.hyperlinkMenuParams = this.initHyperlinkMenuParams(); + this.hyperlinkMenu = hyperlinkMenuFactory(this.hyperlinkMenuParams); this.startMenuUpdateTimer = () => { this.menuUpdateTimer = setTimeout(() => { @@ -152,17 +147,17 @@ class HyperlinkHoverMenuView { } if (this.hyperlinkMark) { - this.updateHyperlinkHoverMenuParams(); + this.updateHyperlinkMenuParams(); // Shows menu. if (!prevHyperlinkMark) { - this.hyperlinkHoverMenu.show(this.hyperlinkHoverMenuParams); + this.hyperlinkMenu.show(this.hyperlinkMenuParams); - this.hyperlinkHoverMenu.element?.addEventListener( + this.hyperlinkMenu.element?.addEventListener( "mouseleave", this.startMenuUpdateTimer ); - this.hyperlinkHoverMenu.element?.addEventListener( + this.hyperlinkMenu.element?.addEventListener( "mouseenter", this.stopMenuUpdateTimer ); @@ -171,27 +166,27 @@ class HyperlinkHoverMenuView { } // Updates menu. - this.hyperlinkHoverMenu.update(this.hyperlinkHoverMenuParams); + this.hyperlinkMenu.update(this.hyperlinkMenuParams); } // Hides menu. if (prevHyperlinkMark && !this.hyperlinkMark) { - this.hyperlinkHoverMenu.element?.removeEventListener( + this.hyperlinkMenu.element?.removeEventListener( "mouseleave", this.startMenuUpdateTimer ); - this.hyperlinkHoverMenu.element?.removeEventListener( + this.hyperlinkMenu.element?.removeEventListener( "mouseenter", this.stopMenuUpdateTimer ); - this.hyperlinkHoverMenu.hide(); + this.hyperlinkMenu.hide(); return; } } - initHyperlinkHoverMenuParams(): HyperlinkMenuParams { + initHyperlinkMenuParams(): HyperlinkMenuParams { return { url: "", text: "", @@ -209,7 +204,7 @@ class HyperlinkHoverMenuView { this.editor.view.dispatch(tr); this.editor.view.focus(); - this.hyperlinkHoverMenu.hide(); + this.hyperlinkMenu.hide(); }, deleteHyperlink: () => { this.editor.view.dispatch( @@ -223,7 +218,7 @@ class HyperlinkHoverMenuView { ); this.editor.view.focus(); - this.hyperlinkHoverMenu.hide(); + this.hyperlinkMenu.hide(); }, boundingBox: new DOMRect(), @@ -231,18 +226,17 @@ class HyperlinkHoverMenuView { }; } - updateHyperlinkHoverMenuParams() { + updateHyperlinkMenuParams() { if (this.hyperlinkMark) { - this.hyperlinkHoverMenuParams.url = this.hyperlinkMark.attrs.href; - this.hyperlinkHoverMenuParams.text = - this.editor.view.state.doc.textBetween( - this.hyperlinkMarkRange!.from, - this.hyperlinkMarkRange!.to - ); + this.hyperlinkMenuParams.url = this.hyperlinkMark.attrs.href; + this.hyperlinkMenuParams.text = this.editor.view.state.doc.textBetween( + this.hyperlinkMarkRange!.from, + this.hyperlinkMarkRange!.to + ); } if (this.hyperlinkMarkRange) { - this.hyperlinkHoverMenuParams.boundingBox = posToDOMRect( + this.hyperlinkMenuParams.boundingBox = posToDOMRect( this.editor.view, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to @@ -258,9 +252,9 @@ export const createHyperlinkMenuPlugin = ( return new Plugin({ key: PLUGIN_KEY, view: () => - new HyperlinkHoverMenuView({ + new HyperlinkMenuView({ editor: editor, - hyperlinkHoverMenuFactory: options.hyperlinkMenuFactory, + hyperlinkMenuFactory: options.hyperlinkMenuFactory, }), }); }; From 038f22b1c7d2f4e4375f04b8378ea2f82fea0b22 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 12 Jan 2023 18:15:08 +0100 Subject: [PATCH 38/55] Refactored block/drag menu to use one factory and separate view class --- packages/core/src/BlockNoteEditor.ts | 14 +- .../DraggableBlocks/BlockMenuFactoryTypes.ts | 14 + .../DraggableBlocks/DragMenuFactoryTypes.ts | 24 -- .../DraggableBlocksExtension.ts | 14 +- .../DraggableBlocks/DraggableBlocksPlugin.ts | 383 ++++++++---------- packages/core/src/index.ts | 2 +- .../plugins/suggestion/SuggestionPlugin.ts | 5 + .../AddBlockButton/AddBlockButtonFactory.tsx | 70 ---- .../components/AddBlockButton.tsx | 14 - .../BlockMenuFactory.tsx} | 41 +- .../src/BlockMenu/components/BlockMenu.tsx | 67 +++ .../src/DragHandle/components/DragHandle.tsx | 10 - .../DragHandleMenu/DragHandleMenuFactory.tsx | 72 ---- .../components/DragHandleMenu.tsx | 24 -- packages/react/src/hooks/useBlockNote.ts | 8 +- packages/react/src/index.ts | 4 +- 16 files changed, 296 insertions(+), 470 deletions(-) create mode 100644 packages/core/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.ts delete mode 100644 packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts delete mode 100644 packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx delete mode 100644 packages/react/src/AddBlockButton/components/AddBlockButton.tsx rename packages/react/src/{DragHandle/DragHandleFactory.tsx => BlockMenu/BlockMenuFactory.tsx} (60%) create mode 100644 packages/react/src/BlockMenu/components/BlockMenu.tsx delete mode 100644 packages/react/src/DragHandle/components/DragHandle.tsx delete mode 100644 packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx delete mode 100644 packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 7810c49bc9..62c6ab7b2e 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -4,11 +4,7 @@ import { Editor, EditorOptions } from "@tiptap/core"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; import { BubbleMenuFactory } from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; -import { - AddBlockButtonFactory, - DragHandleFactory, - DragHandleMenuFactory, -} from "./extensions/DraggableBlocks/DragMenuFactoryTypes"; +import { BlockMenuFactory } from "./extensions/DraggableBlocks/BlockMenuFactoryTypes"; import { HyperlinkMenuFactory } from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; @@ -20,9 +16,7 @@ export type BlockNoteEditorOptions = EditorOptions & { bubbleMenuFactory: BubbleMenuFactory; hyperlinkMenuFactory: HyperlinkMenuFactory; suggestionsMenuFactory: SuggestionsMenuFactory; - addBlockButtonFactory: AddBlockButtonFactory; - dragHandleFactory: DragHandleFactory; - dragHandleMenuFactory: DragHandleMenuFactory; + blockMenuFactory: BlockMenuFactory; }; }; @@ -76,9 +70,7 @@ export class BlockNoteEditor { options.uiFactories ) { return extension.configure({ - addBlockButtonFactory: options.uiFactories.addBlockButtonFactory, - dragHandleFactory: options.uiFactories.dragHandleFactory, - dragHandleMenuFactory: options.uiFactories.dragHandleMenuFactory, + blockMenuFactory: options.uiFactories.blockMenuFactory, }); } diff --git a/packages/core/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.ts new file mode 100644 index 0000000000..d47d8f145c --- /dev/null +++ b/packages/core/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.ts @@ -0,0 +1,14 @@ +import { EditorElement, ElementFactory } from "../../shared/EditorElement"; + +export type BlockMenuParams = { + addBlock: () => void; + deleteBlock: () => void; + blockDragStart: (event: DragEvent) => void; + blockDragEnd: () => void; + freezeMenu: () => void; + unfreezeMenu: () => void; + blockBoundingBox: DOMRect; +}; + +export type BlockMenu = EditorElement; +export type BlockMenuFactory = ElementFactory; diff --git a/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts deleted file mode 100644 index 96668e35eb..0000000000 --- a/packages/core/src/extensions/DraggableBlocks/DragMenuFactoryTypes.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { EditorElement, ElementFactory } from "../../shared/EditorElement"; - -export type AddBlockButtonParams = { - addBlock: () => void; - blockBoundingBox: DOMRect; -}; - -export type AddBlockButton = EditorElement; -export type AddBlockButtonFactory = ElementFactory; - -export type DragHandleParams = { - blockBoundingBox: DOMRect; -}; - -export type DragHandle = EditorElement; -export type DragHandleFactory = ElementFactory; - -export type DragHandleMenuParams = { - deleteBlock: () => void; - dragHandleBoundingBox: DOMRect; -}; - -export type DragHandleMenu = EditorElement; -export type DragHandleMenuFactory = ElementFactory; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts index e02157a156..11a5331b11 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts @@ -1,16 +1,10 @@ import { Editor, Extension } from "@tiptap/core"; import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin"; -import { - AddBlockButtonFactory, - DragHandleFactory, - DragHandleMenuFactory, -} from "./DragMenuFactoryTypes"; +import { BlockMenuFactory } from "./BlockMenuFactoryTypes"; export type DraggableBlocksOptions = { editor: Editor; - addBlockButtonFactory: AddBlockButtonFactory; - dragHandleFactory: DragHandleFactory; - dragHandleMenuFactory: DragHandleMenuFactory; + blockMenuFactory: BlockMenuFactory; }; /** @@ -26,9 +20,7 @@ export const DraggableBlocksExtension = return [ createDraggableBlocksPlugin({ editor: this.editor, - addBlockButtonFactory: this.options.addBlockButtonFactory, - dragHandleFactory: this.options.dragHandleFactory, - dragHandleMenuFactory: this.options.dragHandleMenuFactory, + blockMenuFactory: this.options.blockMenuFactory, }), ]; }, diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 94c8535e8f..1745119770 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -1,15 +1,15 @@ +import { Editor } from "@tiptap/core"; import { Node } from "prosemirror-model"; import { NodeSelection, Plugin, PluginKey, Selection } from "prosemirror-state"; import * as pv from "prosemirror-view"; import { EditorView } from "prosemirror-view"; -// import { BlockNoteTheme } from "../../BlockNoteTheme"; import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection"; import { DraggableBlocksOptions } from "./DraggableBlocksExtension"; import { - AddBlockButtonParams, - DragHandleMenuParams, - DragHandleParams, -} from "./DragMenuFactoryTypes"; + BlockMenu, + BlockMenuFactory, + BlockMenuParams, +} from "./BlockMenuFactoryTypes"; import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension"; @@ -222,29 +222,159 @@ function dragStart(e: DragEvent, view: EditorView) { } } -export const createDraggableBlocksPlugin = ( - options: DraggableBlocksOptions -) => { +export type BlockMenuViewProps = { + editor: Editor; + blockMenuFactory: BlockMenuFactory; + horizontalPosAnchoredAtRoot: boolean; +}; + +export class BlockMenuView { + editor: Editor; + // When true, the drag handle with be anchored at the same level as root elements // When false, the drag handle with be just to the left of the element - const horizontalPosAnchoredAtRoot = true; - // Determines if the drag handle and add block buttons should be visible. Gets set to true on mouse move events. Gets - // set to false on mouse click and key down events. - let blockButtonsVisible = false; - // Determines if the drag handle and add block buttons should be frozen, i.e. should not update on mouse move. Gets - // set to true when clicking the add block button. Gets set to false on mouse click and key down events. - let blockButtonsFrozen = false; - - // Declares callback functions for use for params in drag handle, drag handle menu, and add block button factories. - function addBlock(coords: { left: number; top: number }) { - blockButtonsFrozen = true; - - const pos = options.editor.view.posAtCoords(coords); + horizontalPosAnchoredAtRoot: boolean; + + blockMenuParams: BlockMenuParams; + blockMenu: BlockMenu; + + menuOpen = false; + menuFrozen = false; + + constructor({ + editor, + blockMenuFactory, + horizontalPosAnchoredAtRoot, + }: BlockMenuViewProps) { + this.editor = editor; + this.horizontalPosAnchoredAtRoot = horizontalPosAnchoredAtRoot; + + this.blockMenuParams = { + addBlock: () => this.addBlock({ left: 0, top: 0 }), + deleteBlock: () => this.deleteBlock({ left: 0, top: 0 }), + blockDragStart: (event: DragEvent) => dragStart(event, this.editor.view), + blockDragEnd: () => unsetDragImage(), + freezeMenu: () => { + this.menuFrozen = true; + }, + unfreezeMenu: () => { + this.menuFrozen = false; + }, + blockBoundingBox: new DOMRect(), + }; + this.blockMenu = blockMenuFactory(this.blockMenuParams); + + // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen. + document.body.addEventListener( + "mousemove", + (event) => { + if (this.menuFrozen) { + return; + } + + // Gets block at mouse cursor's vertical position. + const coords = { + left: this.editor.view.dom.clientWidth / 2, // take middle of editor + top: event.clientY, + }; + const block = getDraggableBlockFromCoords(coords, this.editor.view); + + // Closes the menu if the mouse cursor is beyond the editor vertically. + if (!block) { + if (this.menuOpen) { + this.menuOpen = false; + this.blockMenu.hide(); + } + + return; + } + + // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position. + const blockContent = block.node.firstChild as HTMLElement; + + if (!blockContent) { + return; + } + + // Gets bounding box of the block content. + const blockBoundingBox = blockContent.getBoundingClientRect(); + blockBoundingBox.x = this.horizontalPosAnchoredAtRoot + ? getHorizontalAnchor() + : blockBoundingBox.left; + + this.blockMenuParams.addBlock = () => + this.addBlock({ + left: blockBoundingBox.left, + top: blockBoundingBox.top, + }); + this.blockMenuParams.deleteBlock = () => + this.deleteBlock({ + left: blockBoundingBox.left, + top: blockBoundingBox.top, + }); + this.blockMenuParams.blockBoundingBox = blockBoundingBox; + + // Shows or updates elements. + if (!this.menuOpen) { + this.menuOpen = true; + this.blockMenu.show(this.blockMenuParams); + } else { + this.blockMenu.update(this.blockMenuParams); + } + }, + true + ); + + // Hides and unfreezes the menu whenever the user selects the editor with the mouse or presses a key. + // TODO: Better integration with suggestions menu and only editor scope? + document.body.addEventListener( + "mousedown", + (event) => { + if (this.blockMenu.element?.contains(event.target as HTMLElement)) { + return; + } + + if (this.menuOpen) { + this.menuOpen = false; + this.blockMenu.hide(); + } + + this.menuFrozen = false; + }, + true + ); + document.body.addEventListener( + "keydown", + () => { + if (this.menuOpen) { + this.menuOpen = false; + this.blockMenu.hide(); + } + + this.menuFrozen = false; + }, + true + ); + } + + destroy() { + if (this.menuOpen) { + this.menuOpen = false; + this.blockMenu.hide(); + } + } + + addBlock(coords: { left: number; top: number }) { + this.menuOpen = false; + this.menuFrozen = true; + this.blockMenu.hide(); + + const pos = this.editor.view.posAtCoords(coords); if (!pos) { return; } - const blockInfo = getBlockInfoFromPos(options.editor.state.doc, pos.pos); + const blockInfo = getBlockInfoFromPos(this.editor.state.doc, pos.pos); if (blockInfo === undefined) { return; } @@ -256,7 +386,7 @@ export const createDraggableBlocksPlugin = ( const newBlockInsertionPos = endPos + 1; const newBlockContentPos = newBlockInsertionPos + 2; - options.editor + this.editor .chain() .BNCreateBlock(newBlockInsertionPos) .BNSetContentType(newBlockContentPos, "textContent") @@ -265,202 +395,39 @@ export const createDraggableBlocksPlugin = ( } // Focuses and activates the suggestion menu. - options.editor.view.focus(); - options.editor.view.dispatch( - options.editor.view.state.tr - .scrollIntoView() - .setMeta(SlashMenuPluginKey, { - // TODO import suggestion plugin key - activate: true, - type: "drag", - }) + this.editor.view.focus(); + this.editor.view.dispatch( + this.editor.view.state.tr.scrollIntoView().setMeta(SlashMenuPluginKey, { + // TODO import suggestion plugin key + activate: true, + type: "drag", + }) ); } - function deleteBlock(coords: { left: number; top: number }) { - dragHandleMenu.hide(); + deleteBlock(coords: { left: number; top: number }) { + this.menuOpen = false; + this.blockMenu.hide(); - const pos = options.editor.view.posAtCoords(coords); + const pos = this.editor.view.posAtCoords(coords); if (!pos) { return; } - options.editor.commands.BNDeleteBlock(pos.pos); - } - - // Initializes params for use in drag handle, drag handle menu, and add block button factories. - const addBlockButtonParams: AddBlockButtonParams = { - addBlock: () => addBlock({ left: 0, top: 0 }), - blockBoundingBox: new DOMRect(), - }; - const dragHandleParams: DragHandleParams = { - blockBoundingBox: new DOMRect(), - }; - const dragHandleMenuParams: DragHandleMenuParams = { - deleteBlock: () => deleteBlock({ left: 0, top: 0 }), - dragHandleBoundingBox: new DOMRect(), - }; - - // Creates drag handle, drag handle menu, and add block button editor elements. - const addBlockButton = options.addBlockButtonFactory(addBlockButtonParams); - const dragHandle = options.dragHandleFactory(dragHandleParams); - const dragHandleMenu = options.dragHandleMenuFactory(dragHandleMenuParams); - - // Declares additional listeners to attach to the drag handle element for drag/drop & menu opening. - const dragStartCallback = (event: DragEvent) => - dragStart(event, options.editor.view); - const dragEndCallback = (_event: DragEvent) => unsetDragImage(); - const clickCallback = (_event: MouseEvent) => { - dragHandleMenu.show(dragHandleMenuParams); - blockButtonsFrozen = true; - }; - - function addDragHandleListeners() { - dragHandle.element!.addEventListener("dragstart", dragStartCallback); - dragHandle.element!.addEventListener("dragend", dragEndCallback); - dragHandle.element!.addEventListener("click", clickCallback); - } - - function removeDragHandleListeners() { - dragHandle.element!.removeEventListener("dragstart", dragStartCallback); - dragHandle.element!.removeEventListener("dragend", dragEndCallback); - dragHandle.element!.removeEventListener("click", clickCallback); + this.editor.commands.BNDeleteBlock(pos.pos); } +} - // Hides drag handle, drag handle menu, and add block button when scrolling. - window.addEventListener("scroll", () => { - blockButtonsVisible = false; - blockButtonsFrozen = false; - - addBlockButton.hide(); - dragHandle.hide(); - removeDragHandleListeners(); - dragHandleMenu.hide(); - }); - +export const createDraggableBlocksPlugin = ( + options: DraggableBlocksOptions +) => { return new Plugin({ key: new PluginKey("DraggableBlocksPlugin"), - view() { - return { - destroy() { - addBlockButton.hide(); - dragHandle.hide(); - removeDragHandleListeners(); - dragHandleMenu.hide(); - }, - }; - }, - props: { - // handleDOMEvents: { - - // }, - // handleDOMEvents: { - // dragend(view, event) { - // // setTimeout(() => { - // // let node = document.querySelector(".ProseMirror-hideselection"); - // // if (node) { - // // node.classList.remove("ProseMirror-hideselection"); - // // } - // // }, 50); - // return true; - // }, - handleKeyDown(_view, _event) { - blockButtonsVisible = false; - blockButtonsFrozen = false; - - addBlockButton.hide(); - dragHandle.hide(); - removeDragHandleListeners(); - - return false; - }, - handleDOMEvents: { - // drag(view, event) { - // // event.dataTransfer!.; - // return false; - // }, - mouseleave(_view, _event: any) { - // TODO - // dropElement.style.display = "none"; - return true; - }, - mousedown(_view, _event: any) { - blockButtonsVisible = false; - blockButtonsFrozen = false; - - addBlockButton.hide(); - dragHandle.hide(); - removeDragHandleListeners(); - dragHandleMenu.hide(); - - return false; - }, - mousemove(view, event: any) { - if (blockButtonsFrozen) { - return true; - } - - // Gets block at mouse Y coordinate. - const coords = { - left: view.dom.clientWidth / 2, // take middle of editor - top: event.clientY, - }; - const block = getDraggableBlockFromCoords(coords, view); - - if (!block) { - console.warn("Perhaps we should hide element?"); - return true; - } - - // I want the dim of the blocks content node - // because if the block contains other blocks - // Its dims change, moving the position of the drag handle - const blockContent = block.node.firstChild as HTMLElement; - - if (!blockContent) { - return true; - } - - // Gets bounding box of relevant block. - const blockBoundingBox = blockContent.getBoundingClientRect(); - blockBoundingBox.x = horizontalPosAnchoredAtRoot - ? getHorizontalAnchor() - : blockBoundingBox.left; - - // Updates element params. - addBlockButtonParams.addBlock = () => - addBlock({ - left: blockBoundingBox.left, - top: blockBoundingBox.top, - }); - addBlockButtonParams.blockBoundingBox = blockBoundingBox; - - dragHandleParams.blockBoundingBox = blockBoundingBox; - - dragHandleMenuParams.deleteBlock = () => - deleteBlock({ - left: blockBoundingBox.left, - top: blockBoundingBox.top, - }); - dragHandleMenuParams.dragHandleBoundingBox = blockBoundingBox; - - // Shows or updates elements. - if (!blockButtonsVisible) { - blockButtonsVisible = true; - - dragHandle.show(dragHandleParams); - addBlockButton.show(addBlockButtonParams); - - dragHandle.element!.setAttribute("draggable", "true"); - addDragHandleListeners(); - } else { - dragHandle.update(dragHandleParams); - addBlockButton.update(addBlockButtonParams); - } - - return true; - }, - }, - }, + view: () => + new BlockMenuView({ + editor: options.editor, + blockMenuFactory: options.blockMenuFactory, + horizontalPosAnchoredAtRoot: true, + }), }); }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1d9d832763..d1aa3eb99e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,7 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; export * from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; -export * from "./extensions/DraggableBlocks/DragMenuFactoryTypes"; +export * from "./extensions/DraggableBlocks/BlockMenuFactoryTypes"; export * from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index aaa26e2a0c..99b5847625 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -432,6 +432,11 @@ export function createSuggestionPlugin({ return false; }, + // Hides menu in cases where mouse click does not cause an editor state change. + handleClick(view) { + deactivate(view); + }, + // Setup decorator on the currently active suggestion. decorations(state) { const { active, range, decorationId, type } = (this as Plugin).getState( diff --git a/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx b/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx deleted file mode 100644 index 8b3180fb7b..0000000000 --- a/packages/react/src/AddBlockButton/AddBlockButtonFactory.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { - AddBlockButton, - AddBlockButtonFactory, - AddBlockButtonParams, -} from "@blocknote/core"; -import { AddBlockButtonProps } from "./components/AddBlockButton"; -import { AddBlockButton as ReactAddBlockButton } from "./components/AddBlockButton"; -import { BlockNoteTheme } from "../BlockNoteTheme"; -import { MantineProvider } from "@mantine/core"; -import { createRoot, Root } from "react-dom/client"; -import Tippy from "@tippyjs/react"; - -export const ReactAddBlockButtonFactory: AddBlockButtonFactory = ( - params: AddBlockButtonParams -): AddBlockButton => { - const addBlockButtonProps: AddBlockButtonProps = { - ...params, - }; - - function updateAddBlockButtonProps(params: AddBlockButtonParams) { - addBlockButtonProps.addBlock = params.addBlock; - } - - function getMenuComponent() { - return ( - - } - duration={0} - getReferenceClientRect={() => params.blockBoundingBox} - hideOnClick={false} - interactive={true} - offset={[0, 24]} - placement={"left"} - showOnCreate={true} - trigger={"manual"} - /> - - ); - } - - // We don't use the document body as a root as it would cause multiple React roots to be created on a single element - // if other menu factories do the same. - const menuRootElement = document.createElement("div"); - // menuRootElement.className = rootStyles.bnRoot; - let menuRoot: Root | undefined; - - return { - element: menuRootElement, - show: (params: AddBlockButtonParams) => { - updateAddBlockButtonProps(params); - - document.body.appendChild(menuRootElement); - menuRoot = createRoot(menuRootElement); - - menuRoot.render(getMenuComponent()); - }, - hide: () => { - menuRoot?.unmount(); - - menuRootElement.remove(); - }, - update: (_params: AddBlockButtonParams) => { - updateAddBlockButtonProps(params); - - menuRoot?.render(getMenuComponent()); - }, - }; -}; diff --git a/packages/react/src/AddBlockButton/components/AddBlockButton.tsx b/packages/react/src/AddBlockButton/components/AddBlockButton.tsx deleted file mode 100644 index 7a4bbe5cba..0000000000 --- a/packages/react/src/AddBlockButton/components/AddBlockButton.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { ActionIcon } from "@mantine/core"; -import { AiOutlinePlus } from "react-icons/all"; - -export type AddBlockButtonProps = { - addBlock: () => void; -}; - -export const AddBlockButton = (props: AddBlockButtonProps) => { - return ( - - {} - - ); -}; diff --git a/packages/react/src/DragHandle/DragHandleFactory.tsx b/packages/react/src/BlockMenu/BlockMenuFactory.tsx similarity index 60% rename from packages/react/src/DragHandle/DragHandleFactory.tsx rename to packages/react/src/BlockMenu/BlockMenuFactory.tsx index a2ce905400..6465b46508 100644 --- a/packages/react/src/DragHandle/DragHandleFactory.tsx +++ b/packages/react/src/BlockMenu/BlockMenuFactory.tsx @@ -1,21 +1,26 @@ +import { BlockMenu, BlockMenuFactory, BlockMenuParams } from "@blocknote/core"; import { - DragHandle, - DragHandleFactory, - DragHandleParams, -} from "@blocknote/core"; -import { DragHandle as ReactDragHandle } from "./components/DragHandle"; -import { BlockNoteTheme } from "../BlockNoteTheme"; -import { MantineProvider } from "@mantine/core"; + BlockMenu as ReactBlockMenu, + BlockMenuProps, +} from "../BlockMenu/components/BlockMenu"; import { createRoot, Root } from "react-dom/client"; +import { MantineProvider } from "@mantine/core"; +import { BlockNoteTheme } from "../BlockNoteTheme"; import Tippy from "@tippyjs/react"; -export const ReactDragHandleFactory: DragHandleFactory = ( - params: DragHandleParams -): DragHandle => { +export const ReactBlockMenuFactory: BlockMenuFactory = ( + params: BlockMenuParams +): BlockMenu => { + const blockMenuProps: BlockMenuProps = { ...params }; + + function updateBlockMenuProps(params: BlockMenuParams) { + blockMenuProps.addBlock = params.addBlock; + blockMenuProps.deleteBlock = params.deleteBlock; + } + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other menu factories do the same. const menuRootElement = document.createElement("div"); - menuRootElement.style.position = "absolute"; // menuRootElement.className = rootStyles.bnRoot; let menuRoot: Root | undefined; @@ -24,7 +29,7 @@ export const ReactDragHandleFactory: DragHandleFactory = ( } + content={} duration={0} getReferenceClientRect={() => params.blockBoundingBox} hideOnClick={false} @@ -40,19 +45,23 @@ export const ReactDragHandleFactory: DragHandleFactory = ( return { element: menuRootElement, - show: (_params: DragHandleParams) => { + show: (params: BlockMenuParams) => { + updateBlockMenuProps(params); + document.body.appendChild(menuRootElement); menuRoot = createRoot(menuRootElement); menuRoot.render(getMenuComponent()); }, hide: () => { - menuRoot?.unmount(); + menuRoot!.unmount(); menuRootElement.remove(); }, - update: (_params: DragHandleParams) => { - menuRoot?.render(getMenuComponent()); + update: (params: BlockMenuParams) => { + updateBlockMenuProps(params); + + menuRoot!.render(getMenuComponent()); }, }; }; diff --git a/packages/react/src/BlockMenu/components/BlockMenu.tsx b/packages/react/src/BlockMenu/components/BlockMenu.tsx new file mode 100644 index 0000000000..095af1c681 --- /dev/null +++ b/packages/react/src/BlockMenu/components/BlockMenu.tsx @@ -0,0 +1,67 @@ +import { AiOutlinePlus, MdDragIndicator } from "react-icons/all"; +import { ActionIcon, createStyles, Group, Menu } from "@mantine/core"; +import { useEffect, useRef } from "react"; + +export type BlockMenuProps = { + addBlock: () => void; + deleteBlock: () => void; + blockDragStart: (event: DragEvent) => void; + blockDragEnd: () => void; + freezeMenu: () => void; + unfreezeMenu: () => void; +}; + +export const BlockMenu = (props: BlockMenuProps) => { + const { classes } = createStyles({ root: {} })(undefined, { + name: "DragHandleMenu", + }); + + const dragHandleRef = useRef(null); + + useEffect(() => { + const dragHandle = dragHandleRef.current; + + if (dragHandle instanceof HTMLDivElement) { + dragHandle.addEventListener("dragstart", props.blockDragStart); + dragHandle.addEventListener("dragend", props.blockDragEnd); + + return () => { + dragHandle.removeEventListener("dragstart", props.blockDragStart); + dragHandle.removeEventListener("dragend", props.blockDragEnd); + }; + } + + return; + }, [props.blockDragEnd, props.blockDragStart]); + + return ( + + + { + { + console.log("OPEN MENU"); + props.addBlock(); + }} + /> + } + + + +
+ + {} + +
+
+ + Delete + +
+
+ ); +}; diff --git a/packages/react/src/DragHandle/components/DragHandle.tsx b/packages/react/src/DragHandle/components/DragHandle.tsx deleted file mode 100644 index c5c4bea973..0000000000 --- a/packages/react/src/DragHandle/components/DragHandle.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ActionIcon } from "@mantine/core"; -import { MdDragIndicator } from "react-icons/all"; - -export const DragHandle = () => { - return ( - - {} - - ); -}; diff --git a/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx b/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx deleted file mode 100644 index 605ff608a8..0000000000 --- a/packages/react/src/DragHandleMenu/DragHandleMenuFactory.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { MantineProvider } from "@mantine/core"; -import { BlockNoteTheme } from "../BlockNoteTheme"; -import { createRoot, Root } from "react-dom/client"; -import { - DragHandleMenu as ReactDragHandleMenu, - DragHandleMenuProps, -} from "./components/DragHandleMenu"; -import { - DragHandleMenuFactory, - DragHandleMenuParams, - DragHandleMenu, -} from "@blocknote/core"; -import Tippy from "@tippyjs/react"; - -export const ReactDragHandleMenuFactory: DragHandleMenuFactory = ( - params: DragHandleMenuParams -): DragHandleMenu => { - const dragHandleMenuProps: DragHandleMenuProps = { - ...params, - }; - - function updateDragHandleMenuProps(params: DragHandleMenuParams) { - dragHandleMenuProps.deleteBlock = params.deleteBlock; - } - - function getMenuComponent() { - return ( - - } - duration={0} - getReferenceClientRect={() => params.dragHandleBoundingBox} - hideOnClick={false} - interactive={true} - // offset={[24, 0]} - placement={"left"} - showOnCreate={true} - trigger={"manual"} - /> - - ); - } - - // We don't use the document body as a root as it would cause multiple React roots to be created on a single element - // if other menu factories do the same. - const menuRootElement = document.createElement("div"); - // menuRootElement.className = rootStyles.bnRoot; - let menuRoot: Root | undefined; - - return { - element: menuRootElement, - show: (params: DragHandleMenuParams) => { - updateDragHandleMenuProps(params); - - document.body.appendChild(menuRootElement); - menuRoot = createRoot(menuRootElement); - - menuRoot.render(getMenuComponent()); - }, - hide: () => { - menuRoot?.unmount(); - - menuRootElement.remove(); - }, - update: (_params: DragHandleMenuParams) => { - updateDragHandleMenuProps(params); - - menuRoot?.render(getMenuComponent()); - }, - }; -}; diff --git a/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx b/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx deleted file mode 100644 index b8f8a9bc52..0000000000 --- a/packages/react/src/DragHandleMenu/components/DragHandleMenu.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { createStyles, Menu } from "@mantine/core"; - -export type DragHandleMenuProps = { - deleteBlock: () => void; -}; - -export const DragHandleMenu = (props: DragHandleMenuProps) => { - const { classes } = createStyles({ root: {} })(undefined, { - name: "DragHandleMenu", - }); - - return ( -
- - -
-
- - Delete - -
-
- ); -}; diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index 181b748fcf..f992d1a49e 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -1,11 +1,9 @@ import { BlockNoteEditor, BlockNoteEditorOptions } from "@blocknote/core"; import { DependencyList, useEffect, useState } from "react"; -import { ReactAddBlockButtonFactory } from "../AddBlockButton/AddBlockButtonFactory"; import { ReactBubbleMenuFactory } from "../BubbleMenu/BubbleMenuFactory"; -import { ReactDragHandleFactory } from "../DragHandle/DragHandleFactory"; -import { ReactDragHandleMenuFactory } from "../DragHandleMenu/DragHandleMenuFactory"; import { ReactHyperlinkMenuFactory } from "../HyperlinkMenu/HyperlinkMenuFactory"; import { ReactSuggestionsMenuFactory } from "../SuggestionsMenu/SuggestionsMenuFactory"; +import { ReactBlockMenuFactory } from "../BlockMenu/BlockMenuFactory"; //based on https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/useEditor.ts @@ -36,9 +34,7 @@ export const useBlockNote = ( bubbleMenuFactory: ReactBubbleMenuFactory, hyperlinkMenuFactory: ReactHyperlinkMenuFactory, suggestionsMenuFactory: ReactSuggestionsMenuFactory, - addBlockButtonFactory: ReactAddBlockButtonFactory, - dragHandleFactory: ReactDragHandleFactory, - dragHandleMenuFactory: ReactDragHandleMenuFactory, + blockMenuFactory: ReactBlockMenuFactory, }, }; } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 444df8d17c..808ca5a82a 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,9 +1,7 @@ // TODO: review directories -export * from "./AddBlockButton/AddBlockButtonFactory"; export * from "./BlockNoteView"; +export * from "./BlockMenu/BlockMenuFactory"; export * from "./BubbleMenu/BubbleMenuFactory"; -export * from "./DragHandle/DragHandleFactory"; -export * from "./DragHandleMenu/DragHandleMenuFactory"; export * from "./hooks/useBlockNote"; export * from "./hooks/useEditorForceUpdate"; export * from "./HyperlinkMenu/HyperlinkMenuFactory"; From 8b20dee43a7bf88ba4ae8ca3a57ccb148556c5d2 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 12 Jan 2023 18:37:31 +0100 Subject: [PATCH 39/55] Fixed bubble menu sometimes attempting to hide when already hidden --- packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts | 6 ++++-- .../core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts index 96994d81fe..055f0c8fa0 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts @@ -124,8 +124,10 @@ export class BubbleMenuView { return; } - this.bubbleMenu.hide(); - this.menuIsOpen = false; + if (this.menuIsOpen) { + this.bubbleMenu.hide(); + this.menuIsOpen = false; + } }; update(view: EditorView, oldState?: EditorState) { diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts index ee00ffdf99..1bc9bb4676 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts @@ -170,7 +170,7 @@ class HyperlinkMenuView { } // Hides menu. - if (prevHyperlinkMark && !this.hyperlinkMark) { + if (!this.hyperlinkMark && prevHyperlinkMark) { this.hyperlinkMenu.element?.removeEventListener( "mouseleave", this.startMenuUpdateTimer From 9c3cdc8e0ca983c438a218831f946f2f7e2b6094 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 12 Jan 2023 19:44:50 +0100 Subject: [PATCH 40/55] Fixed drag preview styling --- packages/core/src/editor.module.css | 2 +- .../src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/editor.module.css b/packages/core/src/editor.module.css index 2976478420..594eb4069a 100644 --- a/packages/core/src/editor.module.css +++ b/packages/core/src/editor.module.css @@ -24,7 +24,7 @@ Tippy popups that are appended to document.body directly box-sizing: inherit; } -.bnEditor { +.bnEditor, .dragPreview { /* Define a set of colors to be used throughout the app for consistency see https://atlassian.design/foundations/color for more info */ --N800: #172b4d; /* Dark neutral used for tooltips and text on light background */ diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 1745119770..e8eaa115c5 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -12,6 +12,7 @@ import { } from "./BlockMenuFactoryTypes"; import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension"; +import styles from "../../editor.module.css"; const serializeForClipboard = (pv as any).__serializeForClipboard; // code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799 @@ -166,6 +167,7 @@ function setDragImage(view: EditorView, from: number, to = from) { // dataTransfer.setDragImage(element) only works if element is attached to the DOM. dragImageElement = parentClone; + dragImageElement.className = styles.dragPreview; document.body.appendChild(dragImageElement); } From 421d0b69e62e262c43a5bc88ff1ea07444e45b8e Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 12 Jan 2023 20:25:51 +0100 Subject: [PATCH 41/55] Fixed tooltip styling --- packages/react/src/BlockNoteTheme.ts | 16 +++++++++ .../Tooltip/TooltipContent.module.css | 15 -------- .../Tooltip/components/TooltipContent.tsx | 34 ++++++++----------- 3 files changed, 31 insertions(+), 34 deletions(-) delete mode 100644 packages/react/src/SharedComponents/Tooltip/TooltipContent.module.css diff --git a/packages/react/src/BlockNoteTheme.ts b/packages/react/src/BlockNoteTheme.ts index 61b69bd9fc..e02250161a 100644 --- a/packages/react/src/BlockNoteTheme.ts +++ b/packages/react/src/BlockNoteTheme.ts @@ -112,6 +112,22 @@ export const BlockNoteTheme: MantineThemeOverride = { }, }), }, + Tooltip: { + styles: (theme) => ({ + root: { + color: theme.colors.brandFinal[2], + backgroundColor: theme.colors.brandFinal, + border: `1px solid ${theme.colors.brandFinal[1]}`, + borderRadius: "6px", + boxShadow: `0px 4px 8px ${theme.colors.brandFinal[2]}, 0px 0px 1px ${theme.colors.brandFinal[2]}`, + padding: "4px 10px", + textAlign: "center", + "div ~ div": { + color: theme.colors.brandFinal[4], + }, + }, + }), + }, SuggestionList: { styles: (theme) => ({ root: { diff --git a/packages/react/src/SharedComponents/Tooltip/TooltipContent.module.css b/packages/react/src/SharedComponents/Tooltip/TooltipContent.module.css deleted file mode 100644 index c687d30ccf..0000000000 --- a/packages/react/src/SharedComponents/Tooltip/TooltipContent.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.tooltip { - color: var(--N40); - background-color: var(--N800); - box-shadow: 0 0 10px rgba(253, 254, 255, 0.8), - 0 0 3px rgba(253, 254, 255, 0.4); - border-radius: 2px; - font-size: smaller; - text-align: center; - padding: 4px; -} - -.secondaryText { - font-weight: 400; - opacity: 0.6; -} diff --git a/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx index cdcfd0fecf..ddd5782822 100644 --- a/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx +++ b/packages/react/src/SharedComponents/Tooltip/components/TooltipContent.tsx @@ -1,23 +1,19 @@ -import styles from "../TooltipContent.module.css"; +import { createStyles, Stack, Text } from "@mantine/core"; -/** - * Helper for the tooltip for inline bubble menu buttons. - * - * Often used to display a tooltip showing the command name + keyboard shortcut, e.g.: - * - * Bold - * Ctrl+B - * - * TODO: maybe use default Tippy styles instead? - */ export const TooltipContent = (props: { mainTooltip: string; secondaryTooltip?: string; -}) => ( -
-
{props.mainTooltip}
- {props.secondaryTooltip && ( -
{props.secondaryTooltip}
- )} -
-); +}) => { + const { classes } = createStyles({ root: {} })(undefined, { + name: "Tooltip", + }); + + return ( + + {props.mainTooltip} + {props.secondaryTooltip && ( + {props.secondaryTooltip} + )} + + ); +}; From 182471836be5f5976edcf19224d5d23f490ac398 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 12 Jan 2023 20:39:14 +0100 Subject: [PATCH 42/55] Removed unnecessary updates from block menu --- .../extensions/DraggableBlocks/DraggableBlocksPlugin.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index e8eaa115c5..6ce566f14f 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -243,6 +243,8 @@ export class BlockMenuView { menuOpen = false; menuFrozen = false; + blockID: string | undefined; + constructor({ editor, blockMenuFactory, @@ -291,6 +293,12 @@ export class BlockMenuView { return; } + // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block. + if (this.menuOpen && this.blockID === block.id) { + return; + } + this.blockID = block.id; + // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position. const blockContent = block.node.firstChild as HTMLElement; From 36651468d0cd38efa24c5664e3966b5188aac2be Mon Sep 17 00:00:00 2001 From: Yousef Date: Fri, 13 Jan 2023 15:06:05 +0100 Subject: [PATCH 43/55] Vanilla example (#78) * add vanilla example * add bold toggle * add other factories to vanilla example * add dragging --- examples/editor/src/main.tsx | 32 ---------- examples/vanilla/.gitignore | 24 ++++++++ examples/vanilla/README.md | 3 + examples/vanilla/index.html | 13 ++++ examples/vanilla/package.json | 30 ++++++++++ examples/vanilla/src/index.css | 10 ++++ examples/vanilla/src/main.tsx | 31 ++++++++++ examples/vanilla/src/ui/blockMenuFactory.ts | 49 +++++++++++++++ examples/vanilla/src/ui/bubbleMenuFactory.ts | 47 +++++++++++++++ .../vanilla/src/ui/hyperlinkMenuFactory.ts | 48 +++++++++++++++ .../vanilla/src/ui/suggestionsMenuFactory.ts | 59 +++++++++++++++++++ examples/vanilla/src/ui/util.ts | 11 ++++ examples/vanilla/src/vite-env.d.ts | 1 + examples/vanilla/tsconfig.json | 27 +++++++++ examples/vanilla/tsconfig.node.json | 8 +++ examples/vanilla/vite.config.ts | 26 ++++++++ packages/core/src/BlockNoteEditor.ts | 4 +- .../BubbleMenu/BubbleMenuExtension.ts | 7 ++- .../DraggableBlocksExtension.ts | 6 +- .../extensions/Hyperlinks/HyperlinkMark.ts | 5 ++ .../Placeholder/PlaceholderExtension.ts | 4 +- .../SlashMenu/SlashMenuExtension.ts | 7 ++- 22 files changed, 413 insertions(+), 39 deletions(-) create mode 100644 examples/vanilla/.gitignore create mode 100644 examples/vanilla/README.md create mode 100644 examples/vanilla/index.html create mode 100644 examples/vanilla/package.json create mode 100644 examples/vanilla/src/index.css create mode 100644 examples/vanilla/src/main.tsx create mode 100644 examples/vanilla/src/ui/blockMenuFactory.ts create mode 100644 examples/vanilla/src/ui/bubbleMenuFactory.ts create mode 100644 examples/vanilla/src/ui/hyperlinkMenuFactory.ts create mode 100644 examples/vanilla/src/ui/suggestionsMenuFactory.ts create mode 100644 examples/vanilla/src/ui/util.ts create mode 100644 examples/vanilla/src/vite-env.d.ts create mode 100644 examples/vanilla/tsconfig.json create mode 100644 examples/vanilla/tsconfig.node.json create mode 100644 examples/vanilla/vite.config.ts diff --git a/examples/editor/src/main.tsx b/examples/editor/src/main.tsx index 789b8e0fc1..0808f1a4d8 100644 --- a/examples/editor/src/main.tsx +++ b/examples/editor/src/main.tsx @@ -11,35 +11,3 @@ root.render( ); - -// TODO: Separate non-React example using code below. -// import { mountBlockNoteEditor } from "@blocknote/core"; -// import "@blocknote/core/style.css"; -// import { -// ReactBubbleMenuFactory, -// ReactHyperlinkMenuFactory, -// ReactSuggestionsMenuFactory, -// } from "@blocknote/react"; -// import styles from "./App.module.css"; -// import "./index.css"; -// -// mountBlockNoteEditor( -// { -// bubbleMenuFactory: ReactBubbleMenuFactory, -// hyperlinkMenuFactory: ReactHyperlinkMenuFactory, -// suggestionsMenuFactory: ReactSuggestionsMenuFactory, -// }, -// { -// element: document.getElementById("root")!, -// onUpdate: ({ editor }) => { -// console.log(editor.getJSON()); -// (window as any).ProseMirror = editor; // Give tests a way to get editor instance -// }, -// editorProps: { -// attributes: { -// class: styles.editor, -// "data-test": "editor", -// }, -// }, -// } -// ); diff --git a/examples/vanilla/.gitignore b/examples/vanilla/.gitignore new file mode 100644 index 0000000000..a547bf36d8 --- /dev/null +++ b/examples/vanilla/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/vanilla/README.md b/examples/vanilla/README.md new file mode 100644 index 0000000000..868cba437c --- /dev/null +++ b/examples/vanilla/README.md @@ -0,0 +1,3 @@ +# Vanilla editor example + +This is an example client application that consumes @blocknote/core. diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html new file mode 100644 index 0000000000..dc902f4b5b --- /dev/null +++ b/examples/vanilla/index.html @@ -0,0 +1,13 @@ + + + + + + + BlockNote demo vanilla js + + +
+ + + diff --git a/examples/vanilla/package.json b/examples/vanilla/package.json new file mode 100644 index 0000000000..9f5a7e0cdf --- /dev/null +++ b/examples/vanilla/package.json @@ -0,0 +1,30 @@ +{ + "name": "@blocknote/example-vanilla", + "private": true, + "version": "0.1.2", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint src --max-warnings 0" + }, + "dependencies": { + "@blocknote/core": "^0.1.2" + }, + "devDependencies": { + "eslint": "^8.10.0", + "eslint-config-react-app": "^7.0.0", + "typescript": "^4.5.4", + "vite": "^3.0.5", + "vite-plugin-eslint": "^1.7.0" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ], + "rules": { + "curly": 1 + } + } +} diff --git a/examples/vanilla/src/index.css b/examples/vanilla/src/index.css new file mode 100644 index 0000000000..d8ab4ad4a3 --- /dev/null +++ b/examples/vanilla/src/index.css @@ -0,0 +1,10 @@ +html, +body, +#root { + height: 100%; +} + +.editor { + padding: 0 calc((100% - 731px) / 2); + height: 100%; +} diff --git a/examples/vanilla/src/main.tsx b/examples/vanilla/src/main.tsx new file mode 100644 index 0000000000..f0fe80d142 --- /dev/null +++ b/examples/vanilla/src/main.tsx @@ -0,0 +1,31 @@ +import { BlockNoteEditor } from "@blocknote/core"; +import "./index.css"; +import { blockMenuFactory } from "./ui/blockMenuFactory"; +import { bubbleMenuFactory } from "./ui/bubbleMenuFactory"; +import { hyperlinkMenuFactory } from "./ui/hyperlinkMenuFactory"; +import { suggestionsMenuFactory } from "./ui/suggestionsMenuFactory"; + +const editor = new BlockNoteEditor({ + element: document.getElementById("root")!, + uiFactories: { + // Create an example bubble menu which just consists of a bold toggle + bubbleMenuFactory, + // Create an example menu for hyperlinks + hyperlinkMenuFactory, + // Create an example menu for the /-menu + suggestionsMenuFactory, + // Create an example menu for when a block is hovered + blockMenuFactory, + }, + onUpdate: ({ editor }) => { + console.log(editor.getJSON()); + (window as any).ProseMirror = editor; // Give tests a way to get editor instance + }, + editorProps: { + attributes: { + class: "editor", + }, + }, +}); + +console.log("editor created", editor); diff --git a/examples/vanilla/src/ui/blockMenuFactory.ts b/examples/vanilla/src/ui/blockMenuFactory.ts new file mode 100644 index 0000000000..01bea80792 --- /dev/null +++ b/examples/vanilla/src/ui/blockMenuFactory.ts @@ -0,0 +1,49 @@ +import { BlockMenuFactory } from "@blocknote/core"; +import { createButton } from "./util"; + +/** + * This menu is drawn next to a block, when it's hovered over + * It renders a drag handle and + button to create a new block + */ +export const blockMenuFactory: BlockMenuFactory = (props) => { + const container = document.createElement("div"); + container.style.background = "gray"; + container.style.position = "absolute"; + container.style.padding = "10px"; + container.style.opacity = "0.8"; + const addBtn = createButton("+", () => { + props.addBlock(); + }); + container.appendChild(addBtn); + + const dragBtn = createButton("::", () => { + // TODO: render a submenu with a delete option that calls "props.deleteBlock" + }); + + dragBtn.addEventListener("dragstart", props.blockDragStart); + dragBtn.addEventListener("dragend", props.blockDragEnd); + container.style.display = "none"; + container.appendChild(dragBtn); + + document.body.appendChild(container); + + return { + element: container, + show: (params) => { + container.style.display = "block"; + console.log("show blockmenu", params); + container.style.top = params.blockBoundingBox.y + "px"; + container.style.left = + params.blockBoundingBox.x - container.offsetWidth + "px"; + }, + hide: () => { + container.style.display = "none"; + }, + update: (params) => { + console.log("update blockmenu", params); + container.style.top = params.blockBoundingBox.y + "px"; + container.style.left = + params.blockBoundingBox.x - container.offsetWidth + "px"; + }, + }; +}; diff --git a/examples/vanilla/src/ui/bubbleMenuFactory.ts b/examples/vanilla/src/ui/bubbleMenuFactory.ts new file mode 100644 index 0000000000..3db9f80c93 --- /dev/null +++ b/examples/vanilla/src/ui/bubbleMenuFactory.ts @@ -0,0 +1,47 @@ +import { BubbleMenuFactory } from "@blocknote/core"; +import { createButton } from "./util"; + +/** + * This menu is drawn when a piece of text is selected. We can use it to change formatting options + * such as bold, italic, indentation, etc. + */ +export const bubbleMenuFactory: BubbleMenuFactory = (props) => { + const container = document.createElement("div"); + container.style.background = "gray"; + container.style.position = "absolute"; + container.style.padding = "10px"; + container.style.opacity = "0.8"; + const boldBtn = createButton("set bold", () => { + props.toggleBold(); + }); + container.appendChild(boldBtn); + + const linkBtn = createButton("set link", () => { + props.setHyperlink("https://www.google.com"); + }); + + container.appendChild(boldBtn); + container.appendChild(linkBtn); + container.style.display = "none"; + document.body.appendChild(container); + + return { + element: container, + show: (params) => { + container.style.display = "block"; + console.log("show bubble", params); + boldBtn.text = params.boldIsActive ? "unset bold" : "set bold"; + container.style.top = params.selectionBoundingBox.y + "px"; + container.style.left = params.selectionBoundingBox.x + "px"; + }, + hide: () => { + container.style.display = "none"; + }, + update: (params) => { + console.log("update bubble", params); + boldBtn.text = params.boldIsActive ? "unset bold" : "set bold"; + container.style.top = params.selectionBoundingBox.y + "px"; + container.style.left = params.selectionBoundingBox.x + "px"; + }, + }; +}; diff --git a/examples/vanilla/src/ui/hyperlinkMenuFactory.ts b/examples/vanilla/src/ui/hyperlinkMenuFactory.ts new file mode 100644 index 0000000000..e07c5b678e --- /dev/null +++ b/examples/vanilla/src/ui/hyperlinkMenuFactory.ts @@ -0,0 +1,48 @@ +import { HyperlinkMenuFactory } from "@blocknote/core"; +import { createButton } from "./util"; + +/** + * This menu is drawn when the cursor is moved to a hyperlink (using the keyboard), + * or when the mouse is hovering over a hyperlink + */ +export const hyperlinkMenuFactory: HyperlinkMenuFactory = (props) => { + const container = document.createElement("div"); + container.style.background = "gray"; + container.style.position = "absolute"; + container.style.padding = "10px"; + container.style.opacity = "0.8"; + + const editBtn = createButton("edit", () => { + const newUrl = prompt("new url") || props.url; + props.editHyperlink(newUrl, props.text); + }); + container.appendChild(editBtn); + + const removeBtn = createButton("remove", () => { + props.deleteHyperlink(); + }); + + container.appendChild(editBtn); + container.appendChild(removeBtn); + container.style.display = "none"; + document.body.appendChild(container); + + return { + element: container, + show: (params) => { + container.style.display = "block"; + console.log("show", params); + + container.style.top = params.boundingBox.y + "px"; + container.style.left = params.boundingBox.x + "px"; + }, + hide: () => { + container.style.display = "none"; + }, + update: (params) => { + console.log("update", params); + container.style.top = params.boundingBox.y + "px"; + container.style.left = params.boundingBox.x + "px"; + }, + }; +}; diff --git a/examples/vanilla/src/ui/suggestionsMenuFactory.ts b/examples/vanilla/src/ui/suggestionsMenuFactory.ts new file mode 100644 index 0000000000..9e0effcdc5 --- /dev/null +++ b/examples/vanilla/src/ui/suggestionsMenuFactory.ts @@ -0,0 +1,59 @@ +import { SuggestionItem, SuggestionsMenuFactory } from "@blocknote/core"; +import { createButton } from "./util"; + +/** + * This menu is drawn when the cursor is moved to a hyperlink (using the keyboard), + * or when the mouse is hovering over a hyperlink + */ +export const suggestionsMenuFactory: SuggestionsMenuFactory = ( + props +) => { + const container = document.createElement("div"); + container.style.background = "gray"; + container.style.position = "absolute"; + container.style.padding = "10px"; + container.style.opacity = "0.8"; + container.style.display = "none"; + document.body.appendChild(container); + + function updateItems( + items: SuggestionItem[], + onClick: (item: SuggestionItem) => void, + selected: number + ) { + container.innerHTML = ""; + const domItems = items.map((val, i) => { + const element = createButton(val.name, () => { + onClick(val); + }); + element.style.display = "block"; + if (selected === i) { + element.style.fontWeight = "bold"; + } + return element; + }); + container.append(...domItems); + return domItems; + } + + return { + element: container, + show: (params) => { + updateItems(params.items, params.itemCallback, params.selectedItemIndex); + container.style.display = "block"; + console.log("show", params); + + container.style.top = params.queryStartBoundingBox.y + "px"; + container.style.left = params.queryStartBoundingBox.x + "px"; + }, + hide: () => { + container.style.display = "none"; + }, + update: (params) => { + console.log("update", params); + updateItems(params.items, params.itemCallback, params.selectedItemIndex); + container.style.top = params.queryStartBoundingBox.y + "px"; + container.style.left = params.queryStartBoundingBox.x + "px"; + }, + }; +}; diff --git a/examples/vanilla/src/ui/util.ts b/examples/vanilla/src/ui/util.ts new file mode 100644 index 0000000000..74e4c55eb1 --- /dev/null +++ b/examples/vanilla/src/ui/util.ts @@ -0,0 +1,11 @@ +export function createButton(text: string, onClick: () => void) { + const element = document.createElement("a"); + element.href = "#"; + element.text = text; + element.style.margin = "10px"; + element.addEventListener("click", (e) => { + onClick(); + e.preventDefault(); + }); + return element; +} diff --git a/examples/vanilla/src/vite-env.d.ts b/examples/vanilla/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/examples/vanilla/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/vanilla/tsconfig.json b/examples/vanilla/tsconfig.json new file mode 100644 index 0000000000..9387cb5ab6 --- /dev/null +++ b/examples/vanilla/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + // "paths": { + // "@blocknote/core": ["../../packages/core/src"] + // } + }, + "include": ["src"], + "references": [ + { "path": "./tsconfig.node.json" } + // { "path": "../../packages/core/tsconfig.json" } + ] +} diff --git a/examples/vanilla/tsconfig.node.json b/examples/vanilla/tsconfig.node.json new file mode 100644 index 0000000000..e993792cb1 --- /dev/null +++ b/examples/vanilla/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "esnext", + "moduleResolution": "node" + }, + "include": ["vite.config.ts"] +} diff --git a/examples/vanilla/vite.config.ts b/examples/vanilla/vite.config.ts new file mode 100644 index 0000000000..fc8bd4a83d --- /dev/null +++ b/examples/vanilla/vite.config.ts @@ -0,0 +1,26 @@ +import * as path from "path"; +import { defineConfig } from "vite"; +// import eslintPlugin from "vite-plugin-eslint"; +// https://vitejs.dev/config/ +export default defineConfig((conf) => ({ + plugins: [], + optimizeDeps: { + // link: ['vite-react-ts-components'], + }, + build: { + sourcemap: true, + }, + resolve: { + alias: + conf.command === "build" + ? {} + : { + // Comment out the lines below to load a built version of blocknote + // or, keep as is to load live from sources with live reload working + "@blocknote/core": path.resolve( + __dirname, + "../../packages/core/src/" + ), + }, + }, +})); diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 62c6ab7b2e..6c1be05862 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -12,12 +12,12 @@ import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsM export type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; disableHistoryExtension: boolean; - uiFactories: { + uiFactories: Partial<{ bubbleMenuFactory: BubbleMenuFactory; hyperlinkMenuFactory: HyperlinkMenuFactory; suggestionsMenuFactory: SuggestionsMenuFactory; blockMenuFactory: BlockMenuFactory; - }; + }>; }; const blockNoteExtensions = getBlockNoteExtensions(); diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts index a3f852b890..1df34ba98f 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts +++ b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts @@ -1,7 +1,7 @@ import { Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; -import { createBubbleMenuPlugin } from "./BubbleMenuPlugin"; import { BubbleMenuFactory } from "./BubbleMenuFactoryTypes"; +import { createBubbleMenuPlugin } from "./BubbleMenuPlugin"; /** * The menu that is displayed when selecting a piece of text. @@ -12,6 +12,11 @@ export const BubbleMenuExtension = Extension.create<{ name: "BubbleMenuExtension", addProseMirrorPlugins() { + if (!this.options.bubbleMenuFactory) { + console.warn("factories not defined for BubbleMenuExtension"); + return []; + } + return [ createBubbleMenuPlugin({ editor: this.editor, diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts index 11a5331b11..fcb1391a89 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts @@ -1,6 +1,6 @@ import { Editor, Extension } from "@tiptap/core"; -import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin"; import { BlockMenuFactory } from "./BlockMenuFactoryTypes"; +import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin"; export type DraggableBlocksOptions = { editor: Editor; @@ -17,6 +17,10 @@ export const DraggableBlocksExtension = name: "DraggableBlocksExtension", priority: 1000, // Need to be high, in order to hide draghandle when typing slash addProseMirrorPlugins() { + if (!this.options.blockMenuFactory) { + console.warn("factories not defined for DraggableBlocksExtension"); + return []; + } return [ createDraggableBlocksPlugin({ editor: this.editor, diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts b/packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts index f63199100d..e9449e2e10 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts +++ b/packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts @@ -12,6 +12,11 @@ import { const Hyperlink = Link.extend({ priority: 500, addProseMirrorPlugins() { + if (!this.options.hyperlinkMenuFactory) { + console.warn("factories not defined for Hyperlink"); + return [...(this.parent?.() || [])]; + } + return [ ...(this.parent?.() || []), createHyperlinkMenuPlugin(this.editor, { diff --git a/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts b/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts index 4a984d018b..aeb8d21cfb 100644 --- a/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts +++ b/packages/core/src/extensions/Placeholder/PlaceholderExtension.ts @@ -1,7 +1,7 @@ import { Editor, Extension } from "@tiptap/core"; import { Node as ProsemirrorNode } from "prosemirror-model"; -import { Decoration, DecorationSet } from "prosemirror-view"; import { Plugin } from "prosemirror-state"; +import { Decoration, DecorationSet } from "prosemirror-view"; import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension"; /** @@ -78,7 +78,7 @@ export const Placeholder = Extension.create({ } // If slash menu is of drag type and active, show the filter placeholder - if (menuState.type === "drag" && menuState.active) { + if (menuState?.type === "drag" && menuState?.active) { classes.push(this.options.isFilterClass); } // using widget, didn't work (caret position bug) diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts index e269dd5343..3c66e507f3 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts @@ -1,7 +1,7 @@ import { Extension } from "@tiptap/core"; import { PluginKey } from "prosemirror-state"; -import { SuggestionsMenuFactory } from "../../shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; import { createSuggestionPlugin } from "../../shared/plugins/suggestion/SuggestionPlugin"; +import { SuggestionsMenuFactory } from "../../shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; import defaultCommands from "./defaultCommands"; import { SlashMenuItem } from "./SlashMenuItem"; @@ -23,6 +23,11 @@ export const SlashMenuExtension = Extension.create({ }, addProseMirrorPlugins() { + if (!this.options.suggestionsMenuFactory) { + console.warn("factories not defined for SlashMenuExtension"); + return []; + } + return [ createSuggestionPlugin({ pluginKey: SlashMenuPluginKey, From c5e847954ba73da6336c69ede044bc90b9d2d1e8 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 13 Jan 2023 17:49:35 +0100 Subject: [PATCH 44/55] Updated naming for bubble, block, and hyperlink menus --- examples/vanilla/src/main.tsx | 14 +- ...MenuFactory.ts => blockSideMenuFactory.ts} | 4 +- ...Factory.ts => formattingToolbarFactory.ts} | 6 +- ...uFactory.ts => hyperlinkToolbarFactory.ts} | 4 +- packages/core/src/BlockNoteEditor.ts | 25 +-- packages/core/src/BlockNoteExtensions.ts | 6 +- .../BubbleMenu/BubbleMenuExtension.ts | 28 ---- ...yTypes.ts => BlockSideMenuFactoryTypes.ts} | 6 +- .../DraggableBlocksExtension.ts | 13 +- .../DraggableBlocks/DraggableBlocksPlugin.ts | 16 +- .../FormattingToolbarExtension.ts | 28 ++++ .../FormattingToolbarFactoryTypes.ts} | 6 +- .../FormattingToolbarPlugin.ts} | 151 ++++++++++-------- .../HyperlinkMark.ts | 14 +- .../HyperlinkToolbarFactoryTypes.ts} | 6 +- .../HyperlinkToolbarPlugin.ts} | 74 +++++---- packages/core/src/index.ts | 6 +- .../BlockSideMenuFactory.tsx} | 28 ++-- .../components/BlockSideMenu.tsx} | 7 +- .../FormattingToolbarFactory.tsx} | 60 +++---- .../components/FormattingToolbar.tsx} | 80 +++++----- .../components/LinkToolbarButton.tsx | 2 +- .../HyperlinkMenu/HyperlinkMenuFactory.tsx | 71 -------- .../components/EditHyperlinkMenu.tsx | 5 +- .../components/EditHyperlinkMenuItem.tsx | 0 .../components/EditHyperlinkMenuItemIcon.tsx | 0 .../components/EditHyperlinkMenuItemInput.tsx | 12 +- .../HyperlinkToolbarFactory.tsx | 70 ++++++++ .../components/HyperlinkToolbar.tsx} | 6 +- .../Toolbar/components/ToolbarButton.tsx | 2 +- packages/react/src/hooks/useBlockNote.ts | 15 +- packages/react/src/index.ts | 6 +- 32 files changed, 395 insertions(+), 376 deletions(-) rename examples/vanilla/src/ui/{blockMenuFactory.ts => blockSideMenuFactory.ts} (92%) rename examples/vanilla/src/ui/{bubbleMenuFactory.ts => formattingToolbarFactory.ts} (87%) rename examples/vanilla/src/ui/{hyperlinkMenuFactory.ts => hyperlinkToolbarFactory.ts} (90%) delete mode 100644 packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts rename packages/core/src/extensions/DraggableBlocks/{BlockMenuFactoryTypes.ts => BlockSideMenuFactoryTypes.ts} (62%) create mode 100644 packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts rename packages/core/src/extensions/{BubbleMenu/BubbleMenuFactoryTypes.ts => FormattingToolbar/FormattingToolbarFactoryTypes.ts} (79%) rename packages/core/src/extensions/{BubbleMenu/BubbleMenuPlugin.ts => FormattingToolbar/FormattingToolbarPlugin.ts} (68%) rename packages/core/src/extensions/{Hyperlinks => HyperlinkToolbar}/HyperlinkMark.ts (61%) rename packages/core/src/extensions/{Hyperlinks/HyperlinkMenuFactoryTypes.ts => HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts} (57%) rename packages/core/src/extensions/{Hyperlinks/HyperlinkMenuPlugin.ts => HyperlinkToolbar/HyperlinkToolbarPlugin.ts} (76%) rename packages/react/src/{BlockMenu/BlockMenuFactory.tsx => BlockSideMenu/BlockSideMenuFactory.tsx} (71%) rename packages/react/src/{BlockMenu/components/BlockMenu.tsx => BlockSideMenu/components/BlockSideMenu.tsx} (92%) rename packages/react/src/{BubbleMenu/BubbleMenuFactory.tsx => FormattingToolbar/FormattingToolbarFactory.tsx} (58%) rename packages/react/src/{BubbleMenu/components/BubbleMenu.tsx => FormattingToolbar/components/FormattingToolbar.tsx} (65%) rename packages/react/src/{BubbleMenu => FormattingToolbar}/components/LinkToolbarButton.tsx (92%) delete mode 100644 packages/react/src/HyperlinkMenu/HyperlinkMenuFactory.tsx rename packages/react/src/{HyperlinkMenu => HyperlinkToolbar}/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx (99%) rename packages/react/src/{HyperlinkMenu => HyperlinkToolbar}/EditHyperlinkMenu/components/EditHyperlinkMenuItem.tsx (100%) rename packages/react/src/{HyperlinkMenu => HyperlinkToolbar}/EditHyperlinkMenu/components/EditHyperlinkMenuItemIcon.tsx (100%) rename packages/react/src/{HyperlinkMenu => HyperlinkToolbar}/EditHyperlinkMenu/components/EditHyperlinkMenuItemInput.tsx (72%) create mode 100644 packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx rename packages/react/src/{HyperlinkMenu/components/HyperlinkMenu.tsx => HyperlinkToolbar/components/HyperlinkToolbar.tsx} (87%) diff --git a/examples/vanilla/src/main.tsx b/examples/vanilla/src/main.tsx index f0fe80d142..0719daac6a 100644 --- a/examples/vanilla/src/main.tsx +++ b/examples/vanilla/src/main.tsx @@ -1,21 +1,21 @@ import { BlockNoteEditor } from "@blocknote/core"; import "./index.css"; -import { blockMenuFactory } from "./ui/blockMenuFactory"; -import { bubbleMenuFactory } from "./ui/bubbleMenuFactory"; -import { hyperlinkMenuFactory } from "./ui/hyperlinkMenuFactory"; +import { blockSideMenuFactory } from "./ui/blockSideMenuFactory"; +import { formattingToolbarFactory } from "./ui/formattingToolbarFactory"; +import { hyperlinkToolbarFactory } from "./ui/hyperlinkToolbarFactory"; import { suggestionsMenuFactory } from "./ui/suggestionsMenuFactory"; const editor = new BlockNoteEditor({ element: document.getElementById("root")!, uiFactories: { - // Create an example bubble menu which just consists of a bold toggle - bubbleMenuFactory, + // Create an example formatting toolbar which just consists of a bold toggle + formattingToolbarFactory, // Create an example menu for hyperlinks - hyperlinkMenuFactory, + hyperlinkToolbarFactory, // Create an example menu for the /-menu suggestionsMenuFactory, // Create an example menu for when a block is hovered - blockMenuFactory, + blockSideMenuFactory, }, onUpdate: ({ editor }) => { console.log(editor.getJSON()); diff --git a/examples/vanilla/src/ui/blockMenuFactory.ts b/examples/vanilla/src/ui/blockSideMenuFactory.ts similarity index 92% rename from examples/vanilla/src/ui/blockMenuFactory.ts rename to examples/vanilla/src/ui/blockSideMenuFactory.ts index 01bea80792..575bf0c33d 100644 --- a/examples/vanilla/src/ui/blockMenuFactory.ts +++ b/examples/vanilla/src/ui/blockSideMenuFactory.ts @@ -1,11 +1,11 @@ -import { BlockMenuFactory } from "@blocknote/core"; +import { BlockSideMenuFactory } from "@blocknote/core"; import { createButton } from "./util"; /** * This menu is drawn next to a block, when it's hovered over * It renders a drag handle and + button to create a new block */ -export const blockMenuFactory: BlockMenuFactory = (props) => { +export const blockSideMenuFactory: BlockSideMenuFactory = (props) => { const container = document.createElement("div"); container.style.background = "gray"; container.style.position = "absolute"; diff --git a/examples/vanilla/src/ui/bubbleMenuFactory.ts b/examples/vanilla/src/ui/formattingToolbarFactory.ts similarity index 87% rename from examples/vanilla/src/ui/bubbleMenuFactory.ts rename to examples/vanilla/src/ui/formattingToolbarFactory.ts index 3db9f80c93..5fca01d15a 100644 --- a/examples/vanilla/src/ui/bubbleMenuFactory.ts +++ b/examples/vanilla/src/ui/formattingToolbarFactory.ts @@ -1,11 +1,11 @@ -import { BubbleMenuFactory } from "@blocknote/core"; +import { FormattingToolbarFactory } from "@blocknote/core"; import { createButton } from "./util"; /** * This menu is drawn when a piece of text is selected. We can use it to change formatting options * such as bold, italic, indentation, etc. */ -export const bubbleMenuFactory: BubbleMenuFactory = (props) => { +export const formattingToolbarFactory: FormattingToolbarFactory = (props) => { const container = document.createElement("div"); container.style.background = "gray"; container.style.position = "absolute"; @@ -29,7 +29,6 @@ export const bubbleMenuFactory: BubbleMenuFactory = (props) => { element: container, show: (params) => { container.style.display = "block"; - console.log("show bubble", params); boldBtn.text = params.boldIsActive ? "unset bold" : "set bold"; container.style.top = params.selectionBoundingBox.y + "px"; container.style.left = params.selectionBoundingBox.x + "px"; @@ -38,7 +37,6 @@ export const bubbleMenuFactory: BubbleMenuFactory = (props) => { container.style.display = "none"; }, update: (params) => { - console.log("update bubble", params); boldBtn.text = params.boldIsActive ? "unset bold" : "set bold"; container.style.top = params.selectionBoundingBox.y + "px"; container.style.left = params.selectionBoundingBox.x + "px"; diff --git a/examples/vanilla/src/ui/hyperlinkMenuFactory.ts b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts similarity index 90% rename from examples/vanilla/src/ui/hyperlinkMenuFactory.ts rename to examples/vanilla/src/ui/hyperlinkToolbarFactory.ts index e07c5b678e..46579b6d68 100644 --- a/examples/vanilla/src/ui/hyperlinkMenuFactory.ts +++ b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts @@ -1,11 +1,11 @@ -import { HyperlinkMenuFactory } from "@blocknote/core"; +import { HyperlinkToolbarFactory } from "@blocknote/core"; import { createButton } from "./util"; /** * This menu is drawn when the cursor is moved to a hyperlink (using the keyboard), * or when the mouse is hovering over a hyperlink */ -export const hyperlinkMenuFactory: HyperlinkMenuFactory = (props) => { +export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = (props) => { const container = document.createElement("div"); container.style.background = "gray"; container.style.position = "absolute"; diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index 6c1be05862..c6cf38dcc6 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -3,9 +3,9 @@ import { Editor, EditorOptions } from "@tiptap/core"; // import "./blocknote.css"; import { getBlockNoteExtensions } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; -import { BubbleMenuFactory } from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; -import { BlockMenuFactory } from "./extensions/DraggableBlocks/BlockMenuFactoryTypes"; -import { HyperlinkMenuFactory } from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; +import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes"; +import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; +import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; @@ -13,10 +13,10 @@ export type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; disableHistoryExtension: boolean; uiFactories: Partial<{ - bubbleMenuFactory: BubbleMenuFactory; - hyperlinkMenuFactory: HyperlinkMenuFactory; + formattingToolbarFactory: FormattingToolbarFactory; + hyperlinkToolbarFactory: HyperlinkToolbarFactory; suggestionsMenuFactory: SuggestionsMenuFactory; - blockMenuFactory: BlockMenuFactory; + blockSideMenuFactory: BlockSideMenuFactory; }>; }; @@ -39,20 +39,21 @@ export class BlockNoteEditor { // TODO: review extensions = extensions.map((extension) => { if ( - extension.name === "BubbleMenuExtension" && - options.uiFactories?.bubbleMenuFactory + extension.name === "FormattingToolbarExtension" && + options.uiFactories?.formattingToolbarFactory ) { return extension.configure({ - bubbleMenuFactory: options.uiFactories.bubbleMenuFactory, + formattingToolbarFactory: + options.uiFactories.formattingToolbarFactory, }); } if ( extension.name === "link" && - options.uiFactories?.hyperlinkMenuFactory + options.uiFactories?.hyperlinkToolbarFactory ) { return extension.configure({ - hyperlinkMenuFactory: options.uiFactories.hyperlinkMenuFactory, + hyperlinkToolbarFactory: options.uiFactories.hyperlinkToolbarFactory, }); } @@ -70,7 +71,7 @@ export class BlockNoteEditor { options.uiFactories ) { return extension.configure({ - blockMenuFactory: options.uiFactories.blockMenuFactory, + blockSideMenuFactory: options.uiFactories.blockSideMenuFactory, }); } diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index d6b117146f..e7a3acd7b2 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -13,9 +13,9 @@ import Text from "@tiptap/extension-text"; import Underline from "@tiptap/extension-underline"; import { blocks } from "./extensions/Blocks"; import blockStyles from "./extensions/Blocks/nodes/Block.module.css"; -import { BubbleMenuExtension } from "./extensions/BubbleMenu/BubbleMenuExtension"; +import { FormattingToolbarExtension } from "./extensions/FormattingToolbar/FormattingToolbarExtension"; import { DraggableBlocksExtension } from "./extensions/DraggableBlocks/DraggableBlocksExtension"; -import HyperlinkMark from "./extensions/Hyperlinks/HyperlinkMark"; +import HyperlinkMark from "./extensions/HyperlinkToolbar/HyperlinkMark"; import { FixedParagraph } from "./extensions/Paragraph/FixedParagraph"; import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; import SlashMenuExtension from "./extensions/SlashMenu"; @@ -72,7 +72,7 @@ export const getBlockNoteExtensions = () => { ...blocks, DraggableBlocksExtension, DropCursor.configure({ width: 5, color: "#ddeeff" }), - BubbleMenuExtension, + FormattingToolbarExtension, History, // This needs to be at the bottom of this list, because Key events (such as enter, when selecting a /command), // should be handled before Enter handlers in other components like splitListItem diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts b/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts deleted file mode 100644 index 1df34ba98f..0000000000 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuExtension.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Extension } from "@tiptap/core"; -import { PluginKey } from "prosemirror-state"; -import { BubbleMenuFactory } from "./BubbleMenuFactoryTypes"; -import { createBubbleMenuPlugin } from "./BubbleMenuPlugin"; - -/** - * The menu that is displayed when selecting a piece of text. - */ -export const BubbleMenuExtension = Extension.create<{ - bubbleMenuFactory: BubbleMenuFactory; -}>({ - name: "BubbleMenuExtension", - - addProseMirrorPlugins() { - if (!this.options.bubbleMenuFactory) { - console.warn("factories not defined for BubbleMenuExtension"); - return []; - } - - return [ - createBubbleMenuPlugin({ - editor: this.editor, - bubbleMenuFactory: this.options.bubbleMenuFactory, - pluginKey: new PluginKey("BubbleMenuPlugin"), - }), - ]; - }, -}); diff --git a/packages/core/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts similarity index 62% rename from packages/core/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.ts rename to packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts index d47d8f145c..4a4dbb4934 100644 --- a/packages/core/src/extensions/DraggableBlocks/BlockMenuFactoryTypes.ts +++ b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts @@ -1,6 +1,6 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; -export type BlockMenuParams = { +export type BlockSideMenuParams = { addBlock: () => void; deleteBlock: () => void; blockDragStart: (event: DragEvent) => void; @@ -10,5 +10,5 @@ export type BlockMenuParams = { blockBoundingBox: DOMRect; }; -export type BlockMenu = EditorElement; -export type BlockMenuFactory = ElementFactory; +export type BlockSideMenu = EditorElement; +export type BlockSideMenuFactory = ElementFactory; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts index fcb1391a89..baede67a6b 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts @@ -1,30 +1,31 @@ import { Editor, Extension } from "@tiptap/core"; -import { BlockMenuFactory } from "./BlockMenuFactoryTypes"; +import { BlockSideMenuFactory } from "./BlockSideMenuFactoryTypes"; import { createDraggableBlocksPlugin } from "./DraggableBlocksPlugin"; export type DraggableBlocksOptions = { editor: Editor; - blockMenuFactory: BlockMenuFactory; + blockSideMenuFactory: BlockSideMenuFactory; }; /** - * This extension adds a drag handle in front of all nodes with a "data-id" attribute + * This extension adds a menu to the side of blocks which features various BlockNote functions such as adding and + * removing blocks. More importantly, it adds a drag handle which allows the user to drag and drop blocks. * * code based on https://github.com/ueberdosis/tiptap/issues/323#issuecomment-506637799 */ export const DraggableBlocksExtension = Extension.create({ name: "DraggableBlocksExtension", - priority: 1000, // Need to be high, in order to hide draghandle when typing slash + priority: 1000, // Need to be high, in order to hide menu when typing slash addProseMirrorPlugins() { - if (!this.options.blockMenuFactory) { + if (!this.options.blockSideMenuFactory) { console.warn("factories not defined for DraggableBlocksExtension"); return []; } return [ createDraggableBlocksPlugin({ editor: this.editor, - blockMenuFactory: this.options.blockMenuFactory, + blockSideMenuFactory: this.options.blockSideMenuFactory, }), ]; }, diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 6ce566f14f..55c4bc8095 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -6,10 +6,10 @@ import { EditorView } from "prosemirror-view"; import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection"; import { DraggableBlocksOptions } from "./DraggableBlocksExtension"; import { - BlockMenu, - BlockMenuFactory, - BlockMenuParams, -} from "./BlockMenuFactoryTypes"; + BlockSideMenu, + BlockSideMenuFactory, + BlockSideMenuParams, +} from "./BlockSideMenuFactoryTypes"; import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension"; import styles from "../../editor.module.css"; @@ -226,7 +226,7 @@ function dragStart(e: DragEvent, view: EditorView) { export type BlockMenuViewProps = { editor: Editor; - blockMenuFactory: BlockMenuFactory; + blockMenuFactory: BlockSideMenuFactory; horizontalPosAnchoredAtRoot: boolean; }; @@ -237,8 +237,8 @@ export class BlockMenuView { // When false, the drag handle with be just to the left of the element horizontalPosAnchoredAtRoot: boolean; - blockMenuParams: BlockMenuParams; - blockMenu: BlockMenu; + blockMenuParams: BlockSideMenuParams; + blockMenu: BlockSideMenu; menuOpen = false; menuFrozen = false; @@ -436,7 +436,7 @@ export const createDraggableBlocksPlugin = ( view: () => new BlockMenuView({ editor: options.editor, - blockMenuFactory: options.blockMenuFactory, + blockMenuFactory: options.blockSideMenuFactory, horizontalPosAnchoredAtRoot: true, }), }); diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts new file mode 100644 index 0000000000..c3cdb35845 --- /dev/null +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts @@ -0,0 +1,28 @@ +import { Extension } from "@tiptap/core"; +import { PluginKey } from "prosemirror-state"; +import { FormattingToolbarFactory } from "./FormattingToolbarFactoryTypes"; +import { createFormattingToolbarPlugin } from "./FormattingToolbarPlugin"; + +/** + * The menu that is displayed when selecting a piece of text. + */ +export const FormattingToolbarExtension = Extension.create<{ + formattingToolbarFactory: FormattingToolbarFactory; +}>({ + name: "FormattingToolbarExtension", + + addProseMirrorPlugins() { + if (!this.options.formattingToolbarFactory) { + console.warn("factories not defined for FormattingToolbarExtension"); + return []; + } + + return [ + createFormattingToolbarPlugin({ + editor: this.editor, + formattingToolbarFactory: this.options.formattingToolbarFactory, + pluginKey: new PluginKey("FormattingToolbarPlugin"), + }), + ]; + }, +}); diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts similarity index 79% rename from packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts rename to packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts index 3fb9bbec8b..19fc726ce6 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuFactoryTypes.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts @@ -1,6 +1,6 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; -export type BubbleMenuParams = { +export type FormattingToolbarParams = { boldIsActive: boolean; toggleBold: () => void; italicIsActive: boolean; @@ -27,5 +27,5 @@ export type BubbleMenuParams = { editorElement: Element; }; -export type BubbleMenu = EditorElement; -export type BubbleMenuFactory = ElementFactory; +export type FormattingToolbar = EditorElement; +export type FormattingToolbarFactory = ElementFactory; diff --git a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts similarity index 68% rename from packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts rename to packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index 055f0c8fa0..d5c5ad0662 100644 --- a/packages/core/src/extensions/BubbleMenu/BubbleMenuPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -7,17 +7,17 @@ import { import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { - BubbleMenu, - BubbleMenuFactory, - BubbleMenuParams, -} from "./BubbleMenuFactoryTypes"; + FormattingToolbar, + FormattingToolbarFactory, + FormattingToolbarParams, +} from "./FormattingToolbarFactoryTypes"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files -export interface BubbleMenuPluginProps { +export interface FormattingToolbarPluginProps { pluginKey: PluginKey; editor: Editor; - bubbleMenuFactory: BubbleMenuFactory; + formattingToolbarFactory: FormattingToolbarFactory; shouldShow?: | ((props: { editor: Editor; @@ -30,54 +30,52 @@ export interface BubbleMenuPluginProps { | null; } -export type BubbleMenuViewProps = BubbleMenuPluginProps & { +export type FormattingToolbarViewProps = FormattingToolbarPluginProps & { view: EditorView; }; -export class BubbleMenuView { +export class FormattingToolbarView { public editor: Editor; public view: EditorView; - public bubbleMenuParams: BubbleMenuParams; + public formattingToolbarParams: FormattingToolbarParams; - public bubbleMenu: BubbleMenu; + public formattingToolbar: FormattingToolbar; public preventHide = false; public preventShow = false; - public menuIsOpen = false; + public toolbarIsOpen = false; - public shouldShow: Exclude = ({ - view, - state, - from, - to, - }) => { - const { doc, selection } = state; - const { empty } = selection; + public shouldShow: Exclude = + ({ view, state, from, to }) => { + const { doc, selection } = state; + const { empty } = selection; - // Sometime check for `empty` is not enough. - // Doubleclick an empty paragraph returns a node size of 2. - // So we check also for an empty text size. - const isEmptyTextBlock = - !doc.textBetween(from, to).length && isTextSelection(state.selection); + // Sometime check for `empty` is not enough. + // Doubleclick an empty paragraph returns a node size of 2. + // So we check also for an empty text size. + const isEmptyTextBlock = + !doc.textBetween(from, to).length && isTextSelection(state.selection); - return !(!view.hasFocus() || empty || isEmptyTextBlock); - }; + return !(!view.hasFocus() || empty || isEmptyTextBlock); + }; constructor({ editor, - bubbleMenuFactory, + formattingToolbarFactory, view, shouldShow, - }: BubbleMenuViewProps) { + }: FormattingToolbarViewProps) { this.editor = editor; this.view = view; - this.bubbleMenuParams = this.initBubbleMenuParams(); - this.bubbleMenu = bubbleMenuFactory(this.bubbleMenuParams); + this.formattingToolbarParams = this.initFormattingToolbarParams(); + this.formattingToolbar = formattingToolbarFactory( + this.formattingToolbarParams + ); if (shouldShow) { this.shouldShow = shouldShow; @@ -101,8 +99,8 @@ export class BubbleMenuView { }; dragstartHandler = () => { - this.bubbleMenu.hide(); - this.menuIsOpen = false; + this.formattingToolbar.hide(); + this.toolbarIsOpen = false; }; focusHandler = () => { @@ -119,14 +117,16 @@ export class BubbleMenuView { if ( event?.relatedTarget && - this.bubbleMenu.element?.parentNode?.contains(event.relatedTarget as Node) + this.formattingToolbar.element?.parentNode?.contains( + event.relatedTarget as Node + ) ) { return; } - if (this.menuIsOpen) { - this.bubbleMenu.hide(); - this.menuIsOpen = false; + if (this.toolbarIsOpen) { + this.formattingToolbar.hide(); + this.toolbarIsOpen = false; } }; @@ -156,17 +156,17 @@ export class BubbleMenuView { // Checks if menu should be shown. if ( - !this.menuIsOpen && + !this.toolbarIsOpen && !this.preventShow && (shouldShow || this.preventHide) ) { - this.updateBubbleMenuParams(); - this.bubbleMenu.show(this.bubbleMenuParams); - this.menuIsOpen = true; + this.updateFormattingToolbarParams(); + this.formattingToolbar.show(this.formattingToolbarParams); + this.toolbarIsOpen = true; // TODO: Is this necessary? Also for other menu plugins. // Listener stops focus moving to the menu on click. - this.bubbleMenu.element!.addEventListener("mousedown", (event) => + this.formattingToolbar.element!.addEventListener("mousedown", (event) => event.preventDefault() ); @@ -175,28 +175,29 @@ export class BubbleMenuView { // Checks if menu should be updated. if ( - this.menuIsOpen && + this.toolbarIsOpen && !this.preventShow && (shouldShow || this.preventHide) ) { - this.updateBubbleMenuParams(); - this.bubbleMenu.update(this.bubbleMenuParams); + this.updateFormattingToolbarParams(); + this.formattingToolbar.update(this.formattingToolbarParams); return; } // Checks if menu should be hidden. if ( - this.menuIsOpen && + this.toolbarIsOpen && !this.preventHide && (!shouldShow || this.preventShow) ) { - this.bubbleMenu.hide(); - this.menuIsOpen = false; + this.formattingToolbar.hide(); + this.toolbarIsOpen = false; // Listener stops focus moving to the menu on click. - this.bubbleMenu.element!.removeEventListener("mousedown", (event) => - event.preventDefault() + this.formattingToolbar.element!.removeEventListener( + "mousedown", + (event) => event.preventDefault() ); return; @@ -232,7 +233,7 @@ export class BubbleMenuView { return posToDOMRect(this.editor.view, from, to); } - initBubbleMenuParams(): BubbleMenuParams { + initFormattingToolbarParams(): FormattingToolbarParams { return { boldIsActive: this.editor.isActive("bold"), toggleBold: () => { @@ -255,7 +256,9 @@ export class BubbleMenuView { this.editor.commands.toggleStrike(); }, hyperlinkIsActive: this.editor.isActive("link"), - activeHyperlinkUrl: this.editor.getAttributes("link").href, + activeHyperlinkUrl: this.editor.getAttributes("link").href + ? this.editor.getAttributes("link").href + : "", activeHyperlinkText: this.editor.state.doc.textBetween( this.editor.state.selection.from, this.editor.state.selection.to @@ -323,38 +326,48 @@ export class BubbleMenuView { }; } - updateBubbleMenuParams() { - this.bubbleMenuParams.boldIsActive = this.editor.isActive("bold"); - this.bubbleMenuParams.italicIsActive = this.editor.isActive("italic"); - this.bubbleMenuParams.underlineIsActive = this.editor.isActive("underline"); - this.bubbleMenuParams.strikeIsActive = this.editor.isActive("strike"); - this.bubbleMenuParams.hyperlinkIsActive = this.editor.isActive("link"); - this.bubbleMenuParams.activeHyperlinkUrl = - this.editor.getAttributes("link").href; - this.bubbleMenuParams.activeHyperlinkText = + updateFormattingToolbarParams() { + this.formattingToolbarParams.boldIsActive = this.editor.isActive("bold"); + this.formattingToolbarParams.italicIsActive = + this.editor.isActive("italic"); + this.formattingToolbarParams.underlineIsActive = + this.editor.isActive("underline"); + this.formattingToolbarParams.strikeIsActive = + this.editor.isActive("strike"); + this.formattingToolbarParams.hyperlinkIsActive = + this.editor.isActive("link"); + this.formattingToolbarParams.activeHyperlinkUrl = this.editor.getAttributes( + "link" + ).href + ? this.editor.getAttributes("link").href + : ""; + this.formattingToolbarParams.activeHyperlinkText = this.editor.state.doc.textBetween( this.editor.state.selection.from, this.editor.state.selection.to ); - this.bubbleMenuParams.paragraphIsActive = + this.formattingToolbarParams.paragraphIsActive = this.editor.state.selection.$from.node().type.name === "textContent"; - this.bubbleMenuParams.headingIsActive = + this.formattingToolbarParams.headingIsActive = this.editor.state.selection.$from.node().type.name === "headingContent"; - this.bubbleMenuParams.activeHeadingLevel = + this.formattingToolbarParams.activeHeadingLevel = this.editor.state.selection.$from.node().attrs["headingLevel"]; - this.bubbleMenuParams.listItemIsActive = + this.formattingToolbarParams.listItemIsActive = this.editor.state.selection.$from.node().type.name === "listItemContent"; - this.bubbleMenuParams.activeListItemType = + this.formattingToolbarParams.activeListItemType = this.editor.state.selection.$from.node().attrs["listItemType"]; - this.bubbleMenuParams.selectionBoundingBox = this.getSelectionBoundingBox(); + this.formattingToolbarParams.selectionBoundingBox = + this.getSelectionBoundingBox(); } } -export const createBubbleMenuPlugin = (options: BubbleMenuPluginProps) => { +export const createFormattingToolbarPlugin = ( + options: FormattingToolbarPluginProps +) => { return new Plugin({ - key: new PluginKey("BubbleMenuPlugin"), - view: (view) => new BubbleMenuView({ view, ...options }), + key: new PluginKey("FormattingToolbarPlugin"), + view: (view) => new FormattingToolbarView({ view, ...options }), }); }; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts similarity index 61% rename from packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts rename to packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts index e9449e2e10..26bb404d21 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMark.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts @@ -1,26 +1,26 @@ import { Link } from "@tiptap/extension-link"; import { - createHyperlinkMenuPlugin, - HyperlinkMenuPluginProps, -} from "./HyperlinkMenuPlugin"; + createHyperlinkToolbarPlugin, + HyperlinkToolbarPluginProps, +} from "./HyperlinkToolbarPlugin"; /** * This custom link includes a special menu for editing/deleting/opening the link. * The menu will be triggered by hovering over the link with the mouse, * or by moving the cursor inside the link text */ -const Hyperlink = Link.extend({ +const Hyperlink = Link.extend({ priority: 500, addProseMirrorPlugins() { - if (!this.options.hyperlinkMenuFactory) { + if (!this.options.hyperlinkToolbarFactory) { console.warn("factories not defined for Hyperlink"); return [...(this.parent?.() || [])]; } return [ ...(this.parent?.() || []), - createHyperlinkMenuPlugin(this.editor, { - hyperlinkMenuFactory: this.options.hyperlinkMenuFactory, + createHyperlinkToolbarPlugin(this.editor, { + hyperlinkToolbarFactory: this.options.hyperlinkToolbarFactory, }), ]; }, diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts similarity index 57% rename from packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts rename to packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts index 3d45862a67..fa1cc6758a 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuFactoryTypes.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts @@ -1,6 +1,6 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; -export type HyperlinkMenuParams = { +export type HyperlinkToolbarParams = { url: string; text: string; editHyperlink: (url: string, text: string) => void; @@ -10,5 +10,5 @@ export type HyperlinkMenuParams = { editorElement: Element; }; -export type HyperlinkMenu = EditorElement; -export type HyperlinkMenuFactory = ElementFactory; +export type HyperlinkToolbar = EditorElement; +export type HyperlinkToolbarFactory = ElementFactory; diff --git a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts similarity index 76% rename from packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts rename to packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 1bc9bb4676..1dc4dddd58 100644 --- a/packages/core/src/extensions/Hyperlinks/HyperlinkMenuPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -2,26 +2,26 @@ import { Editor, getMarkRange, posToDOMRect, Range } from "@tiptap/core"; import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; import { - HyperlinkMenu, - HyperlinkMenuFactory, - HyperlinkMenuParams, -} from "./HyperlinkMenuFactoryTypes"; -const PLUGIN_KEY = new PluginKey("HyperlinkMenuPlugin"); - -export type HyperlinkMenuPluginProps = { - hyperlinkMenuFactory: HyperlinkMenuFactory; + HyperlinkToolbar, + HyperlinkToolbarFactory, + HyperlinkToolbarParams, +} from "./HyperlinkToolbarFactoryTypes"; +const PLUGIN_KEY = new PluginKey("HyperlinkToolbarPlugin"); + +export type HyperlinkToolbarPluginProps = { + hyperlinkToolbarFactory: HyperlinkToolbarFactory; }; -export type HyperlinkMenuViewProps = { +export type HyperlinkToolbarViewProps = { editor: Editor; - hyperlinkMenuFactory: HyperlinkMenuFactory; + hyperlinkToolbarFactory: HyperlinkToolbarFactory; }; -class HyperlinkMenuView { +class HyperlinkToolbarView { editor: Editor; - hyperlinkMenuParams: HyperlinkMenuParams; - hyperlinkMenu: HyperlinkMenu; + hyperlinkToolbarParams: HyperlinkToolbarParams; + hyperlinkToolbar: HyperlinkToolbar; menuUpdateTimer: NodeJS.Timeout | undefined; startMenuUpdateTimer: () => void; @@ -36,11 +36,13 @@ class HyperlinkMenuView { hyperlinkMark: Mark | undefined; hyperlinkMarkRange: Range | undefined; - constructor({ editor, hyperlinkMenuFactory }: HyperlinkMenuViewProps) { + constructor({ editor, hyperlinkToolbarFactory }: HyperlinkToolbarViewProps) { this.editor = editor; - this.hyperlinkMenuParams = this.initHyperlinkMenuParams(); - this.hyperlinkMenu = hyperlinkMenuFactory(this.hyperlinkMenuParams); + this.hyperlinkToolbarParams = this.initHyperlinkToolbarParams(); + this.hyperlinkToolbar = hyperlinkToolbarFactory( + this.hyperlinkToolbarParams + ); this.startMenuUpdateTimer = () => { this.menuUpdateTimer = setTimeout(() => { @@ -147,17 +149,17 @@ class HyperlinkMenuView { } if (this.hyperlinkMark) { - this.updateHyperlinkMenuParams(); + this.updateHyperlinkToolbarParams(); // Shows menu. if (!prevHyperlinkMark) { - this.hyperlinkMenu.show(this.hyperlinkMenuParams); + this.hyperlinkToolbar.show(this.hyperlinkToolbarParams); - this.hyperlinkMenu.element?.addEventListener( + this.hyperlinkToolbar.element?.addEventListener( "mouseleave", this.startMenuUpdateTimer ); - this.hyperlinkMenu.element?.addEventListener( + this.hyperlinkToolbar.element?.addEventListener( "mouseenter", this.stopMenuUpdateTimer ); @@ -166,27 +168,27 @@ class HyperlinkMenuView { } // Updates menu. - this.hyperlinkMenu.update(this.hyperlinkMenuParams); + this.hyperlinkToolbar.update(this.hyperlinkToolbarParams); } // Hides menu. if (!this.hyperlinkMark && prevHyperlinkMark) { - this.hyperlinkMenu.element?.removeEventListener( + this.hyperlinkToolbar.element?.removeEventListener( "mouseleave", this.startMenuUpdateTimer ); - this.hyperlinkMenu.element?.removeEventListener( + this.hyperlinkToolbar.element?.removeEventListener( "mouseenter", this.stopMenuUpdateTimer ); - this.hyperlinkMenu.hide(); + this.hyperlinkToolbar.hide(); return; } } - initHyperlinkMenuParams(): HyperlinkMenuParams { + initHyperlinkToolbarParams(): HyperlinkToolbarParams { return { url: "", text: "", @@ -204,7 +206,7 @@ class HyperlinkMenuView { this.editor.view.dispatch(tr); this.editor.view.focus(); - this.hyperlinkMenu.hide(); + this.hyperlinkToolbar.hide(); }, deleteHyperlink: () => { this.editor.view.dispatch( @@ -218,7 +220,7 @@ class HyperlinkMenuView { ); this.editor.view.focus(); - this.hyperlinkMenu.hide(); + this.hyperlinkToolbar.hide(); }, boundingBox: new DOMRect(), @@ -226,17 +228,19 @@ class HyperlinkMenuView { }; } - updateHyperlinkMenuParams() { + updateHyperlinkToolbarParams() { if (this.hyperlinkMark) { - this.hyperlinkMenuParams.url = this.hyperlinkMark.attrs.href; - this.hyperlinkMenuParams.text = this.editor.view.state.doc.textBetween( + this.hyperlinkToolbarParams.url = this.hyperlinkMark.attrs.href + ? this.hyperlinkMark.attrs.href + : ""; + this.hyperlinkToolbarParams.text = this.editor.view.state.doc.textBetween( this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to ); } if (this.hyperlinkMarkRange) { - this.hyperlinkMenuParams.boundingBox = posToDOMRect( + this.hyperlinkToolbarParams.boundingBox = posToDOMRect( this.editor.view, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to @@ -245,16 +249,16 @@ class HyperlinkMenuView { } } -export const createHyperlinkMenuPlugin = ( +export const createHyperlinkToolbarPlugin = ( editor: Editor, - options: HyperlinkMenuPluginProps + options: HyperlinkToolbarPluginProps ) => { return new Plugin({ key: PLUGIN_KEY, view: () => - new HyperlinkMenuView({ + new HyperlinkToolbarView({ editor: editor, - hyperlinkMenuFactory: options.hyperlinkMenuFactory, + hyperlinkToolbarFactory: options.hyperlinkToolbarFactory, }), }); }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d1aa3eb99e..d7c8f2396c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,7 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; -export * from "./extensions/BubbleMenu/BubbleMenuFactoryTypes"; -export * from "./extensions/DraggableBlocks/BlockMenuFactoryTypes"; -export * from "./extensions/Hyperlinks/HyperlinkMenuFactoryTypes"; +export * from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes"; +export * from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; +export * from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; diff --git a/packages/react/src/BlockMenu/BlockMenuFactory.tsx b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx similarity index 71% rename from packages/react/src/BlockMenu/BlockMenuFactory.tsx rename to packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx index 6465b46508..6060c3045c 100644 --- a/packages/react/src/BlockMenu/BlockMenuFactory.tsx +++ b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx @@ -1,19 +1,23 @@ -import { BlockMenu, BlockMenuFactory, BlockMenuParams } from "@blocknote/core"; import { - BlockMenu as ReactBlockMenu, - BlockMenuProps, -} from "../BlockMenu/components/BlockMenu"; + BlockSideMenu, + BlockSideMenuFactory, + BlockSideMenuParams, +} from "@blocknote/core"; +import { + BlockSideMenu as ReactSideBlockMenu, + BlockSideMenuProps, +} from "./components/BlockSideMenu"; import { createRoot, Root } from "react-dom/client"; import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../BlockNoteTheme"; import Tippy from "@tippyjs/react"; -export const ReactBlockMenuFactory: BlockMenuFactory = ( - params: BlockMenuParams -): BlockMenu => { - const blockMenuProps: BlockMenuProps = { ...params }; +export const ReactBlockSideMenuFactory: BlockSideMenuFactory = ( + params: BlockSideMenuParams +): BlockSideMenu => { + const blockMenuProps: BlockSideMenuProps = { ...params }; - function updateBlockMenuProps(params: BlockMenuParams) { + function updateBlockMenuProps(params: BlockSideMenuParams) { blockMenuProps.addBlock = params.addBlock; blockMenuProps.deleteBlock = params.deleteBlock; } @@ -29,7 +33,7 @@ export const ReactBlockMenuFactory: BlockMenuFactory = ( } + content={} duration={0} getReferenceClientRect={() => params.blockBoundingBox} hideOnClick={false} @@ -45,7 +49,7 @@ export const ReactBlockMenuFactory: BlockMenuFactory = ( return { element: menuRootElement, - show: (params: BlockMenuParams) => { + show: (params: BlockSideMenuParams) => { updateBlockMenuProps(params); document.body.appendChild(menuRootElement); @@ -58,7 +62,7 @@ export const ReactBlockMenuFactory: BlockMenuFactory = ( menuRootElement.remove(); }, - update: (params: BlockMenuParams) => { + update: (params: BlockSideMenuParams) => { updateBlockMenuProps(params); menuRoot!.render(getMenuComponent()); diff --git a/packages/react/src/BlockMenu/components/BlockMenu.tsx b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx similarity index 92% rename from packages/react/src/BlockMenu/components/BlockMenu.tsx rename to packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx index 095af1c681..e033455749 100644 --- a/packages/react/src/BlockMenu/components/BlockMenu.tsx +++ b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx @@ -2,7 +2,7 @@ import { AiOutlinePlus, MdDragIndicator } from "react-icons/all"; import { ActionIcon, createStyles, Group, Menu } from "@mantine/core"; import { useEffect, useRef } from "react"; -export type BlockMenuProps = { +export type BlockSideMenuProps = { addBlock: () => void; deleteBlock: () => void; blockDragStart: (event: DragEvent) => void; @@ -11,9 +11,9 @@ export type BlockMenuProps = { unfreezeMenu: () => void; }; -export const BlockMenu = (props: BlockMenuProps) => { +export const BlockSideMenu = (props: BlockSideMenuProps) => { const { classes } = createStyles({ root: {} })(undefined, { - name: "DragHandleMenu", + name: "DragSideHandleMenu", }); const dragHandleRef = useRef(null); @@ -41,7 +41,6 @@ export const BlockMenu = (props: BlockMenuProps) => { { - console.log("OPEN MENU"); props.addBlock(); }} /> diff --git a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx similarity index 58% rename from packages/react/src/BubbleMenu/BubbleMenuFactory.tsx rename to packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx index 572341f222..bbc155cf51 100644 --- a/packages/react/src/BubbleMenu/BubbleMenuFactory.tsx +++ b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx @@ -1,23 +1,23 @@ import { createRoot, Root } from "react-dom/client"; import { - BubbleMenu, - BubbleMenuFactory, - BubbleMenuParams, + FormattingToolbar, + FormattingToolbarParams, + FormattingToolbarFactory, } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; import Tippy from "@tippyjs/react"; import { - BubbleMenu as ReactBubbleMenu, - BubbleMenuProps, -} from "./components/BubbleMenu"; + FormattingToolbar as ReactFormattingToolbar, + FormattingToolbarProps, +} from "./components/FormattingToolbar"; import { BlockNoteTheme } from "../BlockNoteTheme"; // import rootStyles from "../../../core/src/root.module.css"; -export const ReactBubbleMenuFactory: BubbleMenuFactory = ( - params: BubbleMenuParams -): BubbleMenu => { +export const ReactFormattingToolbarFactory: FormattingToolbarFactory = ( + params: FormattingToolbarParams +): FormattingToolbar => { // TODO: Maybe just use {...params}? - const bubbleMenuProps: BubbleMenuProps = { + const formattingToolbarProps: FormattingToolbarProps = { boldIsActive: params.boldIsActive, toggleBold: params.toggleBold, italicIsActive: params.italicIsActive, @@ -41,20 +41,20 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( setListItem: params.setListItem, }; - function updateBubbleMenuProps(params: BubbleMenuParams) { - bubbleMenuProps.boldIsActive = params.boldIsActive; - bubbleMenuProps.italicIsActive = params.italicIsActive; - bubbleMenuProps.underlineIsActive = params.underlineIsActive; - bubbleMenuProps.strikeIsActive = params.strikeIsActive; - bubbleMenuProps.hyperlinkIsActive = params.hyperlinkIsActive; - bubbleMenuProps.activeHyperlinkUrl = params.activeHyperlinkUrl; - bubbleMenuProps.activeHyperlinkText = params.activeHyperlinkText; + function updateFormattingToolbarProps(params: FormattingToolbarParams) { + formattingToolbarProps.boldIsActive = params.boldIsActive; + formattingToolbarProps.italicIsActive = params.italicIsActive; + formattingToolbarProps.underlineIsActive = params.underlineIsActive; + formattingToolbarProps.strikeIsActive = params.strikeIsActive; + formattingToolbarProps.hyperlinkIsActive = params.hyperlinkIsActive; + formattingToolbarProps.activeHyperlinkUrl = params.activeHyperlinkUrl; + formattingToolbarProps.activeHyperlinkText = params.activeHyperlinkText; - bubbleMenuProps.paragraphIsActive = params.paragraphIsActive; - bubbleMenuProps.headingIsActive = params.headingIsActive; - bubbleMenuProps.activeHeadingLevel = params.activeHeadingLevel; - bubbleMenuProps.listItemIsActive = params.listItemIsActive; - bubbleMenuProps.activeListItemType = params.activeListItemType; + formattingToolbarProps.paragraphIsActive = params.paragraphIsActive; + formattingToolbarProps.headingIsActive = params.headingIsActive; + formattingToolbarProps.activeHeadingLevel = params.activeHeadingLevel; + formattingToolbarProps.listItemIsActive = params.listItemIsActive; + formattingToolbarProps.activeListItemType = params.activeListItemType; } // We don't use the document body as a root as it would cause multiple React roots to be created on a single element @@ -68,7 +68,11 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( } + content={ + + } duration={0} getReferenceClientRect={() => params.selectionBoundingBox} hideOnClick={false} @@ -83,8 +87,8 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( return { element: menuRootElement, - show: (params: BubbleMenuParams) => { - updateBubbleMenuProps(params); + show: (params: FormattingToolbarParams) => { + updateFormattingToolbarProps(params); document.body.appendChild(menuRootElement); menuRoot = createRoot(menuRootElement); @@ -96,8 +100,8 @@ export const ReactBubbleMenuFactory: BubbleMenuFactory = ( menuRootElement.remove(); }, - update: (params: BubbleMenuParams) => { - updateBubbleMenuProps(params); + update: (params: FormattingToolbarParams) => { + updateFormattingToolbarProps(params); menuRoot!.render(getMenuComponent()); }, diff --git a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx similarity index 65% rename from packages/react/src/BubbleMenu/components/BubbleMenu.tsx rename to packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx index a7e4039d6c..412badb49b 100644 --- a/packages/react/src/BubbleMenu/components/BubbleMenu.tsx +++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx @@ -19,7 +19,7 @@ import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; -export type BubbleMenuProps = { +export type FormattingToolbarProps = { boldIsActive: boolean; toggleBold: () => void; italicIsActive: boolean; @@ -44,36 +44,39 @@ export type BubbleMenuProps = { }; // TODO: add list options, indentation -export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { +export const FormattingToolbar = (props: { + formattingToolbarProps: FormattingToolbarProps; +}) => { const getActiveMarks = () => { const activeMarks = new Set(); - props.bubbleMenuProps.boldIsActive && activeMarks.add("bold"); - props.bubbleMenuProps.italicIsActive && activeMarks.add("italic"); - props.bubbleMenuProps.underlineIsActive && activeMarks.add("underline"); - props.bubbleMenuProps.strikeIsActive && activeMarks.add("strike"); - props.bubbleMenuProps.hyperlinkIsActive && activeMarks.add("link"); + props.formattingToolbarProps.boldIsActive && activeMarks.add("bold"); + props.formattingToolbarProps.italicIsActive && activeMarks.add("italic"); + props.formattingToolbarProps.underlineIsActive && + activeMarks.add("underline"); + props.formattingToolbarProps.strikeIsActive && activeMarks.add("strike"); + props.formattingToolbarProps.hyperlinkIsActive && activeMarks.add("link"); return activeMarks; }; const getActiveBlock = () => { - if (props.bubbleMenuProps.headingIsActive) { - if (props.bubbleMenuProps.activeHeadingLevel === "1") { + if (props.formattingToolbarProps.headingIsActive) { + if (props.formattingToolbarProps.activeHeadingLevel === "1") { return { text: "Heading 1", icon: RiH1, }; } - if (props.bubbleMenuProps.activeHeadingLevel === "2") { + if (props.formattingToolbarProps.activeHeadingLevel === "2") { return { text: "Heading 2", icon: RiH2, }; } - if (props.bubbleMenuProps.activeHeadingLevel === "3") { + if (props.formattingToolbarProps.activeHeadingLevel === "3") { return { text: "Heading 3", icon: RiH3, @@ -81,8 +84,8 @@ export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { } } - if (props.bubbleMenuProps.listItemIsActive) { - if (props.bubbleMenuProps.activeListItemType === "unordered") { + if (props.formattingToolbarProps.listItemIsActive) { + if (props.formattingToolbarProps.activeListItemType === "unordered") { return { text: "Bullet List", icon: RiListUnordered, @@ -111,76 +114,77 @@ export const BubbleMenu = (props: { bubbleMenuProps: BubbleMenuProps }) => { icon={activeBlock!.icon} items={[ { - onClick: () => props.bubbleMenuProps.setParagraph(), + onClick: () => props.formattingToolbarProps.setParagraph(), text: "Text", icon: RiText, - isSelected: props.bubbleMenuProps.paragraphIsActive, + isSelected: props.formattingToolbarProps.paragraphIsActive, }, { - onClick: () => props.bubbleMenuProps.setHeading("1"), + onClick: () => props.formattingToolbarProps.setHeading("1"), text: "Heading 1", icon: RiH1, isSelected: - props.bubbleMenuProps.headingIsActive && - props.bubbleMenuProps.activeHeadingLevel === "1", + props.formattingToolbarProps.headingIsActive && + props.formattingToolbarProps.activeHeadingLevel === "1", }, { - onClick: () => props.bubbleMenuProps.setHeading("2"), + onClick: () => props.formattingToolbarProps.setHeading("2"), text: "Heading 2", icon: RiH2, isSelected: - props.bubbleMenuProps.headingIsActive && - props.bubbleMenuProps.activeHeadingLevel === "2", + props.formattingToolbarProps.headingIsActive && + props.formattingToolbarProps.activeHeadingLevel === "2", }, { - onClick: () => props.bubbleMenuProps.setHeading("3"), + onClick: () => props.formattingToolbarProps.setHeading("3"), text: "Heading 3", icon: RiH3, isSelected: - props.bubbleMenuProps.headingIsActive && - props.bubbleMenuProps.activeHeadingLevel === "3", + props.formattingToolbarProps.headingIsActive && + props.formattingToolbarProps.activeHeadingLevel === "3", }, { - onClick: () => props.bubbleMenuProps.setListItem("unordered"), + onClick: () => + props.formattingToolbarProps.setListItem("unordered"), text: "Bullet List", icon: RiListUnordered, isSelected: - props.bubbleMenuProps.listItemIsActive && - props.bubbleMenuProps.activeListItemType === "unordered", + props.formattingToolbarProps.listItemIsActive && + props.formattingToolbarProps.activeListItemType === "unordered", }, { - onClick: () => props.bubbleMenuProps.setListItem("ordered"), + onClick: () => props.formattingToolbarProps.setListItem("ordered"), text: "Numbered List", icon: RiListOrdered, isSelected: - props.bubbleMenuProps.listItemIsActive && - props.bubbleMenuProps.activeListItemType === "ordered", + props.formattingToolbarProps.listItemIsActive && + props.formattingToolbarProps.activeListItemType === "ordered", }, ]} /> { mainTooltip="Link" secondaryTooltip={formatKeyboardShortcut("Mod+K")} icon={RiLink} - hyperlinkIsActive={props.bubbleMenuProps.hyperlinkIsActive} - activeHyperlinkUrl={props.bubbleMenuProps.activeHyperlinkUrl} - activeHyperlinkText={props.bubbleMenuProps.activeHyperlinkText} - setHyperlink={props.bubbleMenuProps.setHyperlink} + hyperlinkIsActive={props.formattingToolbarProps.hyperlinkIsActive} + activeHyperlinkUrl={props.formattingToolbarProps.activeHyperlinkUrl} + activeHyperlinkText={props.formattingToolbarProps.activeHyperlinkText} + setHyperlink={props.formattingToolbarProps.setHyperlink} /> {/* { - const hyperlinkMenuProps: HyperlinkMenuProps = { ...params }; - - function updateHyperlinkMenuProps(params: HyperlinkMenuParams) { - hyperlinkMenuProps.url = params.url; - hyperlinkMenuProps.text = params.text; - } - - // We don't use the document body as a root as it would cause multiple React roots to be created on a single element - // if other menu factories do the same. - const menuRootElement = document.createElement("div"); - // menuRootElement.className = rootStyles.bnRoot; - let menuRoot: Root | undefined; - - function getMenuComponent() { - return ( - - } - duration={0} - getReferenceClientRect={() => params.boundingBox} - hideOnClick={false} - interactive={true} - placement={"top"} - showOnCreate={true} - trigger={"manual"} - /> - - ); - } - - return { - element: menuRootElement, - show: (params: HyperlinkMenuParams) => { - updateHyperlinkMenuProps(params); - - document.body.appendChild(menuRootElement); - menuRoot = createRoot(menuRootElement); - - menuRoot.render(getMenuComponent()); - }, - hide: () => { - menuRoot!.unmount(); - - menuRootElement.remove(); - }, - update: (params: HyperlinkMenuParams) => { - updateHyperlinkMenuProps(params); - - menuRoot!.render(getMenuComponent()); - }, - }; -}; diff --git a/packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx b/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx similarity index 99% rename from packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx rename to packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx index aa931c57e5..cd5898dc8a 100644 --- a/packages/react/src/HyperlinkMenu/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkToolbar/EditHyperlinkMenu/components/EditHyperlinkMenu.tsx @@ -14,12 +14,13 @@ export type EditHyperlinkMenuProps = { * Provides input fields for setting the hyperlink URL and title. */ export const EditHyperlinkMenu = (props: EditHyperlinkMenuProps) => { - const [url, setUrl] = useState(props.url); - const [title, setTitle] = useState(props.text); const { classes } = createStyles({ root: {} })(undefined, { name: "EditHyperlinkMenu", }); + const [url, setUrl] = useState(props.url); + const [title, setTitle] = useState(props.text); + return ( (null); - - useEffect(() => { - setTimeout(() => { - props.autofocus && inputRef.current?.focus(); - }); - }, [props.autofocus]); - function handleEnter(event: KeyboardEvent) { if (event.key === "Enter") { event.preventDefault(); @@ -29,12 +21,12 @@ export function EditHyperlinkMenuItemInput( return ( props.onChange(event.currentTarget.value)} onKeyDown={handleEnter} placeholder={props.placeholder} - ref={inputRef} /> ); } diff --git a/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx b/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx new file mode 100644 index 0000000000..61f2189f2a --- /dev/null +++ b/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx @@ -0,0 +1,70 @@ +import { createRoot, Root } from "react-dom/client"; +import { + HyperlinkToolbar, + HyperlinkToolbarFactory, + HyperlinkToolbarParams, +} from "@blocknote/core"; +import { MantineProvider } from "@mantine/core"; +import Tippy from "@tippyjs/react"; +import { + HyperlinkToolbar as ReactHyperlinkToolbar, + HyperlinkToolbarProps, +} from "./components/HyperlinkToolbar"; +import { BlockNoteTheme } from "../BlockNoteTheme"; +// import rootStyles from "../../../core/src/root.module.css"; + +export const ReactHyperlinkToolbarFactory: HyperlinkToolbarFactory = ( + params: HyperlinkToolbarParams +): HyperlinkToolbar => { + const hyperlinkToolbarProps: HyperlinkToolbarProps = { ...params }; + + function updateHyperlinkToolbarProps(params: HyperlinkToolbarParams) { + hyperlinkToolbarProps.url = params.url; + hyperlinkToolbarProps.text = params.text; + } + + // We don't use the document body as a root as it would cause multiple React roots to be created on a single element + // if other UI factories do the same. + const rootElement = document.createElement("div"); + let root: Root | undefined; + + function getComponent() { + return ( + + } + duration={0} + getReferenceClientRect={() => params.boundingBox} + hideOnClick={false} + interactive={true} + placement={"top"} + showOnCreate={true} + trigger={"manual"} + /> + + ); + } + + return { + element: rootElement, + show: (params: HyperlinkToolbarParams) => { + updateHyperlinkToolbarProps(params); + + document.body.appendChild(rootElement); + root = createRoot(rootElement); + + root.render(getComponent()); + }, + hide: () => { + root!.unmount(); + + rootElement.remove(); + }, + update: (params: HyperlinkToolbarParams) => { + updateHyperlinkToolbarProps(params); + + root!.render(getComponent()); + }, + }; +}; diff --git a/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbar.tsx similarity index 87% rename from packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx rename to packages/react/src/HyperlinkToolbar/components/HyperlinkToolbar.tsx index 67b32ce0f4..f2ce16b66b 100644 --- a/packages/react/src/HyperlinkMenu/components/HyperlinkMenu.tsx +++ b/packages/react/src/HyperlinkToolbar/components/HyperlinkToolbar.tsx @@ -5,7 +5,7 @@ import { ToolbarButton } from "../../SharedComponents/Toolbar/components/Toolbar import { RiExternalLinkFill, RiLinkUnlink } from "react-icons/ri"; // import rootStyles from "../../../root.module.css"; -export type HyperlinkMenuProps = { +export type HyperlinkToolbarProps = { url: string; text: string; editHyperlink: (url: string, text: string) => void; @@ -14,9 +14,9 @@ export type HyperlinkMenuProps = { /** * Main menu component for the hyperlink extension. - * Either renders a menu to create/edit a hyperlink, or a menu to interact with it on mouse hover. + * Renders a toolbar that appears on hyperlink hover. */ -export const HyperlinkMenu = (props: HyperlinkMenuProps) => { +export const HyperlinkToolbar = (props: HyperlinkToolbarProps) => { const [isEditing, setIsEditing] = useState(false); if (isEditing) { diff --git a/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx b/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx index 7e0879e6a3..61767afe40 100644 --- a/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx +++ b/packages/react/src/SharedComponents/Toolbar/components/ToolbarButton.tsx @@ -15,7 +15,7 @@ export type ToolbarButtonProps = { }; /** - * Helper for basic buttons that show in the inline bubble menu. + * Helper for basic buttons that show in the formatting toolbar. */ export const ToolbarButton = forwardRef((props: ToolbarButtonProps, ref) => { const ButtonIcon = props.icon; diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index f992d1a49e..243de21240 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -1,9 +1,9 @@ import { BlockNoteEditor, BlockNoteEditorOptions } from "@blocknote/core"; import { DependencyList, useEffect, useState } from "react"; -import { ReactBubbleMenuFactory } from "../BubbleMenu/BubbleMenuFactory"; -import { ReactHyperlinkMenuFactory } from "../HyperlinkMenu/HyperlinkMenuFactory"; +import { ReactFormattingToolbarFactory } from "../FormattingToolbar/FormattingToolbarFactory"; +import { ReactHyperlinkToolbarFactory } from "../HyperlinkToolbar/HyperlinkToolbarFactory"; import { ReactSuggestionsMenuFactory } from "../SuggestionsMenu/SuggestionsMenuFactory"; -import { ReactBlockMenuFactory } from "../BlockMenu/BlockMenuFactory"; +import { ReactBlockSideMenuFactory } from "../BlockSideMenu/BlockSideMenuFactory"; //based on https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/useEditor.ts @@ -14,7 +14,7 @@ function useForceUpdate() { } /** - * Main hook for importing a BlockNote editor into a react project + * Main hook for importing a BlockNote editor into a React project */ export const useBlockNote = ( options: Partial = {}, @@ -22,7 +22,6 @@ export const useBlockNote = ( ) => { const [editor, setEditor] = useState(null); const forceUpdate = useForceUpdate(); - // useEditorForceUpdate(editor.tiptapEditor); useEffect(() => { let isMounted = true; @@ -31,10 +30,10 @@ export const useBlockNote = ( newOptions = { ...newOptions, uiFactories: { - bubbleMenuFactory: ReactBubbleMenuFactory, - hyperlinkMenuFactory: ReactHyperlinkMenuFactory, + formattingToolbarFactory: ReactFormattingToolbarFactory, + hyperlinkToolbarFactory: ReactHyperlinkToolbarFactory, suggestionsMenuFactory: ReactSuggestionsMenuFactory, - blockMenuFactory: ReactBlockMenuFactory, + blockSideMenuFactory: ReactBlockSideMenuFactory, }, }; } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 808ca5a82a..040533dead 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,8 +1,8 @@ // TODO: review directories export * from "./BlockNoteView"; -export * from "./BlockMenu/BlockMenuFactory"; -export * from "./BubbleMenu/BubbleMenuFactory"; +export * from "./BlockSideMenu/BlockSideMenuFactory"; +export * from "./FormattingToolbar/FormattingToolbarFactory"; export * from "./hooks/useBlockNote"; export * from "./hooks/useEditorForceUpdate"; -export * from "./HyperlinkMenu/HyperlinkMenuFactory"; +export * from "./HyperlinkToolbar/HyperlinkToolbarFactory"; export * from "./SuggestionsMenu/SuggestionsMenuFactory"; From 1be11e56246edefcb03b248cf3013113c4fe9cf7 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 13 Jan 2023 18:57:36 +0100 Subject: [PATCH 45/55] Fixed drag handle tests --- .../DraggableBlocks/DraggableBlocksPlugin.ts | 13 +++++++++---- .../components/BlockSideMenu.tsx | 7 +++++-- .../end-to-end/draghandle/draghandle.test.ts | 4 +++- .../draghandlemenu-firefox-linux.png | Bin 30664 -> 30621 bytes 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 55c4bc8095..06e4f39e4d 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -308,9 +308,6 @@ export class BlockMenuView { // Gets bounding box of the block content. const blockBoundingBox = blockContent.getBoundingClientRect(); - blockBoundingBox.x = this.horizontalPosAnchoredAtRoot - ? getHorizontalAnchor() - : blockBoundingBox.left; this.blockMenuParams.addBlock = () => this.addBlock({ @@ -322,7 +319,14 @@ export class BlockMenuView { left: blockBoundingBox.left, top: blockBoundingBox.top, }); - this.blockMenuParams.blockBoundingBox = blockBoundingBox; + this.blockMenuParams.blockBoundingBox = new DOMRect( + this.horizontalPosAnchoredAtRoot + ? getHorizontalAnchor() + : blockBoundingBox.x, + blockBoundingBox.y, + blockBoundingBox.width, + blockBoundingBox.height + ); // Shows or updates elements. if (!this.menuOpen) { @@ -388,6 +392,7 @@ export class BlockMenuView { if (blockInfo === undefined) { return; } + console.log(blockInfo); const { contentNode, endPos } = blockInfo; diff --git a/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx index e033455749..9ea03e45e0 100644 --- a/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx +++ b/packages/react/src/BlockSideMenu/components/BlockSideMenu.tsx @@ -13,7 +13,7 @@ export type BlockSideMenuProps = { export const BlockSideMenu = (props: BlockSideMenuProps) => { const { classes } = createStyles({ root: {} })(undefined, { - name: "DragSideHandleMenu", + name: "DragHandleMenu", }); const dragHandleRef = useRef(null); @@ -46,7 +46,10 @@ export const BlockSideMenu = (props: BlockSideMenuProps) => { /> } -
+
{ await page.keyboard.type("Hover over this text"); const heading = await page.locator(H_ONE_BLOCK_SELECTOR); await moveMouseOverElement(page, heading); + await page.waitForSelector(DRAG_HANDLE_SELECTOR); }); @@ -117,6 +118,7 @@ test.describe("Check Draghandle functionality", () => { const heading = await page.locator(H_ONE_BLOCK_SELECTOR); await moveMouseOverElement(page, heading); await page.click(DRAG_HANDLE_ADD_SELECTOR); + await page.waitForSelector(SLASH_MENU_SELECTOR); }); @@ -125,6 +127,7 @@ test.describe("Check Draghandle functionality", () => { await page.keyboard.type("Hover over this text"); await hoverAndAddBlockFromDragHandle(page, H_ONE_BLOCK_SELECTOR, "h2"); await page.waitForSelector(DRAG_HANDLE_SELECTOR, { state: "detached" }); + await page.waitForSelector(DRAG_HANDLE_ADD_SELECTOR, { state: "detached" }); }); @@ -156,7 +159,6 @@ test.describe("Check Draghandle functionality", () => { await page.waitForSelector(H_TWO_BLOCK_SELECTOR, { state: "detached" }); await page.waitForSelector(H_THREE_BLOCK_SELECTOR); - // Compare doc object snapshot await compareDocToSnapshot(page, "dragHandleDocStructure"); }); diff --git a/tests/end-to-end/draghandle/draghandle.test.ts-snapshots/draghandlemenu-firefox-linux.png b/tests/end-to-end/draghandle/draghandle.test.ts-snapshots/draghandlemenu-firefox-linux.png index 3960ede1003ec61894fe21fd1146a809d10cbf55..33cc6b28d42f4081245e1338fa3eb19bf72e376c 100644 GIT binary patch literal 30621 zcmeIbXIPV2_cwY|6l@?Cno`tJRGLT!Au0k&RZ%(wK|l-;P?`h?*ifpCCQXMDloDDf zfe@6Y6cG?Y3qcStBm}8}ge2#7M&>^=&-3M+>zwzz=Q{Ak`$DqswO3!iwf4^Ai)My9 zwu)~B0AR<3^XD!Dz$WmK`!oM$@So05q9p)G0~gMnxf0|^CGv&aUnQ!K>YY9RU4n}& zeLO(_4Da5OSKWxD5k%x?#PgBV#Sz5by*WpxVT$1@9!5`hNk`NpIAtl<1L$T0#DQlG zx7X%(oeZQ6b8df9AhBLTu#mD`6_>Z_b+O=PrLk!sGt9F zx|y51K1SQ>zfsK_-u%y9{7G)iCf;zdz@qN|vQ2&<_P@}|Ew+sxP^Q`E2>s_KcLL%( zf8*0HT6s?j?*!b2tBnr*=O(2AmCgU<)z{^uf%dA~2?qbU$$lb`#WayFGQOAzxaPE`();?Z$Ol^_**CrWu zr-6Rbdm2fRTk6U@ZvF{+DAO#?#wqDOI&4&3|K(Znenppd(=Gql(`f}zkoLjzdKV21 zC)JO2q`u%vWZAe$>wh3;$^pX&GSPQ7_l==xfHblIkdP=`JD((R?kX6i!d&cEy;!hoCm z^tj9Utycus@*X0UdS{6AX4G60njVegT~qG|CS$4@|)_umF9QbOgUa;^eeZY2*oZvltrj^a~4QR-P17m6*^94KO)F> z%^3Q<7l(TcM0~V*)kS}JDa#lL<(2rF|5{l;Q<1yA#6{LP+&$g3nJYB$iH~AlJe3C|-wJDbzeYx5ncgg$OLJB=1?(GFe7J4#K_XM@Ew1t@} zVxmV!uayU5O^<@>1;?5R+KnBfbTs-CF z+6TSlF_F3ZJ-(j~a-pB+B48~EG)smj06zW2aW>(-JBzDAPPlPx1t5E%MNz+f7LLe@ z(@`P@GaYj1j8C`Pa(N79yB2S6mIc=6Wt}ch3Pg>>K6_#PBw>0@puk2fk+X)h?xDL>4G#KdwXD1T7gFxSSAr3Y484E>U2v0$Wk=ZS}O4C@K+C@B*GQx z9e$(Vb!2L2yF4*{u4%TnZ|(h4Pfw`u@`meWNuAPK&2P4;* zZa`v8B0mthLn_h0RQ1|sCnY92K&)6SysnXdr)Vqpa|9sD!C{vfsZmgMD*>LA&y;z29DM z&Ap%Fy~T(Abr+YD+-mIM`nd)-Ii5J=tcFn4tzGA%hyY({%3oS=qM9%Kd1V+KB?9g! z>%Z8CVxue{|N{Q&CO59cFJ{!;bRa`gwf3exybJuwB= zRSYE1mL=1Fvk;-vgCAss0VF6&fy=371SwI*Imo}C!(JKN`6N)bjHd&j z>e?R!V(!DQxC+*PO_;!A%79JljN-0;-K%}Ku#a)i0%lfp@J~@cd>9V++*rJa%G&~r zZy)97Xt{9(><4xsII$9R?}<*{@ClF?OA?yBk6InWcPDXKne(Xe6w?U(RnBhY2K=g5 z@b|BzY(pugRWN&1q$jy@9c4wD>5&l51f0x<>Ljz- z@rFG;oom{-)yd?a@hit8H$|&MjZ)z8^nr*`G?KjFk%A6YHHEPJOyM;h0yS4@Hhq(8 zANG@{+Z1mE$5)L$C9!p9N?X2T3AE~=xCEVXam-5CN+g7>$r?)%%tfv(bnQWP#vPlE z)Ai|5ikY(Le6dBOmy^3vpA?(SKrZcagT&5SgW;xk*Fd1%uof_B3pH0h){4 z0~JBnG4QQO_8+Pw6triOec;q=*Q$ zjC6pUBzWpeYfcVfID|Ffkt^oz$zZcZTc#X*E?`H0Z!>}*n#J6X)|nxn^Y% zgUSj6)1>dO!-3*RCxuyPL@SqclOaEVn6$Uy z9zFrYY`Ggt0OIABtz&Ewl|P;ELJ1;gmeget+v4QHma=y&9j>1h<|6hIMv@iq^Mi|c zLX_AG`Encg6iu&-p3_oa_0q8R=vdv+&qn7h`rK0PO!_sbCaKKC2hC2}&06#7#3zin zcg4%6ebT19z;^EB`nK1~!D-d(!(tg;YG2*ne zfaYp)R6bf3XBv}}T7;A9IMJ}^51ATahCTgMGdi{iM*NB^B<^kPNopfLxWz)aLXTeo zW*1=aD0p}aD%TE<&mWpH_vUiq{mLg1aK)mHzbs(DBlZ0c%8m}EIAHK2upzcG*H{66 z9JkorCcgx?U+k)$G8*#6*f1C;jWKH(1|kCIihH5sD>H#37bpdIo8XCFD`Vqf=bqSZ zgM9@Sm21qf0HoIFlS9VP_?;{B#-C)_^S+a<6rQhN5(WHs|0+XHi-7gY0i|r-=}qA! ze4|wd)1rbtZ?C60SZ)ELIi`D!!;RemH38tzH|980+fBD!qrs0{7AQ9OiwE`P!gOWQ5QvDOOW;ChCe1V>%v!#?QbRUvT! z>;crCvaQ-N6il)MX6z=VFI2~^Q~!}=-|uxSL9K&2bKfjdCaYUrhG`(gW6vrkMPFn&tWROR|&Sm*@|3F?Wcw- zSGL^?E;F&a^C9_7Ri~u3`kL`$OIlCnyS~#gr#z4Q7xO&1&fXadtc+gE9 zbdh!BB?9TYMJQyA9oRFXpLY?hx($FVkej;2+%utc)(k(8)$9;m7c)^D>QQn%3?E8A zPh$6}8^OyO6c9NaW<#bCGLic9d-C?iApUDW))gOT`hkecP#Ntpde&S zIdxMwn5~{0yfwsdxpr#{0IyTsFCa#wh z6cjYEJyB^sehr2i-+s9C5a}aU!KS$S!_$MFZ8JlbB(ouSLZ7wQM7KLA<}m-qiYVmX zGC^p_9K;_+g#n_7D*Y9D4at1#8)i4EWV<@!x4UgcGYscDlXEiZmch(7Q91|UHRQKu zTxST|;B`xJe(mvX?ng{DCte&|HcKt)ClyxcCzl{Dkq+UR__`OWbGQmNoe@5BEr&DWj?8ZZK?ge87_M38&!akI~@Y;=@gLKWc z_A%_8ZZ}Hd=O2SsP{bjJ&~fvg#C-{TM@=Dld?JggKur8}KaY=nrE8t$440@fC(OaK zn3UJ_PM`$uSe|T3KYMSdY|-PxiIEb+ET2g0XX0%>ZMOGKD~Um_L@}E=|Kjdx%2ffI zqp%^bvOBi!9lcmw`IIhSE?;zm$uwqVxGDOc$z-}aZOkcVE+OTlZ{E2qilP=LMuk+n z;8&A9u)WExp%TsIlqHti+ZiXdC}Kz5?eTI9c;KFisO!#*wY`5T$o%qR5e79vqFjSgk%&F;<)*lkvS@P5Ne(-sNeG z#&`FOvD426D{M-xR8R>F9RK;g-jv!{pL$ zH$Nt_=L;Zd!9CCsy($mI)YQ@Z`_ z%P%R1%G3%Y6#hxf#IA5a^Ga%EKATa+%gZlxczQOCqh#78T7z z@JX2=Vs)!%hRs`^mc;j|N9XwU4m?wmoMn+adoH!4fAQ%oejX1w9{P15bH;Dhn!T?r z+(pyJMtkiEt7emEFJ8Op<%ym)hDDvVsljj2QI8PKL&gVwql6v8NT~~R5m40NV&XLW%y)frDuI*Q1bdI*! zQf=-G=ucfS+k*D$;4T=1xeVNqdyww3JYdnD@%B?L(Y3`ZxHqX+y=hl_DUZR}(yUyi zr_ZAD&aEs5!EVKqdE4OB`af3$EfDNN!a~?FL=KdEDqzg0Z*;(-#c|&%Bx@*aPoj8ar&RwoI`Y^(E=6B%EZ6A%vd%<-VK{nmz`4RwduX-kF0%&B+QFD=@?7nAurq=IQCT!;$`m_BS7QxcvVICp?J*$O(wWf zVLoi*I|UJFu&WlccAGK!vF|SAg`NUT!7>t?<1Kn`>93j`F&nUYsM$_y0i-7^ zb(ZWWl$=w(Uxu>%Cg_4|ljco2+KD;}Dx6_*)@}a`|Gw|lk(U9{wjhFi;5atE=DlED z(()jV9a9tJk~VDzce^S|@|$r5t5!Gaq8L78^z|O5egYi~aE6LVQ3?Ni!sJKn^ z_T{e~K4X&~c+{A;TIhQsCYU*2h>|l5Q z9do8f^NHtA%@EMJL0arUD-x5uVJjmM{J^2cx^H)t_dP_FAnn_83kkHU)tBBfAJrCQ zL054nP%d5`rldTVrfhce`(By8#15a-3XSiF6Sx2ACjg&UExukIC5<8JJJc1^t0mfA zbV8V4T8~Y*w!In0M|8P6Bxxk2R5ZLnhfc75!O;|FK8*+Dl#(2B!s8L|>>TH`9iS>`wu;;oNL>D=`O1pggCyw=1s70;+^9-T9U{9$Fc zp-|^HOWmX}f18)VU$R}Ja1k_q0E*rMxPR9JGz$0uP}CE<0JUu)&dZj(hOxGCQp6An zbUDhXUg;n%F2eGb#7TG*>BVce4R`jxh7q1GM zMqW&%`x_ZwUlKU}XeSk$uhppl+O$gQ)4pyV)#@A4ASaad^5aF616(G(ah4Tpbt%vR z!m6v4eb0{<)Z};AcMMpdT@g`$VoB>N$OUP1iWx8RSAFTKNs&_~=*W2#s#;#q_O#<5 zOlGDQd*39~`RrKF_Z+}{FW>^!-0r7j);eZ6miH0vdznB+^5mLd%l7h3(c?FgmURh-$dnp_v{E zj}Rbsb4x)z zJK5B3mJ9T!a=`u=-(gH*Khi&aNj<-&1!^DW2-f^6w$U#hd#?{SF0vkw|xABkttT&dM1%I zc@nGxm&wbkvD2`eBFm!3Hurm<7mYLA4m1y-GLv;zAM4vy`=ilfkANTV7tGAJ$yc3j za1WuaiX7+D`vP-)XQhlU1x~Q-ioYJQLnU_&-i)>=ggEj>bcy)pnepDU5Ro}eH-uSQ zfYsB9X5SROtbs09!t9^@1Z_@^$stmm){-HqT4vn@GQhTFzvyM|dqP0n;yfpc#ikie z*#{dlJW4pE{be0#3RPnjt)4hzvmpqGCHaxRJFn8@lsleO4EbTqltI?h$B@tq#&USq z;L$$i9z(ax!>lq;NF5z?^P~)4CkEAd8_JFrChc&0p|kq|Du#Ajs{9bJ-%jpAm^-zm z$$;Jy5S&49M3$Z8@wq;iFH?945M?%`hrGX}Q%!7yLFLWP6G5C{x~`JCXLKp7Tt9d+ z#od_HGremdXh8|0s|mU|i5&Xs%I{LfQyBxHq$2)kOWROfcMisvztKzGp_hF6E{NXc z?fT&7Rf2N3Z|D2Wo6wh6=XfBgmh2u#vLBEt9TeXbJ{MCWNcfT?t%av<0W0tb}B9#Qsu=ECVjA(t-v_j_w8a4BBp4q=_1^gx3?tz zn-;7nQMc~0DcL#UoPxI-E*h~$W{7mRHEzOHs( zVtP4j;rSyEHTXbp!RtESi4lvgFJqV>*C;Mcx*jgak&;MZH0CEW*L!|;xT`42OIH0= zVW0@x_wbn714UQ~!IHY8&q#Zmx?n*iUhSJq4b1Q(KX6Z~>pMEEvHU@=`fXV2kP$2wGU&4|JU|hOXFa&9xX-Ts8w+ z3WnX1%rB7esb-j>X<7TJ+m#5_w+Nhn-}xcW0VRZ7UF5kRO#H!O+MsjAsQd$WUJVC3 zaOvX(526J$eMse0^&=AByQr|Y38MuM;=O`CMk)c@pNO^@O5S!RN670aIe0vnkVUB^ z#lFcHMg@D7DRuqf*zD1tU}(H%dQhvFsaS9s+yk2aQuoES_2B_8u%TKZ)Kkb%IB}87 ziInMggZLWFHEO{LJJ59CXm3UPv&{T8H37(*iq*;0^DmJZapC0&YA?9!HMlg)xD{ag z3MwrJZ!W~z9XpopGcQKQ?(DDVZ(CN)2Zg?v}A*H1P~^-9eMW~Ahn-xfinJ6>)F_Frmm z_r`!q-`nXcvF^VleunU)Si>Q(6Nt+KL8<;bn@7u=Jn?Z8*Q?usVGt4l%O+XxVKM`Y&oPJJ@v1k=UJ#xK~Ur-lD<~q3WX`##SXFcj&ZCtYd2iS_K z8=8p}X+y$mX{FAaC%5NhN*!jkJ7l@nd{!!SIrf zp7&JAm-QaGVO}7ssDnRQL*tVzysA}JFv4}HH!@DoV66CQsjza#*uWqWG}7AnX-f3K z<%t7O>Lt<0gvJBEb=YP}`&JCNnG&G>zC^oWi~A~kQUQKs3Sze8c2u}mP5SbA!AFz8JLQ$sinzfwV`X((!(sLb|>YlKNfqXw7Z;GX!Ga*koMb1CYO^JzIo*JT#~TT zbCvh?cs5JwnAuV9K4(tHzUTq0gJ7}8a$ONxech)zz=CF$2tC++o*835GA!-nI^zd# zx@)Tk!=dT6p$dfcnWI6l+3OkkJEGhaZ2oJv27oHY{Kl9SH!=?I)S}Jl9_Ffm)!ad56nu-a0jlDRI?(Hw7C?NXy zoS zjs8|ys(0Gi#!C)eNO0cbBe%)>OHmcJKNH=L%6HVJl#ll~XRe(i62`MTuM&n&Gzrc* z*vavwFC4o4LV*<%KA-EnI=k4@3kjcCN6Qw=VDuac@%wQk!D&;tP@VoxruRq5kh$!V zN(ZVhA&)hl8K2WpSp<1rl;9f9w?E_@c6JI+T~_KcSIQ93$X+|~Y)}u{nxS-ea<8Qg zYVmr%^;SuQ(kGRk>;~(>E>y?an0Ep|lHD?e{E9{Qw!Nu6E&9PCJXQG0)Lmowh+u z_p}$fb93>kGr!kqsj>i(^+~M3qaeo)<=QPd{N$M49~=Wty#GCcU=6{|8LVC|uAW>w z&-kc`%1PdPxzG8S4YiCu--dosCuwYe#*MvKDd4{$jtP!@)c7$y%OKVSjb|HFRw^#_ z9`5dnlpk3EOEunM$QO_wD%0^ga>7AQQ4z1Yp?leJ-wb=8s$yh)> zutS?xYQMR2xR55!3j+_l3?rF-rZ+^-c1FE0x+zrNQQw5FX zmFE`JHtW8hIS+YQ+1#5UG?V87R($RmchtN}X3TPyOf*AU{XRZ$_t)~Rp*$$D8r{4P zewA=Y;QeDBUq<&Bf_NO}eUUCar*o`MUG*l9&L0x6Ybl_VR$NlDKtww?FE=&mrn?gc zP-R^iMHsqgqy?1m5wSkkqa(BvSU%T=VS(dkREZM6=b4=iAd1fLU0x-Z$`-xZwqNFk z{MeIZjq6#CH8aimTAONdGXQjl%iV~&SBtoF{S-Wgk(!^B&&%fnBB!EY004aKDkV-7Do;!gOhFL zw1=rl50~a9Etk!`A}!#I7Q}isg!{>OgAN4a56^1*Q|W`bMmb<@JQ0agZ;5#meTqJO zJLjqu<7?x93nyf8&X2f59mT&1?0dXcUv7wcQfL!BhZ`K+my_feTs@GJKtPEr9VCJi z1L{n;V*hf75Gr}5X`P}26EblzSE&VsaF6Dpaq!SP%h zb8~GYuFD5(x64ZSQy5)hCD*4=KbQUCSCpax6~W}f+FR`6fp zt{*d#s)eqVt%Oojh4+!;r8r?FU)eQtM!s551RZ~~?VjMgsrcsbhTQWdR5TiOcDcOq zwIBcarGa1VzEs7-)C+}taU;s1AdFD4miXLUTf1p{q2(YUiG5QG>|@wfm)|lyw2%nz zo_L>NXECDv_kAkG{&>I?Q1 z%Vr~PcdEKa;kLOzdv8W_kOovfP|q#;oZSN3e|Ucx{QBj2n6LbLURnv}r6a9(pm=_j ztw76OE~mzO-1W3YA;6$rz4wDqlOVm}_yIlY1L4=CJ>2zXPA!s1qY^^@jF%YRwaoD) zaYYfyLJS5o`^=I(U1#eCJEPH=KL-$4Yiy`0A|}xhdj0EzC^6rb+&8EU?9kFZ380yW z7&;qzY#9qPjMnKf)BXCrwV=(??!I~n-5J+&4mR5}JratU^$6(K%kLB;GV_&1&2IlTejDOAf+|$evnZPm%u5HV)(Z!hq_O2IsF_H&C_;t zvGmyAECdOm=M)nzz5oD5sAg6eS$xJSZQ# zzmzgtVlsX*t)M|)#JYG&H_r(fN@aTE7c#n^fnl*FssV0RpuIZ`9V%!fcxsW-O;=#X=JXuk zqYXWI>tNQwXrQd~vJRZ@WBxvc^AdTK>FbgRCH$OmZT`~1Qx8rG7Vt>ctL;ew2#H97 z4n60&nNHc_H===wa_1?FL4TB2k2=2253QOi5wug?aXGauTTh@LMaUE?+dqEWHYMGX zc|d9Wu$!83vI>n`x zchj{8mcO5(q-{Hn5L4TlK9)TJLsR#sVY1>P1b}-7-e;{iE@pDpDqCWd%9I7pKc1kl zddC{6yQA?ck5jy32S*Upw6GP{Y#%Ng(&9@+RyV0F3HEyk*F#pn^mvRrSGag;UhXqH z=EjGtTZz7FQ0QRB>W!xv|3XoIo-NQbdgb!WxP%U+ZnCEGh?ZeS6xo_=)?CxcZ2Iv7 zedRr7=>}h@UzM}Bir$YDvucmozhK#NA=|=K)wP5d79^x%(pAE%!M3@;<_&jsSm?8Z z=TM?k!UKt+P@>+6x3keb7A`Jq>(&8Jixsy9Y3}@vO79SH>TAB4A=DG4j05jqP~cTL z*2kG;5BZE5F+!I^VFRw+SXkl63Awj@KQV!z+!e>Up0UN@)ab+69IO4FyEu+${h-A{ zSG?mEEtf1#U3s!^H8?(yqdpQ;M)R_?b8RYbi(4}H_nKI6c{r}44{5$8hCFKa-1`cY zx6)XCb)q~#$yhZ}O&vDF|LJ9@fjJb(NI{}BI+lKpM&mB-_t_n}()f%b=9aA6)V+uf zS&jCXQMH^=$;tzL`TAOc}sck6dAWTnQj zH7h~gv@u>9p2f-}Cs;vE;k9f7@k{MmnPnPY%(6`O6U-e&6|2o?ylyT(;~;RMX>zhb76BNnh`fUa`R`{d{ory+ZKKkMSm0n! zcUC+rf0x=7_YPBaV!20==9FAmp2N`&aN>Z)-p*R(xLtP3>JS;|wf6Izxp{d~9S*as6N&^Pd(-yy}vsg=Eh}d&K)Nv-4Im{ z`|{yUl+<@afj90_F_CweGbQBolJTfli>%hKo6-eHeBX~>)a1_==e7hV|H0`RWv=LyQ$m|96RU*JjH>j+yloF=7GsiktXh^( z)$iheJhnhawovg~P@DicE?P3ninVl2`Lg0a7nlAFmfI@39;Tf(ToKvLm5<;Oen+~G z16&52??X%CFQx_ArHg5Ni~2sr_x96b^z)~Yf>MrK&d9cffm}OVf9wdjG(X3g`_(Fv zHGJ6H{1?sAe~y4YEC8pKKLm6{(%&8Da=H{Yk*UfAECqSrP`6kQi1>s=^+dFY5UwQc zKxF8TO7S`m56k_{lEU9~N@ESn&F2HgmgR41` zZhUs)mmgk_|K?!)*PXj_8_EAglDB*5#FOb;qj5ZEM>0isS|)8byk-4ZF#nwm^3>>Q zlCs+m{TTIYAdGy+x8AU23tpm({kDvQm1KVfo^lESJ6n#hH>-`Kr%derdBFp`*m2-qD(SND?zH7Z+d7Y&s?DEe3 zxteLv829;%&5tDSu)aMwn+^C}{^7m~mmMrR@F&C9Q~lMg{J_G@&_`^kQHYh!_UzK^ z%3$g9hM%pr@WKSPz1gu{Mp!om&0EwMrrN_*)5%TcUp?|^C3PpS!DZ`JXv-uLQ<+@Q zazAo?=3>2W4weCHw)E7%9pUxaI6!-Oi6Xw@Po$%RL;X4Uj~)YXollQfN%vKXPi`x2 z3k!EhVw!E|fTs>EM1P3k>x$oLCu$>37r|eufwgE7m(<*L{ff1 z`M^0Mw~2PL1`QtS2nxNt;y#Pt9vSiMuLb^iIE@A~XPomwZp5Cv9bB?XPA!blIBB1= z-wAUv5)n=ng#ntg_ep`*@9yTVe<_AsX0-+EIU4g%6I%oinDx7ygo|#Ec-)4NaSJIq z8st1}wZMEAgymXm2T!rYeTKj1w`7@9dv&$EB?QD2i-K|RHHaqlJfc)!08W>#9)cywAMS$NQpb=Ko^Di6uIo8S_ zuJ+f5|L4I8yhA@Olk#7`J>b=je`muWt)kob!^fwz{_73;8(a93?;BG1n@VzVb8jfp zuRg?zVGA3!uwe`TH`@a58)R{t=BLUs0Q|Y2Z+5Qe Itkd291Ip$LLI3~& literal 30664 zcmeIbcUY6z`ZoGf6ch!qP^8Tsv-dv#oO7M)_nq(KFWw8@vetUq{oK#97LPBQ8|~b_ ze>(sGJB@$+=*%^yaGq#L`3ke}l-@!oq^S4r-~KTgejxte2KC1u#0B^P z^#zBV|1#=bzFbKJKxcJ8lHq@>i-7@fW5?gt z>E|my+86+-{v4!gvFN0g5 z%=Kqe9Ep{sHo~+T27(M5RTI7Gl1{)~DJuPa{Pi=_v?Ed{JoldCmQnQ5B=r8>{}ANMg$#Oh?~g+;CF zy~e$Z@(*)1$E-$X)bOaWz|s1cS?G+v{`ZeI$2|-Q$3fgTzMnyR=YeO^HiAZ zUy2!Zf{W{=zuK`a@A$&6HltG)Xp=^+y{S{k#MlN-?hE&KXVyk}SQq^oSiWfmHoH$> zFO$QVew4#pm`L08`MoR;UP?a^h_ZOwq~4>SLK`DLq0FJ75=T{=cVP2e6HjX^F)*n^bk`s)=~^Jfe7--eyyS8?o>P`dzp zJh74fd{4rK*sv9ehT1AEVOiCs?HcQ2s$r`ol=3>?g_J3M#md)%H=h+MsB>y733=(t zjzMc%Jk#FhEu{1WU-_2_7H;PU?2~TcBe(wo*yxe8!X38(M)B_yxw4kju{*z)@1P06 zr_p|v$$*8v_uwF)n#>7Sk%FU0gjKMgI>2W)IxC!ddrLU$^FacV6INCcxi*W`Lil|mYbyDIW1zE` zDTI=PiTR8$U2;i~9lWbQO`qdj>-_a2y%&WWze@jYz|OUQuX+uH6pzhA@V-I*p_rx) z8~T`!*57Ag|AJ+G)ozVGxTr#7#R94{U@s6X1h@({T>!KRucgX?d^u--!u_-@#{lNl zu$tpf4eWUn^d|cPXD!G|4LG3`$0JfMieFH^wbIDNQd&)L?e%!z*yf!sSe4(jTq zY<*MLR(_zay=_O69OjH{^Fz>jV}P4az4*1GQn&;F1{v0px$Wx_RW-iIFQR#wcbsY` zbM5PbI)suPb);@JjqPy^*;Z*sJYimVswU=7qZE_e(9|zNS+5LV9s~+(*%IY|>(+_y zUqE*xlg>5hR`Cls^C!mG#ZPVBfTBC6Uztaf=W~iE7xz6!XBQ!08f~jJ7G}THLrQ8k zaL@8=tyqrf$lIx9lea9YzQ=x#H)_B&!!F>~zCW!|RhySL@?N&gX^c&j&ALQh#8n~o z8$~W@T^`kBppC4=wQ$smxHt_Q)K#40E zoiG&PzkM+^v2Aa3WSGqhsT*6JXD4@aIUfpZR)7P7ZxuO(nV^p{Tlv z3=2=j9!z8_vxuMaunNUY-S{waLpwY-&8uC5;a#4yMIQpS3oG@@^Fypicz*x<`%W_5 zX=yCU8VZSLm+%*u3@1iV?p1K~>#8UxBi^4+$?YhruUtSCX0)um=gaPGPGoB8EswPI zLO9f`p(F^$_@Gf^%0nTUWcSqYb&V14^4#-mGSRb?9H!8v+cT=9XYCN!(-WSRaC)Yt zmzZM1)Fq@I6e_A0;ag==7LYl0sw783g6hhCA;yRqsyKhj79?fAb61Q(D7gdNr`7cuM66slw=6 zmYdH&PxsRz^!n3W`Xd2p28&}-M+!2)!{}qKv+Y^_@pfSlCKky_3#SsJu@LrJ@!q_i z1jV`7dl9+F@Y?pq04QhJy0=Pn4c(5m$8*XrJraiUgp7wbSGUFJ_%>1#C@CMlMsVQy z%X)VvRl?;mLk8rPPF+=5$33&i>8cugYeatAaGPpreW8wAhRCDSzwVvTzBo6FUbS4u zs6=G4FNHbQug!3da~ig_t<9!uDV46iA{ccjrRR$2*Avt=1)_?ozt(He9Rf5PEW!#; zvsd>JLK0bWtkUnZ95%g+GGOnQ7czF|=uV`|?IqTYcN!l4%L{h}!u6HdnSOc22x^BC z-5)EYqC7}$YQ`4JguYHPF)6NL&E#V#STh`j#ZpU9sr?!g_{q&WDV!FFHMbATWFIn9 z!|V-0Dzz?&jLbVQwI<&7!mR8jjXaThA#dw7FPfXlIsS}k-+zw;N}`>W$TJJOftx`g&hXMviVhM zdJ~^7h~ZEuU4Z%R9@k3y&9J#uv;Lj9_Gkb~S!{2zq$mIrf_h22S&9l;MKFYoMP><0`NLplmW zautdil^fXW5=vA*2TIM5hslUv`FT69PZ;}fi);bD1A}wL{!BKX%xkLV=m8eJOUuM$ z)N3Uk+bU6LUB%=JUvk!2U39$wP29CQr=WbCz3l(6oyarUmn;~x=SO5W5Ct)hoZ1`S zxh;{Ud=uXfWW?P5yrZex(P|qI%Q2HSi8S#5G=+eJUl}XYQtC3f>A`c9{s*(g%=Ml` z$cwR1`1NnNC66V$d>>W5$R0h;S)H$sT(MUy^srv1GK=iL3kbY!797^GcDO2{Oq>-qpU% zbWH(3NcTFrqKj5%>CQ?*1#gK|n_fEMbjQ1J}7%7Y?Mo~M^15-y3+W^;@ zNj3&5)|c)!+k|UyEReIKJ=*^tvKc!3 ztZJZH_@PHw+?~4fNpg40%JrNFpRGgMTmD?WAwTVF{2pd zr0_)+Icdy24^K0WU}B*cpt0X7k4i;cZ%*_R*@bz!XtRF|h01TVp@j{2wuyGUtc#Xe zdeOz*bj4g-$EN|oDpf7zjjVEraNB>0SC4atMU@sx0TJ{eM3s$*!$2C({!L9gIf_$ zBt3e_&WFX6366xZZJ8%#Y7`OS)0sc_N^62c6Q3D0Vqz ztEiv=D}{`R=Skugg#{sV_bKHJnPUyPP7}L>d`hVaKKXtrQdP#V!cs4Sy2f@u)ShBb zAh^&b(Hxc?en037TBUu3*{n%UywF=lZ(ivRt@T59JBmm%E_Y@=%r0#o$8cYs6W26jc*Ay;f1Kz>u%ZC!rYTo=HjJqr?%a zQOU_C)7sl3Dh7#xJqTTz3~AVf2JPgI8;a}~u|HFQUa|^8TT4}wrIQ+DVfGQ))LLx) zZ$Hwz&pcpyFI3_%%;Dw{2C^cC3JDX_D(^C$>)MuH*L1Ipw}G!!P#;;QZFTdjUkkQ# z$szTQM+CTCiPMdsT}*>_+hS8h(~1cwM9oVnF6XfDHtERhI;E8^PobvNHTsClJ(i)8 zVDKXr^%cgu)>`7ejlY><>FMQ?g=pQDr+qdL?FSBc*AfR{#O{wV#rG@+Mql3gc-D60 zTAdO$6juC&l+!@hJ*4L#>3+EwGpl@A^7@Hc$#PLX*SNQx&mW&Vyx%cm2s8V-W+o$h zatS46(&6fdDHAEbe6S*ZN$rY% zbq9ltMzU#DM|*ujm>LZ$zH3=o>fNg^0?!HGJpX1iJ=ogcrboRe&|6~HNsOF4)lQP4 zZsad(>@)Jp=J*H|<{{hWPqm#Y$OM7SUftGXlJ- zICU=}8KGF{*ts7Z%lEe+Y{+vdB3nmAZ3mFIm)8Q9Cv`2)K0)g*1rC!x24JVg*%x$f z(9I+UaL<67421aw6D1n8V$3VkL#RTc#F5Ln4w;`6nTtn?({;by&ps|^jiY^TPRvCJ zY8NaRwQl>ZG;yF8bGddd?vq#|odafpgL^Ds)gFU{qcZ7@OVr}v+7LxfyjxSLWOad5 z(CQgHcidc%|BY6wk)RB1+X3JDncltEEPY@aD~F3>Rj<)At1N_f_f=|ZOn=T!$gh)e z;guE+3-r<32B5Frzv=Hn*r9HU98;iG#NL#nU2^bA+6O<$>ykSiD&dngb$>e4r#xQk zjbeF1v;I4!z&DpFf|^p97~)TIbnX# zq#UOE#wN6WR4AE1X%iMKET5c7`kYsQ;gaDaRpF{%)`prXqT*h}Y>ECl+z@w$GYgj!emo4A4bfDZ1aRJ=raSce_&?Ipb&Z%wb; zZ8l-Fn&oES1pTR(?b~n&pDr)2Z@kM<(rx`|u70?#m=~Yj7{RUdHEm&SLH1*^3{eE? zK@}8yEdE~2zU~q%`U;fM&e-;YeEf@c;NKAl*eK0Wwb&^l_WmQ3?|9MIJ*E(|^|mWL zd3r*63M>|~CwI$C=0PdML?uB2>LeSq6CgTMW$rW-2g zlOA~t8789idf;3IjwbOMWldn-i|~5_#=gQ5tFV& z&Zc|NTw1TQp83`3Gn@qg3DUGk)W;=^V$onvVZk4%2^XJ?EnW5z2E9TG9Q_g<3@2VQ zl0q)jaYS?IaXF!-*NY1rgE1!wcVkr)5K3pG$}J$JBd~bxcQUL+M$)e0vCE5tvED0% zA;)*NewbN@DI1rHTKRy8B*SgyMXwy+!m~!Z&H*ovx@w!&wPzhg@8BJSB@oR86_|?X zz0nE|MJn+k2df-X7Xf*GIs6ih`XHOsQTSL$KX-T!uqMi zwC?8%W>BH4fLQrfA$@0Z@8Oa7;lX z3iG}2uA5%S=_D4G8xCI^QG#Q)WR77Aa;>Pn@{ z!Jc#OI4K4)`G;vQB2ki{|mRgU#-3n&oxo+(MEjZqgv5`FhUaN zbsiF4vn*C>8o1<#s97dPhtpH08l^g%w|ZDH+7sXA==ne< z2SF=Fx<*g5-+wSYG!r!(`3tuPbAX>q+><}jWk{hWUb9vgP$a>&_YRtr&g7f_4 z1N-v>LkTDx``Lhl-?Ls~9@4jVs_9AWmCi8FKIzb}Rh~$ekW#~IZ^Dgokd~^bD;bno z3J5a4u+WhT^|;s7t5BQ(smP@{3AZ{fJzF_$!X%=!yEM&Fg*X@E5JggvkN=I%cQEfK zE{0Is{E%ZNyfwn#lTo3J<-)ozBA9}TW!sq9%%EGPvtws>u1(omL*c);J#@l}Tw9D{ z1PLBWe<0?ccu)`zQEKShjUZG$BuhO4RL{;UD4(jjc6&dg4dXo=N?I84`yTw%TOC~n zvVU&hQXDc2$`MdZG?WlSibI-};(@m&-d79lefG<=MHy~-!iIXUGBXp8% za>JI^Jgm&+RqV~BI6LF?iM1^?ofT5%2$BN%=#MnC0_3EI08;rn#H6<*LSl#_gHJOztK+L@cwh;;SKH z2@1XR!c3Gjs9y9dGK^_F?3c3q;zHrw5Z*|?kZnL7`5d6_iIKBY=5j7?I_mCs{$}4B z%<>ndszRCF$x7cO9eGvr3rfTH+bY79U~caX6!*m+&UQOK)EcT?7_mFW zFSqU%e?T@rWOrbL!R;P|;Dto?0zC)W&{m}TT-h^0?F#w%RI}ii79xVsUB2XWdnu>l zjQ_DSwy_QJDmvu2lCBLZV_euF7wR=I?J8`5+hdQZ&!qIG7GZ49NQ%k5D=)aNtEe`qO_l0Z5 z!~&GvuqR`E+|>(`R}7{#5B^GPNV~zphDk4C_z&3xRqAN_l)jHSe@yiRDXi(OXRgau zM^wBt>zdBOOVV-px}^u;6hiDFI#sQ2HwoGfKo#8wm6n^8bnpSDrJRZ59V(Y<{GfJ> zWB{!Zylt`5F&W@^%cG^+`>qM|Ve2>Ik}P9v z@42a?9`2M#UZ`G6^Nl_-3LoboJj(8%KZLpEdQpeEb?6@gyMnZm^kKY_D3e;yj-vOP zxJQ)C_jditpsa%eIVuxzecdY4jolxe$ePx~dqvlxT5xA@wuN2Hc2If4JlY)TzBfcn zzNZ|ct;8Ots40{{+_3}jNcWQ`1%RV1+6#6j^XS^KhxUE=>EZ&KXYLCfM>qx}&&FRM z=3h}jPVWE73Dki(5b0?kvXbBQ4Y1gcW2ZOTXDl!Vdz4Kh*dtd6)#}BjA&&j5=*3|@ zAY$#a4ADtc3}q5I4v)BMcVmf)-6OP*f!Nn^Z)9j#7^qyi^NzmX6cmQi+j!@L+zD28 zk_UE2KYornkEV77PWc-J!sX4s#bfVu*v+51eOdTIU!~!-v4y(%i;_;CSrc)FRMchb za+phEEqtN+cdT1HNSjnGFbJ!AoYL{=qs3ZwmnHZzepwv65PQX=M`!j!uZb(V?l!v! zex&u*dwxp0xo`a80spc9(zb&5vrpWTolZ!FFNWjw(U%Z~ef{kx)nZFRdxIRt6P>sK_wT2r8RP`6ZnX|NIr`y;$fYfK5J`=q-V!t#~O zQS7tAyoS)0-*tp{m|w~O@G^xz?V>|+e^-KXUHGh2r8%s4Xidd!{~E3TEo8l0?R23g(yS_sB|5X?V|Ujd!Ue(Q;<+xNk%Iwnk3a`hyB0S6)tj z52T<>#wlp4b2c#-89di6ci@WIsrs@Otz?#OL)#MO$nurEgEah!^Rm^%sJvVsEnCEB zIJ|-M2xL#PV|Wbo#i-Yo;;saRKCc=JuUcj{w<MOb}$0kS94ZSE%DiclSR# zx4|IL8{F~$zY^n8@|1u|Qe$5K;j*QfX$u`aeE2LrhragVuE1-79#W&9_5Jht1vO^~ zy~EnPQ$-_1#6a2iX+&R#ZP#ln2eHZ@3bl=^V6dyu;wL6!lvpI*X-KAQc>k8jrMS|g zmJi%fKYIaOA}aQn>mS4iw^^9@;h!BYi#NX(pFR%9ndMAgn13e5IGl_s$~)6$+Q`q< zVK=0I&)+e7efZm*_Qytf8;tX`FHW)Lm_W%eRk5G3;}>A?f>deM^?5bhVw3_9$7p-o zTSpEuh*2%kQo1`gXmqOqh5Hq1v$)bwa~@CEkl6F5SL=v*T)0zngGp{uV^IF}L`1~Gjo_&CH81sD6Iqrw%nuoe{ao(6o8HLwf z1JG-uD{X{ExJ+>HI&n|WYg_j;eG;m9t7rT^v~}qAn?phV+5Cc4!=!2#`7h522?_}y z`J!I7TVsr#-xlM1&BhM)EeFf4^LDs@oGPSnV@m~?0pAac2xq=+Z#)0%+F!(+t?h0p zCAOjy40Uolke3;p7C5Vfs|?LI$DL$E<%3y2B2!pi^MMMi6lXQRbKrvREHn1XJl=9( zCM_T#@LSGoO5?z>QSHgpW^9Ov~%Xro>kE)<`Wk7#JEJnOQZ7Qw^nz{ zt@NwDoJ6()s;6BPWdfLQbNDJLErCf%$;BDH4w|{4w~%wFuGU}5ftzRJrbC0qZm*CC zed^tw?Vr;TCXBg0%I^+8@WHMd*6lUzSg{9-N zE;N6yIA7+~k-rEvQAjH2X}Pq`xs_oOJ|*VE?j2_K?}*Q_J;qx2TW|uZv_dsmEEzSf zM~kk2XWGX4zw%>FNGs92V;e@=hsWUa8CFjIYrVLE<8zFV%yqlt1rK6M!chJD=IVIbuoU#3BG9ivP{QHw)lv$PH5^}=5A~wD zAmgVAPX%;a4YJRih!A^xmUwmAzAepS2HH6d=}2pr*4Km%_ErE1t9W z>Dp-`wChDcLQhVvnivJQa;T$$}ik*rwD@u zFP>3A`PKF3%G0Ld?{HSopd71q*zK+uh*J^v=@)mo zhrrF#9ud5e{dGpa261{KUPv>VW&Ws06}xX~g}S#uy(p-Ye>o7ZMs}-xFTo?JrxT0< zBZZnK5=H3Xjxa=!*V;XYUw;rFH|~K7x$}|l7k$u;^eBN@Xlq{ZJePLu$cb_GAkWR0 zDF7KhagCrQlq55@l%%4P(XG#FNuH3{@;Kf-2-77&dK^Sb618^66_&K?l+rsu9>ej%<5oU{t_HjJfcsNJvDMW*oL~LV`j?9^{Kmk{l@uCjR!$E>Gz_KQ z5CO~|`H!L=f&oak5d9@tUXbjG!Yni26b4rYWYfVg**|+r>PLshODV7j9ek|TXao$G zdU@W~z~G97FEinN1@CITfQs?bX*VTvp9ThvnEudk>D2Fwx9mWRZF z^K0iyCppS+5T%_Q+m*-Y)*&heSqO~zk@3CTJ2)i^4X#5s6=jrR>3+4BhP|hE4&xg< zH`*4Il8rTWc#qJ4xCS5{1y=Ip!D?y(LZz-URqf8L6zmIF@g2@Rq2nEqM)CXmNe%wB zWPSMj<+!Av%bm>m;bf@qP+(Idybn>Y?R-dZzdUSwb=~@kq(<+;z#rBqD)R;?DM;in zX3`1C_GHr((eaLU^H)WUM+M7LJr-)#HBA4qM@IqurRVC{!m4r0xm{62zu#|pz!jP0k zjAgCGAZq7OE8RM$Ho8>i8l{gSz#`nEy^xj=pHj&{uyA_0mZHwxJ`(c{jWu=T0LjtP z;ToiXrfWG!$2t|q9wTd32#6L{KUDF%Kop1Wsbp!=Dr5B)_M;-0E1xR9RZ=~AQDr+8 zCz=z?g_z4Yy#A4O`Bv=v=oA^l%kb|eL+XZ~>wCS3Nm)B2Oy0Yyv=Z`UBE(E|*DGuv zsr}B=SoNn_p{ zQHMd-h3eT|epG8bw5_)|J5?^MTl{FQR0tl-<+I#KcLU5#1Q6Gvavl4u^KxY-xPO9Q z39wbqrzJ4I5p2MN`yK=4y{$w5Lz{{@JBi$oF?r~A&5;768Kh>SU~l;on+YxdG|%!PAyzLY{+nst~;#23UJR`t>JS z{|aBIbjAAZ_1{h$XS08)A|8=v9}1hYeOGAv=u3tIZ##<2a)Gv%(H?ApAeB$k zDY-IV5_Gm60Kz68r1V;ikT2WyTt9cj#M&X$DQ{0zos$@Ri*E%%Ge~brHbZ+?o{CZ< zuCS738005B{%i_Zq#Y?%q(RdF-9=OoWaEiC_os3D&0O zAnQ|pVyHc!^RuXM7$Jjdo3%#(&`nR;66udQH4Duss-1aQ+vI z?{3gsgxXF_xho}eT2D-f$rM&5S*>0g_UKuj$fHTqA&dEe--W3u>xR%@kbSUtL4BKv zm6g2M9!NIUendC}<#mxGYdOy%TG+bKA3f}{k95bwd!Jz^tU=_`2uw1)F#{YJZc%Vn0;q6Jaw35_<&m%dZ z$MJz3B>A&Zp-SQ3<5?vKuEPdg!iq~5S_xF#_>ExCZ&a{*(5z}aEGQ2fLTpgWQghz| z%FRtFIZ+up@3Rm7xyx1&pI*DlrR`qaVp>l0t6`dE~169)_tBjkFI z^$w^^F;NUMQCztkbN>5Ul1uQnMM`f+@#~FDeG)$9E-cylUbV17oBxm?FBm-NE4)IscgR@m?mVaA^P#-5xQzRIe29}3&YHFA2o z=nmNQW;M2l@uF2s9;SDy?~OPu4MTrd_M*UztiUk23o@Ha4%xBjPO(d2GGSmH$E0J# zt<9&`4pyMEfPybHK&0+?uMtS6vWdr)+fO*Ffk}dcCQXJ0`_5SMTE!3VJ%5|^V(R`P|7}-=yY7WU($Biapp4LLTZ|+5ipp)tkN%_c z@QL~It~3|&he2!9@D!)}5zAm&1>SDgIiDat7H_AU5!$O#bod9}r&xh_)czEiC>mwR z1$H?Aakno5gD>>Cn;xwlkM;$i;ls&G_L{_; zp?D5$b&uzWl)dh?vZMxv7eu8oTu=w>EA8Nmz|O1Lg|b#wdaOGi>B?>N%HWHTpFvLg zbI(GAEXFB>bfeT`7)+?GUjz+##V#J9ITeJY-5??C@O8yBy)Hrc4eF`s+Ek^G8PB$m zv`bb9jx#?nZJVIGRMD>3Wi_m6@0+GM?3`(yQkk6qmdB>w`_+)T1AE_kpf3BG;Q9oJ z_Mriv&Tljlx{*fnB+f9Ld* z+|WWt^9IsU^3RU(f3c<)RRE^8JCLBv-E?4L7ZAJ<19r#VM>HRiNjtI+$PG1#%=c2u z*ASe3B-6;~9k7STS2PFP)8<5Ag=o)pIvNUF9b1jk0bS*emsu5w`s>G}EEPS8pL+Ys z7;SM`(+T<&3xwu-azNZ{W1YnDpN7;x-} z66)&|;Z3vPe4wN5oY+S|RhnIAj{{fb z7+SBrQm&M<7j=H(E)_6I;}E@)c`P@*5S0+gg~6C+0QRqt#)+=Sw# zkI@ZuNSy&D7GplYoZM&>#)k~)Z?NYLB0j+rXGU+o$VoAcmZ^spLk_y1YMre(p<75h z=hAA@={;IuvpzsIcs@+rY61cUN@4^%zbp|~-lIQ$u=sb3H(D5@SQf8kSB!gaJYOZ6 zw_D2#`QqBcz!hqWmfqpoF+DbA7H#F;*04BbqW7-&L_*A>3&=uW*l1wD(<{LhUAotQ z=iL04J7Zf?uF_6;@72K=cW60{oYIDY5KIc#dhWi_Qp0z!9D#LhO&t>Fd)=~3WqbL% z(B5L8X)UHzhCUl9gaZ3^O>GKgRy%>EQZ-z}=F;urUiCo2^?mF*gX5Hp+K0t4^Ap9Q z5tlx{L3O-EuYW=Owq{Ht?&s6lsHw@gBwT)GhhpK z0|fqZK~Ain-b#2|surR6&AU=Qto+)psQ%;;P5qs;N9X9|D<8#rc3o@lsv5ndn8Z5Y z%yW2ZnE!1fy7MVW#}9amiCDrUOhG%p5>~u=$3Jv|1mX*b2w!>&8O+ra!TT;Jy;Rp+ zC!55Ir)*GSp|hDG#4-2yek=1jk^|=|&PpHRu03W=ag|J7%A3+47*fyQtXmCfOd3#f zf7{wS1J=CW+m`oyClGtki!L!(hX}@?Om9AhOUwU}g7 zV*i^@{nChEqT<~~e#qt0(zPe;(viaK^NXTZQqd8nsUSn+CitrQdcSW@R%mSj=oE{#94&{HpJT&<14`gS1wiLI{)L3Yud}D~I8fH=BiZBfk-gqbW$`gpzL{DG0P@J>Y9Gv|iC8k+! ztZ@E|$nQ`|5nZ`Jn0!S zFFkme9q}Zb%{yIL=vDX$RqwedgabP_j5!xy%Yu;hSQ1` zWi6xH&&c-gDkdtR|Ab(>A+*Ta1uV z2Po2nW3Kq+tMl+Vg3Dvj|03M~`0^9EbH}-ExDELUub=1F{xo>FNoo>m3TSYOW$Ky> zf1324b7*?G(d)dfyiU<`J`5zrLZ=` zyM$+tPt((0XUCPA+>UK;Ri2gFtTU>{%-PZRx^>o$0Dh09aP3n)e z|1qTsOECUhnQOtK4M=BX9ff$_D)TL9%YL2q=eid3>E@y6q}7uBS*Yo#BVYAgwnAHO zyLv32PZi}03(Qy?Q6m=XFp7zjGJ?R1CsMQJ|2CD04Jl+bE*?6`jp~SmrErI2Y#%K> z>U>hN0I#mrll@=oe{4DG zfZ_|AGGP`~e)k)}4zFEe@5hh!fA;g{*H&It;C|oEX;}tTOUqmxg2oX=J1&1mCF;1+%hP<9G||VrO6-nx?~xh(8clc`_kC$fAojF z|9rdiE3mL};HylokdzPsnL!)dwd#Q|b3=Uu8iYjM!Si z@;6h45Fi2GMx1Vjz&TWhA>E&?lYeTNK_eK2!x)q+U!60wwOPNCc-$k}KxGKaHRP+*C(P>z3fV>^!q` zMS4X-^2>Ou4^ku;953#sVbztqP5cT*n_`OTP>Cr{|#8SvX+t-g&_7yuJ2U;RrBWxkNG?f4C0WNO* zhMjNu0bG6vU_%u*H!c3JxoL587wKP)@{f!E@m-{w0@)PEpYC&WpY^{*m;X00WL6Av z>Sr&&KNV~Pcx-Nh{}U+ug`74|CHUXvRD#WT@=tT$4Df$LdFo0@^^^x0wHOe0~$xjsJE;e=E0OlYh;t zfA0|l-+2=l{;QJzuZyVCNS>PxMK}Kb(I3;7jMV&Rr{W*?I4e#7k|_%RaYj2>K;(P5 z^S>PEzd41!ukEH3e(K()BK_T1HqH5GLbd6U{^4~uo$60B*$i!eLexLiV>3GcG?Pt$ zu?a9Xk?NnYYSR`rZDG?EHf`ZQ8H~-ub2IVW Date: Fri, 13 Jan 2023 21:30:53 +0100 Subject: [PATCH 46/55] Fixed drag and drop tests --- tests/end-to-end/dragdrop/dragdrop.test.ts | 7 +++++++ tests/utils/mouse.ts | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/end-to-end/dragdrop/dragdrop.test.ts b/tests/end-to-end/dragdrop/dragdrop.test.ts index ac671cb08f..9a1c5f9b1a 100644 --- a/tests/end-to-end/dragdrop/dragdrop.test.ts +++ b/tests/end-to-end/dragdrop/dragdrop.test.ts @@ -9,6 +9,7 @@ import { import { compareDocToSnapshot, focusOnEditor } from "../../utils/editor"; import { insertHeading, insertParagraph } from "../../utils/copypaste"; import { dragAndDropBlock } from "../../utils/mouse"; +import { showMouseCursor } from "../../utils/debug"; test.beforeEach(async ({ page }) => { await page.goto(BASE_URL, { waitUntil: "networkidle" }); @@ -24,6 +25,7 @@ test.describe("Check Block Dragging Functionality", () => { "Playwright doesn't correctly simulate drag events in Firefox." ); await focusOnEditor(page); + await showMouseCursor(page); await insertHeading(page, 1); await insertHeading(page, 2); @@ -31,6 +33,7 @@ test.describe("Check Block Dragging Functionality", () => { const dragTarget = await page.locator(H_ONE_BLOCK_SELECTOR); const dropTarget = await page.locator(H_TWO_BLOCK_SELECTOR); + await page.pause(); await dragAndDropBlock(page, dragTarget, dropTarget, false); await page.pause(); @@ -47,6 +50,7 @@ test.describe("Check Block Dragging Functionality", () => { "Playwright doesn't correctly simulate drag events in Firefox." ); await focusOnEditor(page); + await showMouseCursor(page); await insertHeading(page, 1); await insertParagraph(page); @@ -63,16 +67,19 @@ test.describe("Check Block Dragging Functionality", () => { // Dragging first heading into next nested element. let dragTarget = await page.locator(H_ONE_BLOCK_SELECTOR); let dropTarget = await page.locator(H_TWO_BLOCK_SELECTOR); + await page.pause(); await dragAndDropBlock(page, dragTarget, dropTarget, true); // Dragging second heading into next nested element. dragTarget = await page.locator(H_TWO_BLOCK_SELECTOR); dropTarget = await page.locator(H_THREE_BLOCK_SELECTOR); + await page.pause(); await dragAndDropBlock(page, dragTarget, dropTarget, true); // Dragging third heading into outside nesting. dragTarget = await page.locator(H_THREE_BLOCK_SELECTOR); dropTarget = await page.locator(BLOCK_SELECTOR).last(); + await page.pause(); await dragAndDropBlock(page, dragTarget, dropTarget, true); await page.pause(); diff --git a/tests/utils/mouse.ts b/tests/utils/mouse.ts index ba324446bc..308b4f39b7 100644 --- a/tests/utils/mouse.ts +++ b/tests/utils/mouse.ts @@ -15,6 +15,14 @@ async function getElementRightCoords(page: Page, element: Locator) { return { x: boundingBox.x + boundingBox.width - 1, y: centerY }; } +async function getElementCenterCoords(page: Page, element: Locator) { + const boundingBox = await element.boundingBox(); + const centerX = boundingBox.x + boundingBox.width / 2; + const centerY = boundingBox.y + boundingBox.height / 2; + + return { x: centerX, y: centerY }; +} + export async function moveMouseOverElement(page: Page, element: Locator) { const boundingBox = await element.boundingBox(); const coords = { x: boundingBox.x, y: boundingBox.y }; @@ -31,7 +39,10 @@ export async function dragAndDropBlock( await page.waitForSelector(DRAG_HANDLE_SELECTOR); const dragHandle = await page.locator(DRAG_HANDLE_SELECTOR); - await moveMouseOverElement(page, dragHandle); + const dragHandleCenterCoords = await getElementCenterCoords(page, dragHandle); + await page.mouse.move(dragHandleCenterCoords.x, dragHandleCenterCoords.y, { + steps: 5, + }); await page.mouse.down(); const dropTargetCoords = dropAbove From e931b3d8e14e5e8781012642dfe053bd26b8067f Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 13 Jan 2023 22:05:49 +0100 Subject: [PATCH 47/55] Improved how UI element factories are passed to extensions --- packages/core/src/BlockNoteEditor.ts | 62 ++----------------- packages/core/src/BlockNoteExtensions.ts | 55 ++++++++++++++-- .../DraggableBlocksExtension.ts | 5 +- .../FormattingToolbarExtension.ts | 5 +- .../HyperlinkToolbar/HyperlinkMark.ts | 3 +- .../SlashMenu/SlashMenuExtension.ts | 3 +- 6 files changed, 64 insertions(+), 69 deletions(-) diff --git a/packages/core/src/BlockNoteEditor.ts b/packages/core/src/BlockNoteEditor.ts index c6cf38dcc6..5ac5b7ef73 100644 --- a/packages/core/src/BlockNoteEditor.ts +++ b/packages/core/src/BlockNoteEditor.ts @@ -1,27 +1,15 @@ import { Editor, EditorOptions } from "@tiptap/core"; // import "./blocknote.css"; -import { getBlockNoteExtensions } from "./BlockNoteExtensions"; +import { getBlockNoteExtensions, UiFactories } from "./BlockNoteExtensions"; import styles from "./editor.module.css"; -import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes"; -import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; -import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; -import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; -import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; export type BlockNoteEditorOptions = EditorOptions & { enableBlockNoteExtensions: boolean; disableHistoryExtension: boolean; - uiFactories: Partial<{ - formattingToolbarFactory: FormattingToolbarFactory; - hyperlinkToolbarFactory: HyperlinkToolbarFactory; - suggestionsMenuFactory: SuggestionsMenuFactory; - blockSideMenuFactory: BlockSideMenuFactory; - }>; + uiFactories: UiFactories; }; -const blockNoteExtensions = getBlockNoteExtensions(); - const blockNoteOptions = { enableInputRules: true, enablePasteRules: true, @@ -32,52 +20,14 @@ export class BlockNoteEditor { public readonly tiptapEditor: Editor & { contentComponent: any }; constructor(options: Partial = {}) { + const blockNoteExtensions = getBlockNoteExtensions( + options.uiFactories || {} + ); + let extensions = options.disableHistoryExtension ? blockNoteExtensions.filter((e) => e.name !== "history") : blockNoteExtensions; - // TODO: review - extensions = extensions.map((extension) => { - if ( - extension.name === "FormattingToolbarExtension" && - options.uiFactories?.formattingToolbarFactory - ) { - return extension.configure({ - formattingToolbarFactory: - options.uiFactories.formattingToolbarFactory, - }); - } - - if ( - extension.name === "link" && - options.uiFactories?.hyperlinkToolbarFactory - ) { - return extension.configure({ - hyperlinkToolbarFactory: options.uiFactories.hyperlinkToolbarFactory, - }); - } - - if ( - extension.name === "slash-command" && - options.uiFactories?.suggestionsMenuFactory - ) { - return extension.configure({ - suggestionsMenuFactory: options.uiFactories.suggestionsMenuFactory, - }); - } - - if ( - extension.name === "DraggableBlocksExtension" && - options.uiFactories - ) { - return extension.configure({ - blockSideMenuFactory: options.uiFactories.blockSideMenuFactory, - }); - } - - return extension; - }); - const tiptapOptions = { ...blockNoteOptions, ...options, diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index e7a3acd7b2..e3ec80ab58 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -21,6 +21,12 @@ import { Placeholder } from "./extensions/Placeholder/PlaceholderExtension"; import SlashMenuExtension from "./extensions/SlashMenu"; import { TrailingNode } from "./extensions/TrailingNode/TrailingNodeExtension"; import UniqueID from "./extensions/UniqueID/UniqueID"; +import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes"; +import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; +import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; +import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; +import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; +import { Link } from "@tiptap/extension-link"; export const Document = Node.create({ name: "doc", @@ -28,10 +34,17 @@ export const Document = Node.create({ content: "block+", }); +export type UiFactories = Partial<{ + formattingToolbarFactory: FormattingToolbarFactory; + hyperlinkToolbarFactory: HyperlinkToolbarFactory; + suggestionsMenuFactory: SuggestionsMenuFactory; + blockSideMenuFactory: BlockSideMenuFactory; +}>; + /** * Get all the Tiptap extensions BlockNote is configured with by default */ -export const getBlockNoteExtensions = () => { +export const getBlockNoteExtensions = (uiFactories: UiFactories) => { const ret: Extensions = [ extensions.ClipboardTextSerializer, extensions.Commands, @@ -65,19 +78,51 @@ export const getBlockNoteExtensions = () => { Italic, Strike, Underline, - HyperlinkMark, FixedParagraph, // custom blocks: ...blocks, - DraggableBlocksExtension, + DropCursor.configure({ width: 5, color: "#ddeeff" }), - FormattingToolbarExtension, History, // This needs to be at the bottom of this list, because Key events (such as enter, when selecting a /command), // should be handled before Enter handlers in other components like splitListItem - SlashMenuExtension, TrailingNode, ]; + + if (uiFactories.blockSideMenuFactory) { + ret.push( + DraggableBlocksExtension.configure({ + blockSideMenuFactory: uiFactories.blockSideMenuFactory, + }) + ); + } + + if (uiFactories.formattingToolbarFactory) { + ret.push( + FormattingToolbarExtension.configure({ + formattingToolbarFactory: uiFactories.formattingToolbarFactory, + }) + ); + } + + if (uiFactories.hyperlinkToolbarFactory) { + ret.push( + HyperlinkMark.configure({ + hyperlinkToolbarFactory: uiFactories.hyperlinkToolbarFactory, + }) + ); + } else { + ret.push(Link); + } + + if (uiFactories.suggestionsMenuFactory) { + ret.push( + SlashMenuExtension.configure({ + suggestionsMenuFactory: uiFactories.suggestionsMenuFactory, + }) + ); + } + return ret; }; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts index baede67a6b..8f4a7af603 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksExtension.ts @@ -19,8 +19,9 @@ export const DraggableBlocksExtension = priority: 1000, // Need to be high, in order to hide menu when typing slash addProseMirrorPlugins() { if (!this.options.blockSideMenuFactory) { - console.warn("factories not defined for DraggableBlocksExtension"); - return []; + throw new Error( + "UI Element factory not defined for DraggableBlocksExtension" + ); } return [ createDraggableBlocksPlugin({ diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts index c3cdb35845..fb63801145 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarExtension.ts @@ -13,8 +13,9 @@ export const FormattingToolbarExtension = Extension.create<{ addProseMirrorPlugins() { if (!this.options.formattingToolbarFactory) { - console.warn("factories not defined for FormattingToolbarExtension"); - return []; + throw new Error( + "UI Element factory not defined for FormattingToolbarExtension" + ); } return [ diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts index 26bb404d21..e61cc46dfd 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkMark.ts @@ -13,8 +13,7 @@ const Hyperlink = Link.extend({ priority: 500, addProseMirrorPlugins() { if (!this.options.hyperlinkToolbarFactory) { - console.warn("factories not defined for Hyperlink"); - return [...(this.parent?.() || [])]; + throw new Error("UI Element factory not defined for HyperlinkMark"); } return [ diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts index 3c66e507f3..0ed30f244c 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts @@ -24,8 +24,7 @@ export const SlashMenuExtension = Extension.create({ addProseMirrorPlugins() { if (!this.options.suggestionsMenuFactory) { - console.warn("factories not defined for SlashMenuExtension"); - return []; + throw new Error("UI Element factory not defined for SlashMenuExtension"); } return [ From 4ce7ed9bb816d080832d14ebaf2ea3b4656a4888 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 13 Jan 2023 22:29:54 +0100 Subject: [PATCH 48/55] Updated suggestions/slash menu naming --- examples/vanilla/src/main.tsx | 4 +-- ...ionsMenuFactory.ts => slashMenuFactory.ts} | 10 ++++---- packages/core/src/BlockNoteExtensions.ts | 8 +++--- .../SlashMenu/SlashMenuExtension.ts | 8 +++--- packages/core/src/index.ts | 1 + .../plugins/suggestion/SuggestionPlugin.ts | 2 +- packages/react/src/BlockNoteTheme.ts | 2 +- .../SlashMenuFactory.tsx} | 25 ++++++++----------- .../components/SlashMenu.tsx} | 24 ++++++++---------- .../components/SlashMenuItem.tsx} | 5 ++-- packages/react/src/hooks/useBlockNote.ts | 4 +-- packages/react/src/index.ts | 2 +- 12 files changed, 44 insertions(+), 51 deletions(-) rename examples/vanilla/src/ui/{suggestionsMenuFactory.ts => slashMenuFactory.ts} (87%) rename packages/react/src/{SuggestionsMenu/SuggestionsMenuFactory.tsx => SlashMenu/SlashMenuFactory.tsx} (74%) rename packages/react/src/{SuggestionsMenu/components/SuggestionList.tsx => SlashMenu/components/SlashMenu.tsx} (83%) rename packages/react/src/{SuggestionsMenu/components/SuggestionListItem.tsx => SlashMenu/components/SlashMenuItem.tsx} (94%) diff --git a/examples/vanilla/src/main.tsx b/examples/vanilla/src/main.tsx index 0719daac6a..2929893849 100644 --- a/examples/vanilla/src/main.tsx +++ b/examples/vanilla/src/main.tsx @@ -3,7 +3,7 @@ import "./index.css"; import { blockSideMenuFactory } from "./ui/blockSideMenuFactory"; import { formattingToolbarFactory } from "./ui/formattingToolbarFactory"; import { hyperlinkToolbarFactory } from "./ui/hyperlinkToolbarFactory"; -import { suggestionsMenuFactory } from "./ui/suggestionsMenuFactory"; +import { slashMenuFactory } from "./ui/slashMenuFactory"; const editor = new BlockNoteEditor({ element: document.getElementById("root")!, @@ -13,7 +13,7 @@ const editor = new BlockNoteEditor({ // Create an example menu for hyperlinks hyperlinkToolbarFactory, // Create an example menu for the /-menu - suggestionsMenuFactory, + slashMenuFactory: slashMenuFactory, // Create an example menu for when a block is hovered blockSideMenuFactory, }, diff --git a/examples/vanilla/src/ui/suggestionsMenuFactory.ts b/examples/vanilla/src/ui/slashMenuFactory.ts similarity index 87% rename from examples/vanilla/src/ui/suggestionsMenuFactory.ts rename to examples/vanilla/src/ui/slashMenuFactory.ts index 9e0effcdc5..4408554149 100644 --- a/examples/vanilla/src/ui/suggestionsMenuFactory.ts +++ b/examples/vanilla/src/ui/slashMenuFactory.ts @@ -1,12 +1,12 @@ -import { SuggestionItem, SuggestionsMenuFactory } from "@blocknote/core"; +import { SlashMenuItem, SuggestionsMenuFactory } from "@blocknote/core"; import { createButton } from "./util"; /** * This menu is drawn when the cursor is moved to a hyperlink (using the keyboard), * or when the mouse is hovering over a hyperlink */ -export const suggestionsMenuFactory: SuggestionsMenuFactory = ( - props +export const slashMenuFactory: SuggestionsMenuFactory = ( + _props ) => { const container = document.createElement("div"); container.style.background = "gray"; @@ -17,8 +17,8 @@ export const suggestionsMenuFactory: SuggestionsMenuFactory = ( document.body.appendChild(container); function updateItems( - items: SuggestionItem[], - onClick: (item: SuggestionItem) => void, + items: SlashMenuItem[], + onClick: (item: SlashMenuItem) => void, selected: number ) { container.innerHTML = ""; diff --git a/packages/core/src/BlockNoteExtensions.ts b/packages/core/src/BlockNoteExtensions.ts index e3ec80ab58..ea1ad8e75e 100644 --- a/packages/core/src/BlockNoteExtensions.ts +++ b/packages/core/src/BlockNoteExtensions.ts @@ -24,9 +24,9 @@ import UniqueID from "./extensions/UniqueID/UniqueID"; import { FormattingToolbarFactory } from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes"; import { HyperlinkToolbarFactory } from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; import { SuggestionsMenuFactory } from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; -import { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; import { BlockSideMenuFactory } from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; import { Link } from "@tiptap/extension-link"; +import { SlashMenuItem } from "./extensions/SlashMenu/SlashMenuItem"; export const Document = Node.create({ name: "doc", @@ -37,7 +37,7 @@ export const Document = Node.create({ export type UiFactories = Partial<{ formattingToolbarFactory: FormattingToolbarFactory; hyperlinkToolbarFactory: HyperlinkToolbarFactory; - suggestionsMenuFactory: SuggestionsMenuFactory; + slashMenuFactory: SuggestionsMenuFactory; blockSideMenuFactory: BlockSideMenuFactory; }>; @@ -116,10 +116,10 @@ export const getBlockNoteExtensions = (uiFactories: UiFactories) => { ret.push(Link); } - if (uiFactories.suggestionsMenuFactory) { + if (uiFactories.slashMenuFactory) { ret.push( SlashMenuExtension.configure({ - suggestionsMenuFactory: uiFactories.suggestionsMenuFactory, + slashMenuFactory: uiFactories.slashMenuFactory, }) ); } diff --git a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts index 0ed30f244c..35757879f0 100644 --- a/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts +++ b/packages/core/src/extensions/SlashMenu/SlashMenuExtension.ts @@ -7,7 +7,7 @@ import { SlashMenuItem } from "./SlashMenuItem"; export type SlashMenuOptions = { commands: { [key: string]: SlashMenuItem }; - suggestionsMenuFactory: SuggestionsMenuFactory | undefined; + slashMenuFactory: SuggestionsMenuFactory | undefined; }; export const SlashMenuPluginKey = new PluginKey("suggestions-slash-commands"); @@ -18,12 +18,12 @@ export const SlashMenuExtension = Extension.create({ addOptions() { return { commands: defaultCommands, - suggestionsMenuFactory: undefined, // TODO: fix undefined + slashMenuFactory: undefined, // TODO: fix undefined }; }, addProseMirrorPlugins() { - if (!this.options.suggestionsMenuFactory) { + if (!this.options.slashMenuFactory) { throw new Error("UI Element factory not defined for SlashMenuExtension"); } @@ -32,7 +32,7 @@ export const SlashMenuExtension = Extension.create({ pluginKey: SlashMenuPluginKey, editor: this.editor, char: "/", - suggestionsMenuFactory: this.options.suggestionsMenuFactory!, + suggestionsMenuFactory: this.options.slashMenuFactory!, items: (query) => { const commands = []; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d7c8f2396c..b915496fb5 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,5 +3,6 @@ export * from "./BlockNoteExtensions"; export * from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes"; export * from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; export * from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; +export * from "./extensions/SlashMenu/SlashMenuItem"; export type { SuggestionItem } from "./shared/plugins/suggestion/SuggestionItem"; export * from "./shared/plugins/suggestion/SuggestionsMenuFactoryTypes"; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 99b5847625..8eb00914d0 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -242,7 +242,7 @@ export function createSuggestionPlugin({ deactivate(view); selectItemCallback(props); }, - suggestionsMenuFactory, + suggestionsMenuFactory: suggestionsMenuFactory, }), state: { diff --git a/packages/react/src/BlockNoteTheme.ts b/packages/react/src/BlockNoteTheme.ts index e02250161a..bb17186a7a 100644 --- a/packages/react/src/BlockNoteTheme.ts +++ b/packages/react/src/BlockNoteTheme.ts @@ -128,7 +128,7 @@ export const BlockNoteTheme: MantineThemeOverride = { }, }), }, - SuggestionList: { + SlashMenu: { styles: (theme) => ({ root: { // ...theme.other.defaultMenuStyles(theme), diff --git a/packages/react/src/SuggestionsMenu/SuggestionsMenuFactory.tsx b/packages/react/src/SlashMenu/SlashMenuFactory.tsx similarity index 74% rename from packages/react/src/SuggestionsMenu/SuggestionsMenuFactory.tsx rename to packages/react/src/SlashMenu/SlashMenuFactory.tsx index 81398c8b1f..263090487a 100644 --- a/packages/react/src/SuggestionsMenu/SuggestionsMenuFactory.tsx +++ b/packages/react/src/SlashMenu/SlashMenuFactory.tsx @@ -1,30 +1,25 @@ import { createRoot, Root } from "react-dom/client"; import { - SuggestionItem, + SlashMenuItem, SuggestionsMenu, SuggestionsMenuFactory, SuggestionsMenuParams, } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; import Tippy from "@tippyjs/react"; -import { - SuggestionList, - SuggestionListProps, -} from "./components/SuggestionList"; +import { SlashMenu, SlashMenuProps } from "./components/SlashMenu"; import { BlockNoteTheme } from "../BlockNoteTheme"; // import rootStyles from "../../../core/src/root.module.css"; -export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< - SuggestionItem -> = ( - params: SuggestionsMenuParams -): SuggestionsMenu => { - const suggestionsMenuProps: SuggestionListProps = { +export const ReactSlashMenuFactory: SuggestionsMenuFactory = ( + params: SuggestionsMenuParams +): SuggestionsMenu => { + const suggestionsMenuProps: SlashMenuProps = { ...params, }; function updateSuggestionsMenuProps( - params: SuggestionsMenuParams + params: SuggestionsMenuParams ) { suggestionsMenuProps.items = params.items; suggestionsMenuProps.selectedItemIndex = params.selectedItemIndex; @@ -42,7 +37,7 @@ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< } + content={} duration={0} getReferenceClientRect={() => params.queryStartBoundingBox} hideOnClick={false} @@ -57,7 +52,7 @@ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< return { element: menuRootElement as HTMLElement, - show: (params: SuggestionsMenuParams) => { + show: (params: SuggestionsMenuParams) => { updateSuggestionsMenuProps(params); document.body.appendChild(menuRootElement); @@ -70,7 +65,7 @@ export const ReactSuggestionsMenuFactory: SuggestionsMenuFactory< menuRootElement.remove(); }, - update: (params: SuggestionsMenuParams) => { + update: (params: SuggestionsMenuParams) => { updateSuggestionsMenuProps(params); menuRoot!.render(getMenuComponent()); diff --git a/packages/react/src/SuggestionsMenu/components/SuggestionList.tsx b/packages/react/src/SlashMenu/components/SlashMenu.tsx similarity index 83% rename from packages/react/src/SuggestionsMenu/components/SuggestionList.tsx rename to packages/react/src/SlashMenu/components/SlashMenu.tsx index e4fd8e04dd..a12b68f6a1 100644 --- a/packages/react/src/SuggestionsMenu/components/SuggestionList.tsx +++ b/packages/react/src/SlashMenu/components/SlashMenu.tsx @@ -1,22 +1,20 @@ -import { SuggestionItem } from "@blocknote/core"; +import { SlashMenuItem } from "@blocknote/core"; import { createStyles, Menu } from "@mantine/core"; -import { SuggestionListItem } from "./SuggestionListItem"; +import { SlashMenuItem as ReactSlashMenuItem } from "./SlashMenuItem"; -export type SuggestionListProps = { - items: T[]; +export type SlashMenuProps = { + items: SlashMenuItem[]; selectedItemIndex: number; - itemCallback: (item: T) => void; + itemCallback: (item: SlashMenuItem) => void; }; -export function SuggestionList( - props: SuggestionListProps -) { +export function SlashMenu(props: SlashMenuProps) { const { classes } = createStyles({ root: {} })(undefined, { - name: "SuggestionList", + name: "SlashMenu", }); - const headingGroup: T[] = []; - const basicBlockGroup: T[] = []; + const headingGroup: SlashMenuItem[] = []; + const basicBlockGroup: SlashMenuItem[] = []; for (const item of props.items) { if (item.name === "Heading") { @@ -54,7 +52,7 @@ export function SuggestionList( for (const item of headingGroup) { renderedItems.push( - ( for (const item of basicBlockGroup) { renderedItems.push( - void; }; -export function SuggestionListItem(props: SuggestionGroupItemProps) { +export function SlashMenuItem(props: SlashMenuItemProps) { const itemRef = useRef(null); const { classes } = createStyles({ root: {} })(undefined, { name: "SuggestionListItem", diff --git a/packages/react/src/hooks/useBlockNote.ts b/packages/react/src/hooks/useBlockNote.ts index 243de21240..e907de768d 100644 --- a/packages/react/src/hooks/useBlockNote.ts +++ b/packages/react/src/hooks/useBlockNote.ts @@ -2,7 +2,7 @@ import { BlockNoteEditor, BlockNoteEditorOptions } from "@blocknote/core"; import { DependencyList, useEffect, useState } from "react"; import { ReactFormattingToolbarFactory } from "../FormattingToolbar/FormattingToolbarFactory"; import { ReactHyperlinkToolbarFactory } from "../HyperlinkToolbar/HyperlinkToolbarFactory"; -import { ReactSuggestionsMenuFactory } from "../SuggestionsMenu/SuggestionsMenuFactory"; +import { ReactSlashMenuFactory } from "../SlashMenu/SlashMenuFactory"; import { ReactBlockSideMenuFactory } from "../BlockSideMenu/BlockSideMenuFactory"; //based on https://github.com/ueberdosis/tiptap/blob/main/packages/react/src/useEditor.ts @@ -32,7 +32,7 @@ export const useBlockNote = ( uiFactories: { formattingToolbarFactory: ReactFormattingToolbarFactory, hyperlinkToolbarFactory: ReactHyperlinkToolbarFactory, - suggestionsMenuFactory: ReactSuggestionsMenuFactory, + slashMenuFactory: ReactSlashMenuFactory, blockSideMenuFactory: ReactBlockSideMenuFactory, }, }; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 040533dead..01fcd48d2b 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -5,4 +5,4 @@ export * from "./FormattingToolbar/FormattingToolbarFactory"; export * from "./hooks/useBlockNote"; export * from "./hooks/useEditorForceUpdate"; export * from "./HyperlinkToolbar/HyperlinkToolbarFactory"; -export * from "./SuggestionsMenu/SuggestionsMenuFactory"; +export * from "./SlashMenu/SlashMenuFactory"; From 5a29c713537b65f4386d2a497e87ef5627d40b62 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 13 Jan 2023 22:54:15 +0100 Subject: [PATCH 49/55] Updated slash menu selector for tests --- tests/utils/const.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/const.ts b/tests/utils/const.ts index 2a8e271e56..0ab5e9ba0d 100644 --- a/tests/utils/const.ts +++ b/tests/utils/const.ts @@ -18,6 +18,6 @@ export const DRAG_HANDLE_SELECTOR = `[data-test="dragHandle"]`; export const DRAG_HANDLE_ADD_SELECTOR = `[data-test="dragHandleAdd"]`; export const DRAG_HANDLE_MENU_SELECTOR = `.mantine-DragHandleMenu-root`; -export const SLASH_MENU_SELECTOR = `.mantine-SuggestionList-root`; +export const SLASH_MENU_SELECTOR = `.mantine-SlashMenu-root`; export const TYPE_DELAY = 10; From 275341a54fef5df888bc6fde5f4a4ab49d8be790 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 16 Jan 2023 22:57:53 +0100 Subject: [PATCH 50/55] Split factory params into static and dynamic --- .../vanilla/src/ui/hyperlinkToolbarFactory.ts | 11 +- examples/vanilla/src/ui/slashMenuFactory.ts | 6 +- .../BlockSideMenuFactoryTypes.ts | 12 +- .../DraggableBlocks/DraggableBlocksPlugin.ts | 106 ++++++++++-------- .../FormattingToolbarFactoryTypes.ts | 30 +++-- .../FormattingToolbarPlugin.ts | 101 +++++------------ .../HyperlinkToolbarFactoryTypes.ts | 17 ++- .../HyperlinkToolbarPlugin.ts | 43 +++---- packages/core/src/shared/EditorElement.ts | 13 ++- .../plugins/suggestion/SuggestionPlugin.ts | 102 +++++++++-------- .../suggestion/SuggestionsMenuFactoryTypes.ts | 13 ++- .../BlockSideMenu/BlockSideMenuFactory.tsx | 35 ++---- .../FormattingToolbarFactory.tsx | 71 ++---------- .../components/FormattingToolbar.tsx | 74 ++++++------ .../HyperlinkToolbarFactory.tsx | 37 +++--- .../react/src/SlashMenu/SlashMenuFactory.tsx | 39 +++---- .../src/SlashMenu/components/SlashMenu.tsx | 2 + 17 files changed, 305 insertions(+), 407 deletions(-) diff --git a/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts index 46579b6d68..4c02614713 100644 --- a/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts +++ b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts @@ -12,10 +12,14 @@ export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = (props) => { container.style.padding = "10px"; container.style.opacity = "0.8"; + let url = ""; + let text = ""; + const editBtn = createButton("edit", () => { - const newUrl = prompt("new url") || props.url; - props.editHyperlink(newUrl, props.text); + const newUrl = prompt("new url") || url; + props.editHyperlink(newUrl, text); }); + container.appendChild(editBtn); const removeBtn = createButton("remove", () => { @@ -30,6 +34,9 @@ export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = (props) => { return { element: container, show: (params) => { + url = params.url; + text = params.text; + container.style.display = "block"; console.log("show", params); diff --git a/examples/vanilla/src/ui/slashMenuFactory.ts b/examples/vanilla/src/ui/slashMenuFactory.ts index 4408554149..842d5bebdf 100644 --- a/examples/vanilla/src/ui/slashMenuFactory.ts +++ b/examples/vanilla/src/ui/slashMenuFactory.ts @@ -6,7 +6,7 @@ import { createButton } from "./util"; * or when the mouse is hovering over a hyperlink */ export const slashMenuFactory: SuggestionsMenuFactory = ( - _props + props ) => { const container = document.createElement("div"); container.style.background = "gray"; @@ -39,7 +39,7 @@ export const slashMenuFactory: SuggestionsMenuFactory = ( return { element: container, show: (params) => { - updateItems(params.items, params.itemCallback, params.selectedItemIndex); + updateItems(params.items, props.itemCallback, params.selectedItemIndex); container.style.display = "block"; console.log("show", params); @@ -51,7 +51,7 @@ export const slashMenuFactory: SuggestionsMenuFactory = ( }, update: (params) => { console.log("update", params); - updateItems(params.items, params.itemCallback, params.selectedItemIndex); + updateItems(params.items, props.itemCallback, params.selectedItemIndex); container.style.top = params.queryStartBoundingBox.y + "px"; container.style.left = params.queryStartBoundingBox.x + "px"; }, diff --git a/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts index 4a4dbb4934..333a763f21 100644 --- a/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts +++ b/packages/core/src/extensions/DraggableBlocks/BlockSideMenuFactoryTypes.ts @@ -1,14 +1,20 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; -export type BlockSideMenuParams = { +export type BlockSideMenuStaticParams = { addBlock: () => void; deleteBlock: () => void; blockDragStart: (event: DragEvent) => void; blockDragEnd: () => void; freezeMenu: () => void; unfreezeMenu: () => void; +}; + +export type BlockSideMenuDynamicParams = { blockBoundingBox: DOMRect; }; -export type BlockSideMenu = EditorElement; -export type BlockSideMenuFactory = ElementFactory; +export type BlockSideMenu = EditorElement; +export type BlockSideMenuFactory = ElementFactory< + BlockSideMenuStaticParams, + BlockSideMenuDynamicParams +>; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 06e4f39e4d..66f61206aa 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -7,8 +7,9 @@ import { MultipleNodeSelection } from "../Blocks/MultipleNodeSelection"; import { DraggableBlocksOptions } from "./DraggableBlocksExtension"; import { BlockSideMenu, + BlockSideMenuDynamicParams, BlockSideMenuFactory, - BlockSideMenuParams, + BlockSideMenuStaticParams, } from "./BlockSideMenuFactoryTypes"; import { getBlockInfoFromPos } from "../Blocks/helpers/getBlockInfoFromPos"; import { SlashMenuPluginKey } from "../SlashMenu/SlashMenuExtension"; @@ -237,14 +238,13 @@ export class BlockMenuView { // When false, the drag handle with be just to the left of the element horizontalPosAnchoredAtRoot: boolean; - blockMenuParams: BlockSideMenuParams; blockMenu: BlockSideMenu; + hoveredBlock: HTMLElement | undefined; + menuOpen = false; menuFrozen = false; - blockID: string | undefined; - constructor({ editor, blockMenuFactory, @@ -253,20 +253,7 @@ export class BlockMenuView { this.editor = editor; this.horizontalPosAnchoredAtRoot = horizontalPosAnchoredAtRoot; - this.blockMenuParams = { - addBlock: () => this.addBlock({ left: 0, top: 0 }), - deleteBlock: () => this.deleteBlock({ left: 0, top: 0 }), - blockDragStart: (event: DragEvent) => dragStart(event, this.editor.view), - blockDragEnd: () => unsetDragImage(), - freezeMenu: () => { - this.menuFrozen = true; - }, - unfreezeMenu: () => { - this.menuFrozen = false; - }, - blockBoundingBox: new DOMRect(), - }; - this.blockMenu = blockMenuFactory(this.blockMenuParams); + this.blockMenu = blockMenuFactory(this.getStaticParams()); // Shows or updates menu position whenever the cursor moves, if the menu isn't frozen. document.body.addEventListener( @@ -294,46 +281,28 @@ export class BlockMenuView { } // Doesn't update if the menu is already open and the mouse cursor is still hovering the same block. - if (this.menuOpen && this.blockID === block.id) { + if ( + this.menuOpen && + this.hoveredBlock?.hasAttribute("data-id") && + this.hoveredBlock?.getAttribute("data-id") === block.id + ) { return; } - this.blockID = block.id; // Gets the block's content node, which lets to ignore child blocks when determining the block menu's position. const blockContent = block.node.firstChild as HTMLElement; + this.hoveredBlock = blockContent; if (!blockContent) { return; } - // Gets bounding box of the block content. - const blockBoundingBox = blockContent.getBoundingClientRect(); - - this.blockMenuParams.addBlock = () => - this.addBlock({ - left: blockBoundingBox.left, - top: blockBoundingBox.top, - }); - this.blockMenuParams.deleteBlock = () => - this.deleteBlock({ - left: blockBoundingBox.left, - top: blockBoundingBox.top, - }); - this.blockMenuParams.blockBoundingBox = new DOMRect( - this.horizontalPosAnchoredAtRoot - ? getHorizontalAnchor() - : blockBoundingBox.x, - blockBoundingBox.y, - blockBoundingBox.width, - blockBoundingBox.height - ); - // Shows or updates elements. if (!this.menuOpen) { this.menuOpen = true; - this.blockMenu.show(this.blockMenuParams); + this.blockMenu.show(this.getDynamicParams()); } else { - this.blockMenu.update(this.blockMenuParams); + this.blockMenu.update(this.getDynamicParams()); } }, true @@ -378,12 +347,17 @@ export class BlockMenuView { } } - addBlock(coords: { left: number; top: number }) { + addBlock() { this.menuOpen = false; this.menuFrozen = true; this.blockMenu.hide(); - const pos = this.editor.view.posAtCoords(coords); + const blockBoundingBox = this.hoveredBlock!.getBoundingClientRect(); + + const pos = this.editor.view.posAtCoords({ + left: blockBoundingBox.left, + top: blockBoundingBox.top, + }); if (!pos) { return; } @@ -392,7 +366,6 @@ export class BlockMenuView { if (blockInfo === undefined) { return; } - console.log(blockInfo); const { contentNode, endPos } = blockInfo; @@ -420,17 +393,52 @@ export class BlockMenuView { ); } - deleteBlock(coords: { left: number; top: number }) { + deleteBlock() { this.menuOpen = false; this.blockMenu.hide(); - const pos = this.editor.view.posAtCoords(coords); + const blockBoundingBox = this.hoveredBlock!.getBoundingClientRect(); + + const pos = this.editor.view.posAtCoords({ + left: blockBoundingBox.left, + top: blockBoundingBox.top, + }); if (!pos) { return; } this.editor.commands.BNDeleteBlock(pos.pos); } + + getStaticParams(): BlockSideMenuStaticParams { + return { + addBlock: () => this.addBlock(), + deleteBlock: () => this.deleteBlock(), + blockDragStart: (event: DragEvent) => dragStart(event, this.editor.view), + blockDragEnd: () => unsetDragImage(), + freezeMenu: () => { + this.menuFrozen = true; + }, + unfreezeMenu: () => { + this.menuFrozen = false; + }, + }; + } + + getDynamicParams(): BlockSideMenuDynamicParams { + const blockBoundingBox = this.hoveredBlock!.getBoundingClientRect(); + + return { + blockBoundingBox: new DOMRect( + this.horizontalPosAnchoredAtRoot + ? getHorizontalAnchor() + : blockBoundingBox.x, + blockBoundingBox.y, + blockBoundingBox.width, + blockBoundingBox.height + ), + }; + } } export const createDraggableBlocksPlugin = ( diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts index 19fc726ce6..a9293d92df 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts @@ -1,31 +1,37 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; -export type FormattingToolbarParams = { - boldIsActive: boolean; +export type FormattingToolbarStaticParams = { toggleBold: () => void; - italicIsActive: boolean; toggleItalic: () => void; - underlineIsActive: boolean; toggleUnderline: () => void; - strikeIsActive: boolean; toggleStrike: () => void; + setHyperlink: (url: string, text?: string) => void; + + setParagraph: () => void; + setHeading: (level: string) => void; + setListItem: (type: string) => void; +}; + +export type FormattingToolbarDynamicParams = { + boldIsActive: boolean; + italicIsActive: boolean; + underlineIsActive: boolean; + strikeIsActive: boolean; hyperlinkIsActive: boolean; activeHyperlinkUrl: string; activeHyperlinkText: string; - setHyperlink: (url: string, text?: string) => void; paragraphIsActive: boolean; - setParagraph: () => void; headingIsActive: boolean; activeHeadingLevel: string; - setHeading: (level: string) => void; - setListItem: (type: string) => void; listItemIsActive: boolean; activeListItemType: string; selectionBoundingBox: DOMRect; - editorElement: Element; }; -export type FormattingToolbar = EditorElement; -export type FormattingToolbarFactory = ElementFactory; +export type FormattingToolbar = EditorElement; +export type FormattingToolbarFactory = ElementFactory< + FormattingToolbarStaticParams, + FormattingToolbarDynamicParams +>; diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index d5c5ad0662..f98013e06d 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -8,8 +8,9 @@ import { EditorState, Plugin, PluginKey } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { FormattingToolbar, + FormattingToolbarDynamicParams, FormattingToolbarFactory, - FormattingToolbarParams, + FormattingToolbarStaticParams, } from "./FormattingToolbarFactoryTypes"; // Same as TipTap bubblemenu plugin, but with these changes: @@ -39,8 +40,6 @@ export class FormattingToolbarView { public view: EditorView; - public formattingToolbarParams: FormattingToolbarParams; - public formattingToolbar: FormattingToolbar; public preventHide = false; @@ -72,10 +71,7 @@ export class FormattingToolbarView { this.editor = editor; this.view = view; - this.formattingToolbarParams = this.initFormattingToolbarParams(); - this.formattingToolbar = formattingToolbarFactory( - this.formattingToolbarParams - ); + this.formattingToolbar = formattingToolbarFactory(this.getStaticParams()); if (shouldShow) { this.shouldShow = shouldShow; @@ -160,8 +156,7 @@ export class FormattingToolbarView { !this.preventShow && (shouldShow || this.preventHide) ) { - this.updateFormattingToolbarParams(); - this.formattingToolbar.show(this.formattingToolbarParams); + this.formattingToolbar.show(this.getDynamicParams()); this.toolbarIsOpen = true; // TODO: Is this necessary? Also for other menu plugins. @@ -179,9 +174,7 @@ export class FormattingToolbarView { !this.preventShow && (shouldShow || this.preventHide) ) { - this.updateFormattingToolbarParams(); - this.formattingToolbar.update(this.formattingToolbarParams); - + this.formattingToolbar.update(this.getDynamicParams()); return; } @@ -233,36 +226,24 @@ export class FormattingToolbarView { return posToDOMRect(this.editor.view, from, to); } - initFormattingToolbarParams(): FormattingToolbarParams { + getStaticParams(): FormattingToolbarStaticParams { return { - boldIsActive: this.editor.isActive("bold"), toggleBold: () => { this.editor.view.focus(); this.editor.commands.toggleBold(); }, - italicIsActive: this.editor.isActive("italic"), toggleItalic: () => { this.editor.view.focus(); this.editor.commands.toggleItalic(); }, - underlineIsActive: this.editor.isActive("underline"), toggleUnderline: () => { this.editor.view.focus(); this.editor.commands.toggleUnderline(); }, - strikeIsActive: this.editor.isActive("strike"), toggleStrike: () => { this.editor.view.focus(); this.editor.commands.toggleStrike(); }, - hyperlinkIsActive: this.editor.isActive("link"), - activeHyperlinkUrl: this.editor.getAttributes("link").href - ? this.editor.getAttributes("link").href - : "", - activeHyperlinkText: this.editor.state.doc.textBetween( - this.editor.state.selection.from, - this.editor.state.selection.to - ), setHyperlink: (url: string, text?: string) => { if (url === "") { return; @@ -283,8 +264,6 @@ export class FormattingToolbarView { ); this.editor.view.focus(); }, - paragraphIsActive: - this.editor.state.selection.$from.node().type.name === "textContent", setParagraph: () => { this.editor.view.focus(); this.editor.commands.BNSetContentType( @@ -292,10 +271,6 @@ export class FormattingToolbarView { "textContent" ); }, - headingIsActive: - this.editor.state.selection.$from.node().type.name === "headingContent", - activeHeadingLevel: - this.editor.state.selection.$from.node().attrs["headingLevel"], setHeading: (level: string = "1") => { this.editor.view.focus(); this.editor.commands.BNSetContentType( @@ -306,11 +281,6 @@ export class FormattingToolbarView { } ); }, - listItemIsActive: - this.editor.state.selection.$from.node().type.name === - "listItemContent", - activeListItemType: - this.editor.state.selection.$from.node().attrs["listItemType"], setListItem: (type: string = "unordered") => { this.editor.view.focus(); this.editor.commands.BNSetContentType( @@ -321,45 +291,36 @@ export class FormattingToolbarView { } ); }, - selectionBoundingBox: this.getSelectionBoundingBox(), - editorElement: this.editor.options.element, }; } - updateFormattingToolbarParams() { - this.formattingToolbarParams.boldIsActive = this.editor.isActive("bold"); - this.formattingToolbarParams.italicIsActive = - this.editor.isActive("italic"); - this.formattingToolbarParams.underlineIsActive = - this.editor.isActive("underline"); - this.formattingToolbarParams.strikeIsActive = - this.editor.isActive("strike"); - this.formattingToolbarParams.hyperlinkIsActive = - this.editor.isActive("link"); - this.formattingToolbarParams.activeHyperlinkUrl = this.editor.getAttributes( - "link" - ).href - ? this.editor.getAttributes("link").href - : ""; - this.formattingToolbarParams.activeHyperlinkText = - this.editor.state.doc.textBetween( + getDynamicParams(): FormattingToolbarDynamicParams { + return { + boldIsActive: this.editor.isActive("bold"), + italicIsActive: this.editor.isActive("italic"), + underlineIsActive: this.editor.isActive("underline"), + strikeIsActive: this.editor.isActive("strike"), + hyperlinkIsActive: this.editor.isActive("link"), + activeHyperlinkUrl: this.editor.getAttributes("link").href + ? this.editor.getAttributes("link").href + : "", + activeHyperlinkText: this.editor.state.doc.textBetween( this.editor.state.selection.from, this.editor.state.selection.to - ); - - this.formattingToolbarParams.paragraphIsActive = - this.editor.state.selection.$from.node().type.name === "textContent"; - this.formattingToolbarParams.headingIsActive = - this.editor.state.selection.$from.node().type.name === "headingContent"; - this.formattingToolbarParams.activeHeadingLevel = - this.editor.state.selection.$from.node().attrs["headingLevel"]; - this.formattingToolbarParams.listItemIsActive = - this.editor.state.selection.$from.node().type.name === "listItemContent"; - this.formattingToolbarParams.activeListItemType = - this.editor.state.selection.$from.node().attrs["listItemType"]; - - this.formattingToolbarParams.selectionBoundingBox = - this.getSelectionBoundingBox(); + ), + paragraphIsActive: + this.editor.state.selection.$from.node().type.name === "textContent", + headingIsActive: + this.editor.state.selection.$from.node().type.name === "headingContent", + activeHeadingLevel: + this.editor.state.selection.$from.node().attrs["headingLevel"], + listItemIsActive: + this.editor.state.selection.$from.node().type.name === + "listItemContent", + activeListItemType: + this.editor.state.selection.$from.node().attrs["listItemType"], + selectionBoundingBox: this.getSelectionBoundingBox(), + }; } } diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts index fa1cc6758a..77c8cc2b41 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes.ts @@ -1,14 +1,19 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; -export type HyperlinkToolbarParams = { - url: string; - text: string; +export type HyperlinkToolbarStaticParams = { editHyperlink: (url: string, text: string) => void; deleteHyperlink: () => void; +}; + +export type HyperlinkToolbarDynamicParams = { + url: string; + text: string; boundingBox: DOMRect; - editorElement: Element; }; -export type HyperlinkToolbar = EditorElement; -export type HyperlinkToolbarFactory = ElementFactory; +export type HyperlinkToolbar = EditorElement; +export type HyperlinkToolbarFactory = ElementFactory< + HyperlinkToolbarStaticParams, + HyperlinkToolbarDynamicParams +>; diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 1dc4dddd58..963e27dc72 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -3,8 +3,9 @@ import { Mark } from "prosemirror-model"; import { Plugin, PluginKey } from "prosemirror-state"; import { HyperlinkToolbar, + HyperlinkToolbarDynamicParams, HyperlinkToolbarFactory, - HyperlinkToolbarParams, + HyperlinkToolbarStaticParams, } from "./HyperlinkToolbarFactoryTypes"; const PLUGIN_KEY = new PluginKey("HyperlinkToolbarPlugin"); @@ -20,7 +21,6 @@ export type HyperlinkToolbarViewProps = { class HyperlinkToolbarView { editor: Editor; - hyperlinkToolbarParams: HyperlinkToolbarParams; hyperlinkToolbar: HyperlinkToolbar; menuUpdateTimer: NodeJS.Timeout | undefined; @@ -39,10 +39,7 @@ class HyperlinkToolbarView { constructor({ editor, hyperlinkToolbarFactory }: HyperlinkToolbarViewProps) { this.editor = editor; - this.hyperlinkToolbarParams = this.initHyperlinkToolbarParams(); - this.hyperlinkToolbar = hyperlinkToolbarFactory( - this.hyperlinkToolbarParams - ); + this.hyperlinkToolbar = hyperlinkToolbarFactory(this.getStaticParams()); this.startMenuUpdateTimer = () => { this.menuUpdateTimer = setTimeout(() => { @@ -149,11 +146,11 @@ class HyperlinkToolbarView { } if (this.hyperlinkMark) { - this.updateHyperlinkToolbarParams(); + this.getDynamicParams(); // Shows menu. if (!prevHyperlinkMark) { - this.hyperlinkToolbar.show(this.hyperlinkToolbarParams); + this.hyperlinkToolbar.show(this.getDynamicParams()); this.hyperlinkToolbar.element?.addEventListener( "mouseleave", @@ -168,7 +165,7 @@ class HyperlinkToolbarView { } // Updates menu. - this.hyperlinkToolbar.update(this.hyperlinkToolbarParams); + this.hyperlinkToolbar.update(this.getDynamicParams()); } // Hides menu. @@ -188,10 +185,8 @@ class HyperlinkToolbarView { } } - initHyperlinkToolbarParams(): HyperlinkToolbarParams { + getStaticParams(): HyperlinkToolbarStaticParams { return { - url: "", - text: "", editHyperlink: (url: string, text: string) => { const tr = this.editor.view.state.tr.insertText( text, @@ -222,30 +217,22 @@ class HyperlinkToolbarView { this.hyperlinkToolbar.hide(); }, - - boundingBox: new DOMRect(), - editorElement: this.editor.options.element, }; } - updateHyperlinkToolbarParams() { - if (this.hyperlinkMark) { - this.hyperlinkToolbarParams.url = this.hyperlinkMark.attrs.href - ? this.hyperlinkMark.attrs.href - : ""; - this.hyperlinkToolbarParams.text = this.editor.view.state.doc.textBetween( + getDynamicParams(): HyperlinkToolbarDynamicParams { + return { + url: this.hyperlinkMark!.attrs.href, + text: this.editor.view.state.doc.textBetween( this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to - ); - } - - if (this.hyperlinkMarkRange) { - this.hyperlinkToolbarParams.boundingBox = posToDOMRect( + ), + boundingBox: posToDOMRect( this.editor.view, this.hyperlinkMarkRange!.from, this.hyperlinkMarkRange!.to - ); - } + ), + }; } } diff --git a/packages/core/src/shared/EditorElement.ts b/packages/core/src/shared/EditorElement.ts index 386df572c5..ce68afe778 100644 --- a/packages/core/src/shared/EditorElement.ts +++ b/packages/core/src/shared/EditorElement.ts @@ -1,10 +1,11 @@ -export type EditorElement> = { +export type EditorElement> = { element: HTMLElement | undefined; - show: (params: ElementParams) => void; + show: (params: ElementDynamicParams) => void; hide: () => void; - update: (params: ElementParams) => void; + update: (params: ElementDynamicParams) => void; }; -export type ElementFactory> = ( - params: ElementParams -) => EditorElement; +export type ElementFactory< + ElementStaticParams extends Record, + ElementDynamicParams extends Record +> = (params: ElementStaticParams) => EditorElement; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 8eb00914d0..92247d2a23 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -5,8 +5,9 @@ import { Decoration, DecorationSet, EditorView } from "prosemirror-view"; import { findBlock } from "../../../extensions/Blocks/helpers/findBlock"; import { SuggestionsMenu, + SuggestionsMenuDynamicParams, SuggestionsMenuFactory, - SuggestionsMenuParams, + SuggestionsMenuStaticParams, } from "./SuggestionsMenuFactoryTypes"; import { SuggestionItem } from "./SuggestionItem"; @@ -47,6 +48,17 @@ export type SuggestionPluginOptions = { allow?: (props: { editor: Editor; range: Range }) => boolean; }; +type SuggestionPluginState = { + active: boolean; + range: Range | null; + query: string | null; + notFoundCount: number; + items: T[]; + selectedItemIndex: number; + type: string; + decorationId: string | null; +}; + type SuggestionPluginViewOptions = { editor: Editor; pluginKey: PluginKey; @@ -99,11 +111,11 @@ class SuggestionPluginView { editor: Editor; pluginKey: PluginKey; - itemCallback: (props: { item: T; editor: Editor; range: Range }) => void; - - suggestionsMenuParams: SuggestionsMenuParams; suggestionsMenu: SuggestionsMenu; + pluginState: SuggestionPluginState; + itemCallback: (item: T) => void; + constructor({ editor, pluginKey, @@ -113,10 +125,25 @@ class SuggestionPluginView { this.editor = editor; this.pluginKey = pluginKey; - this.itemCallback = selectItemCallback; + this.pluginState = { + active: false, + range: null, + query: null, + notFoundCount: 0, + items: [], + selectedItemIndex: 0, + type: "slash", + decorationId: null, + }; + + this.itemCallback = (item: T) => + selectItemCallback({ + item: item, + editor: editor, + range: this.pluginState.range as Range, + }); - this.suggestionsMenuParams = this.initSuggestionsMenuParams(); - this.suggestionsMenu = suggestionsMenuFactory(this.suggestionsMenuParams); + this.suggestionsMenu = suggestionsMenuFactory(this.getStaticParams()); } update(view: EditorView, prevState: EditorState) { @@ -135,7 +162,7 @@ class SuggestionPluginView { return; } - const state = stopped ? prev : next; + this.pluginState = stopped ? prev : next; if (stopped) { this.suggestionsMenu.hide(); @@ -147,13 +174,11 @@ class SuggestionPluginView { } if (changed) { - this.updateSuggestionsMenuParams(state); - this.suggestionsMenu.update(this.suggestionsMenuParams); + this.suggestionsMenu.update(this.getDynamicParams()); } if (started) { - this.updateSuggestionsMenuParams(state); - this.suggestionsMenu.show(this.suggestionsMenuParams); + this.suggestionsMenu.show(this.getDynamicParams()); // Listener stops focus moving to the menu on click. this.suggestionsMenu.element!.addEventListener("mousedown", (event) => @@ -162,41 +187,22 @@ class SuggestionPluginView { } } - initSuggestionsMenuParams(): SuggestionsMenuParams { + getStaticParams(): SuggestionsMenuStaticParams { return { - items: [], - selectedItemIndex: 0, - itemCallback: (item: T) => { - this.itemCallback({ - item: item, - editor: this.editor, - range: { from: 0, to: 0 }, - }); - }, - queryStartBoundingBox: new DOMRect(), - editorElement: this.editor.options.element, + itemCallback: (item: T) => this.itemCallback(item), }; } - updateSuggestionsMenuParams(pluginState: any) { - this.suggestionsMenuParams.items = pluginState.items; - this.suggestionsMenuParams.selectedItemIndex = - pluginState.selectedItemIndex; - this.suggestionsMenuParams.itemCallback = (item: T) => { - this.itemCallback({ - item: item, - editor: this.editor, - range: pluginState.range, - }); - }; - + getDynamicParams(): SuggestionsMenuDynamicParams { const decorationNode = document.querySelector( - `[data-decoration-id="${pluginState.decorationId}"]` + `[data-decoration-id="${this.pluginState.decorationId}"]` ); - this.suggestionsMenuParams.queryStartBoundingBox = - decorationNode !== null - ? decorationNode.getBoundingClientRect() - : new DOMRect(); + + return { + items: this.pluginState.items, + selectedItemIndex: this.pluginState.selectedItemIndex, + queryStartBoundingBox: decorationNode!.getBoundingClientRect(), + }; } } @@ -247,16 +253,16 @@ export function createSuggestionPlugin({ state: { // Initialize the plugin's internal state. - init() { + init(): SuggestionPluginState { return { active: false, - range: {} as any, // TODO - query: null as string | null, + range: null, // TODO + query: null, notFoundCount: 0, items: [] as T[], selectedItemIndex: 0, type: "slash", - decorationId: null as string | null, + decorationId: null, }; }, @@ -301,7 +307,7 @@ export function createSuggestionPlugin({ !transaction.getMeta("pointer") ) { // Reset active state if we just left the previous suggestion range (e.g.: key arrows moving before /) - if (prev.active && selection.from <= prev.range.from) { + if (prev.active && selection.from <= prev.range!.from) { next.active = false; } else if (transaction.getMeta(pluginKey)?.activate) { // Start showing suggestions. activate has been set after typing a "/" (or whatever the specified character is), so let's create the decoration and initialize @@ -346,7 +352,7 @@ export function createSuggestionPlugin({ } else { // Update the "notFoundCount", // which indicates how many characters have been typed after showing no results - if (next.range.to > prev.range.to) { + if (next.range!.to > prev.range!.to) { // Text has been entered (selection moved to right), but still no items found, update Count next.notFoundCount = prev.notFoundCount + 1; } else { @@ -364,7 +370,7 @@ export function createSuggestionPlugin({ // Make sure to empty the range if suggestion is inactive if (!next.active) { next.decorationId = null; - next.range = {}; + next.range = null; next.query = null; next.notFoundCount = 0; next.items = []; diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts index dd8b1399ef..390a17bb4e 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionsMenuFactoryTypes.ts @@ -1,18 +1,21 @@ import { EditorElement, ElementFactory } from "../../EditorElement"; import { SuggestionItem } from "./SuggestionItem"; -export type SuggestionsMenuParams = { +export type SuggestionsMenuStaticParams = { + itemCallback: (item: T) => void; +}; + +export type SuggestionsMenuDynamicParams = { items: T[]; selectedItemIndex: number; - itemCallback: (item: T) => void; queryStartBoundingBox: DOMRect; - editorElement: Element; }; export type SuggestionsMenu = EditorElement< - SuggestionsMenuParams + SuggestionsMenuDynamicParams >; export type SuggestionsMenuFactory = ElementFactory< - SuggestionsMenuParams + SuggestionsMenuStaticParams, + SuggestionsMenuDynamicParams >; diff --git a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx index 6060c3045c..62f507e341 100644 --- a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx +++ b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx @@ -1,41 +1,32 @@ import { BlockSideMenu, + BlockSideMenuDynamicParams, BlockSideMenuFactory, - BlockSideMenuParams, + BlockSideMenuStaticParams, } from "@blocknote/core"; -import { - BlockSideMenu as ReactSideBlockMenu, - BlockSideMenuProps, -} from "./components/BlockSideMenu"; +import { BlockSideMenu as ReactSideBlockMenu } from "./components/BlockSideMenu"; import { createRoot, Root } from "react-dom/client"; import { MantineProvider } from "@mantine/core"; import { BlockNoteTheme } from "../BlockNoteTheme"; import Tippy from "@tippyjs/react"; export const ReactBlockSideMenuFactory: BlockSideMenuFactory = ( - params: BlockSideMenuParams + staticParams: BlockSideMenuStaticParams ): BlockSideMenu => { - const blockMenuProps: BlockSideMenuProps = { ...params }; - - function updateBlockMenuProps(params: BlockSideMenuParams) { - blockMenuProps.addBlock = params.addBlock; - blockMenuProps.deleteBlock = params.deleteBlock; - } - // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other menu factories do the same. const menuRootElement = document.createElement("div"); // menuRootElement.className = rootStyles.bnRoot; let menuRoot: Root | undefined; - function getMenuComponent() { + function getMenuComponent(dynamicParams: BlockSideMenuDynamicParams) { return ( } + content={} duration={0} - getReferenceClientRect={() => params.blockBoundingBox} + getReferenceClientRect={() => dynamicParams.blockBoundingBox} hideOnClick={false} interactive={true} offset={[0, 0]} @@ -49,23 +40,19 @@ export const ReactBlockSideMenuFactory: BlockSideMenuFactory = ( return { element: menuRootElement, - show: (params: BlockSideMenuParams) => { - updateBlockMenuProps(params); - + show: (dynamicParams: BlockSideMenuDynamicParams) => { document.body.appendChild(menuRootElement); menuRoot = createRoot(menuRootElement); - menuRoot.render(getMenuComponent()); + menuRoot.render(getMenuComponent(dynamicParams)); }, hide: () => { menuRoot!.unmount(); menuRootElement.remove(); }, - update: (params: BlockSideMenuParams) => { - updateBlockMenuProps(params); - - menuRoot!.render(getMenuComponent()); + update: (dynamicParams: BlockSideMenuDynamicParams) => { + menuRoot!.render(getMenuComponent(dynamicParams)); }, }; }; diff --git a/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx index bbc155cf51..7df5a8b76d 100644 --- a/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx +++ b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx @@ -1,80 +1,35 @@ import { createRoot, Root } from "react-dom/client"; import { FormattingToolbar, - FormattingToolbarParams, FormattingToolbarFactory, + FormattingToolbarStaticParams, + FormattingToolbarDynamicParams, } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; import Tippy from "@tippyjs/react"; -import { - FormattingToolbar as ReactFormattingToolbar, - FormattingToolbarProps, -} from "./components/FormattingToolbar"; +import { FormattingToolbar as ReactFormattingToolbar } from "./components/FormattingToolbar"; import { BlockNoteTheme } from "../BlockNoteTheme"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactFormattingToolbarFactory: FormattingToolbarFactory = ( - params: FormattingToolbarParams + staticParams: FormattingToolbarStaticParams ): FormattingToolbar => { - // TODO: Maybe just use {...params}? - const formattingToolbarProps: FormattingToolbarProps = { - boldIsActive: params.boldIsActive, - toggleBold: params.toggleBold, - italicIsActive: params.italicIsActive, - toggleItalic: params.toggleItalic, - underlineIsActive: params.underlineIsActive, - toggleUnderline: params.toggleUnderline, - strikeIsActive: params.strikeIsActive, - toggleStrike: params.toggleStrike, - hyperlinkIsActive: params.hyperlinkIsActive, - activeHyperlinkUrl: params.activeHyperlinkUrl, - activeHyperlinkText: params.activeHyperlinkText, - setHyperlink: params.setHyperlink, - - paragraphIsActive: params.paragraphIsActive, - setParagraph: params.setParagraph, - headingIsActive: params.headingIsActive, - activeHeadingLevel: params.activeHeadingLevel, - setHeading: params.setHeading, - listItemIsActive: params.listItemIsActive, - activeListItemType: params.activeListItemType, - setListItem: params.setListItem, - }; - - function updateFormattingToolbarProps(params: FormattingToolbarParams) { - formattingToolbarProps.boldIsActive = params.boldIsActive; - formattingToolbarProps.italicIsActive = params.italicIsActive; - formattingToolbarProps.underlineIsActive = params.underlineIsActive; - formattingToolbarProps.strikeIsActive = params.strikeIsActive; - formattingToolbarProps.hyperlinkIsActive = params.hyperlinkIsActive; - formattingToolbarProps.activeHyperlinkUrl = params.activeHyperlinkUrl; - formattingToolbarProps.activeHyperlinkText = params.activeHyperlinkText; - - formattingToolbarProps.paragraphIsActive = params.paragraphIsActive; - formattingToolbarProps.headingIsActive = params.headingIsActive; - formattingToolbarProps.activeHeadingLevel = params.activeHeadingLevel; - formattingToolbarProps.listItemIsActive = params.listItemIsActive; - formattingToolbarProps.activeListItemType = params.activeListItemType; - } - // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other menu factories do the same. const menuRootElement = document.createElement("div"); // menuRootElement.className = rootStyles.bnRoot; let menuRoot: Root | undefined; - function getMenuComponent() { + function getMenuComponent(dynamicParams: FormattingToolbarDynamicParams) { return ( + } duration={0} - getReferenceClientRect={() => params.selectionBoundingBox} + getReferenceClientRect={() => dynamicParams.selectionBoundingBox} hideOnClick={false} interactive={true} placement={"top-start"} @@ -87,23 +42,19 @@ export const ReactFormattingToolbarFactory: FormattingToolbarFactory = ( return { element: menuRootElement, - show: (params: FormattingToolbarParams) => { - updateFormattingToolbarProps(params); - + show: (dynamicParams: FormattingToolbarDynamicParams) => { document.body.appendChild(menuRootElement); menuRoot = createRoot(menuRootElement); - menuRoot.render(getMenuComponent()); + menuRoot.render(getMenuComponent(dynamicParams)); }, hide: () => { menuRoot!.unmount(); menuRootElement.remove(); }, - update: (params: FormattingToolbarParams) => { - updateFormattingToolbarProps(params); - - menuRoot!.render(getMenuComponent()); + update: (dynamicParams: FormattingToolbarDynamicParams) => { + menuRoot!.render(getMenuComponent(dynamicParams)); }, }; }; diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx index 412badb49b..52ea29a3ec 100644 --- a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx +++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx @@ -44,39 +44,36 @@ export type FormattingToolbarProps = { }; // TODO: add list options, indentation -export const FormattingToolbar = (props: { - formattingToolbarProps: FormattingToolbarProps; -}) => { +export const FormattingToolbar = (props: FormattingToolbarProps) => { const getActiveMarks = () => { const activeMarks = new Set(); - props.formattingToolbarProps.boldIsActive && activeMarks.add("bold"); - props.formattingToolbarProps.italicIsActive && activeMarks.add("italic"); - props.formattingToolbarProps.underlineIsActive && - activeMarks.add("underline"); - props.formattingToolbarProps.strikeIsActive && activeMarks.add("strike"); - props.formattingToolbarProps.hyperlinkIsActive && activeMarks.add("link"); + props.boldIsActive && activeMarks.add("bold"); + props.italicIsActive && activeMarks.add("italic"); + props.underlineIsActive && activeMarks.add("underline"); + props.strikeIsActive && activeMarks.add("strike"); + props.hyperlinkIsActive && activeMarks.add("link"); return activeMarks; }; const getActiveBlock = () => { - if (props.formattingToolbarProps.headingIsActive) { - if (props.formattingToolbarProps.activeHeadingLevel === "1") { + if (props.headingIsActive) { + if (props.activeHeadingLevel === "1") { return { text: "Heading 1", icon: RiH1, }; } - if (props.formattingToolbarProps.activeHeadingLevel === "2") { + if (props.activeHeadingLevel === "2") { return { text: "Heading 2", icon: RiH2, }; } - if (props.formattingToolbarProps.activeHeadingLevel === "3") { + if (props.activeHeadingLevel === "3") { return { text: "Heading 3", icon: RiH3, @@ -84,8 +81,8 @@ export const FormattingToolbar = (props: { } } - if (props.formattingToolbarProps.listItemIsActive) { - if (props.formattingToolbarProps.activeListItemType === "unordered") { + if (props.listItemIsActive) { + if (props.activeListItemType === "unordered") { return { text: "Bullet List", icon: RiListUnordered, @@ -114,77 +111,72 @@ export const FormattingToolbar = (props: { icon={activeBlock!.icon} items={[ { - onClick: () => props.formattingToolbarProps.setParagraph(), + onClick: () => props.setParagraph(), text: "Text", icon: RiText, - isSelected: props.formattingToolbarProps.paragraphIsActive, + isSelected: props.paragraphIsActive, }, { - onClick: () => props.formattingToolbarProps.setHeading("1"), + onClick: () => props.setHeading("1"), text: "Heading 1", icon: RiH1, isSelected: - props.formattingToolbarProps.headingIsActive && - props.formattingToolbarProps.activeHeadingLevel === "1", + props.headingIsActive && props.activeHeadingLevel === "1", }, { - onClick: () => props.formattingToolbarProps.setHeading("2"), + onClick: () => props.setHeading("2"), text: "Heading 2", icon: RiH2, isSelected: - props.formattingToolbarProps.headingIsActive && - props.formattingToolbarProps.activeHeadingLevel === "2", + props.headingIsActive && props.activeHeadingLevel === "2", }, { - onClick: () => props.formattingToolbarProps.setHeading("3"), + onClick: () => props.setHeading("3"), text: "Heading 3", icon: RiH3, isSelected: - props.formattingToolbarProps.headingIsActive && - props.formattingToolbarProps.activeHeadingLevel === "3", + props.headingIsActive && props.activeHeadingLevel === "3", }, { - onClick: () => - props.formattingToolbarProps.setListItem("unordered"), + onClick: () => props.setListItem("unordered"), text: "Bullet List", icon: RiListUnordered, isSelected: - props.formattingToolbarProps.listItemIsActive && - props.formattingToolbarProps.activeListItemType === "unordered", + props.listItemIsActive && + props.activeListItemType === "unordered", }, { - onClick: () => props.formattingToolbarProps.setListItem("ordered"), + onClick: () => props.setListItem("ordered"), text: "Numbered List", icon: RiListOrdered, isSelected: - props.formattingToolbarProps.listItemIsActive && - props.formattingToolbarProps.activeListItemType === "ordered", + props.listItemIsActive && props.activeListItemType === "ordered", }, ]} /> {/* { - const hyperlinkToolbarProps: HyperlinkToolbarProps = { ...params }; - - function updateHyperlinkToolbarProps(params: HyperlinkToolbarParams) { - hyperlinkToolbarProps.url = params.url; - hyperlinkToolbarProps.text = params.text; - } - // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other UI factories do the same. const rootElement = document.createElement("div"); let root: Root | undefined; - function getComponent() { + function getComponent(dynamicParams: HyperlinkToolbarDynamicParams) { return ( } + content={ + + } duration={0} - getReferenceClientRect={() => params.boundingBox} + getReferenceClientRect={() => dynamicParams.boundingBox} hideOnClick={false} interactive={true} placement={"top"} @@ -48,23 +41,19 @@ export const ReactHyperlinkToolbarFactory: HyperlinkToolbarFactory = ( return { element: rootElement, - show: (params: HyperlinkToolbarParams) => { - updateHyperlinkToolbarProps(params); - + show: (dynamicParams: HyperlinkToolbarDynamicParams) => { document.body.appendChild(rootElement); root = createRoot(rootElement); - root.render(getComponent()); + root.render(getComponent(dynamicParams)); }, hide: () => { root!.unmount(); rootElement.remove(); }, - update: (params: HyperlinkToolbarParams) => { - updateHyperlinkToolbarProps(params); - - root!.render(getComponent()); + update: (dynamicParams: HyperlinkToolbarDynamicParams) => { + root!.render(getComponent(dynamicParams)); }, }; }; diff --git a/packages/react/src/SlashMenu/SlashMenuFactory.tsx b/packages/react/src/SlashMenu/SlashMenuFactory.tsx index 263090487a..ea9a14af84 100644 --- a/packages/react/src/SlashMenu/SlashMenuFactory.tsx +++ b/packages/react/src/SlashMenu/SlashMenuFactory.tsx @@ -2,44 +2,35 @@ import { createRoot, Root } from "react-dom/client"; import { SlashMenuItem, SuggestionsMenu, + SuggestionsMenuDynamicParams, SuggestionsMenuFactory, - SuggestionsMenuParams, + SuggestionsMenuStaticParams, } from "@blocknote/core"; import { MantineProvider } from "@mantine/core"; import Tippy from "@tippyjs/react"; -import { SlashMenu, SlashMenuProps } from "./components/SlashMenu"; +import { SlashMenu } from "./components/SlashMenu"; import { BlockNoteTheme } from "../BlockNoteTheme"; // import rootStyles from "../../../core/src/root.module.css"; export const ReactSlashMenuFactory: SuggestionsMenuFactory = ( - params: SuggestionsMenuParams + staticParams: SuggestionsMenuStaticParams ): SuggestionsMenu => { - const suggestionsMenuProps: SlashMenuProps = { - ...params, - }; - - function updateSuggestionsMenuProps( - params: SuggestionsMenuParams - ) { - suggestionsMenuProps.items = params.items; - suggestionsMenuProps.selectedItemIndex = params.selectedItemIndex; - suggestionsMenuProps.itemCallback = params.itemCallback; - } - // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other menu factories do the same. const menuRootElement = document.createElement("div"); // menuRootElement.className = rootStyles.bnRoot; let menuRoot: Root | undefined; - function getMenuComponent() { + function getMenuComponent( + dynamicParams: SuggestionsMenuDynamicParams + ) { return ( } + content={} duration={0} - getReferenceClientRect={() => params.queryStartBoundingBox} + getReferenceClientRect={() => dynamicParams.queryStartBoundingBox} hideOnClick={false} interactive={true} placement={"bottom-start"} @@ -52,23 +43,19 @@ export const ReactSlashMenuFactory: SuggestionsMenuFactory = ( return { element: menuRootElement as HTMLElement, - show: (params: SuggestionsMenuParams) => { - updateSuggestionsMenuProps(params); - + show: (dynamicParams: SuggestionsMenuDynamicParams) => { document.body.appendChild(menuRootElement); menuRoot = createRoot(menuRootElement); - menuRoot.render(getMenuComponent()); + menuRoot.render(getMenuComponent(dynamicParams)); }, hide: () => { menuRoot!.unmount(); menuRootElement.remove(); }, - update: (params: SuggestionsMenuParams) => { - updateSuggestionsMenuProps(params); - - menuRoot!.render(getMenuComponent()); + update: (dynamicParams: SuggestionsMenuDynamicParams) => { + menuRoot!.render(getMenuComponent(dynamicParams)); }, }; }; diff --git a/packages/react/src/SlashMenu/components/SlashMenu.tsx b/packages/react/src/SlashMenu/components/SlashMenu.tsx index a12b68f6a1..9aa81f1058 100644 --- a/packages/react/src/SlashMenu/components/SlashMenu.tsx +++ b/packages/react/src/SlashMenu/components/SlashMenu.tsx @@ -56,6 +56,7 @@ export function SlashMenu(props: SlashMenuProps) { key={item.name} name={item.name} hint={item.hint} + shortcut={item.shortcut} isSelected={props.selectedItemIndex === index} set={() => props.itemCallback(item)} /> @@ -75,6 +76,7 @@ export function SlashMenu(props: SlashMenuProps) { key={item.name} name={item.name} hint={item.hint} + shortcut={item.shortcut} isSelected={props.selectedItemIndex === index} set={() => props.itemCallback(item)} /> From f6cd54e6f5f8f78cf81dbf235b1bd14b855d5a0b Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Mon, 16 Jan 2023 22:58:33 +0100 Subject: [PATCH 51/55] Minor fix to `BNSetContentType()` --- packages/core/src/extensions/Blocks/nodes/Block.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/extensions/Blocks/nodes/Block.ts b/packages/core/src/extensions/Blocks/nodes/Block.ts index aee2aa93f4..6f8c036fbf 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.ts +++ b/packages/core/src/extensions/Blocks/nodes/Block.ts @@ -282,7 +282,7 @@ export const Block = Node.create({ return true; }, - // Changes the block at a given position to a given content type. + // Changes the content of a block at a given position to a given type. BNSetContentType: (posInBlock, type, attributes) => ({ state, dispatch }) => { @@ -291,12 +291,12 @@ export const Block = Node.create({ return false; } - const { startPos, endPos } = blockInfo; + const { startPos, contentNode } = blockInfo; if (dispatch) { state.tr.setBlockType( startPos + 1, - endPos - 1, + startPos + contentNode.nodeSize + 1, state.schema.node(type).type, attributes ); From 378b7d782e8fc263b1006c5f30a96d437deab79d Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 17 Jan 2023 14:56:38 +0100 Subject: [PATCH 52/55] Improved getting & setting block type in formatting toolbar factory API --- .../core/src/extensions/Blocks/nodes/Block.ts | 67 +++++++++-------- .../BlockTypes/HeadingBlock/HeadingContent.ts | 20 +++-- .../ListItemBlock/ListItemContent.ts | 30 +++++--- .../nodes/BlockTypes/TextBlock/TextContent.ts | 5 ++ .../DraggableBlocks/DraggableBlocksPlugin.ts | 2 +- .../FormattingToolbarFactoryTypes.ts | 14 ++-- .../FormattingToolbarPlugin.ts | 40 ++-------- .../extensions/SlashMenu/defaultCommands.tsx | 37 +++++++--- packages/core/src/index.ts | 1 + .../components/FormattingToolbar.tsx | 73 ++++++++++++------- 10 files changed, 162 insertions(+), 127 deletions(-) diff --git a/packages/core/src/extensions/Blocks/nodes/Block.ts b/packages/core/src/extensions/Blocks/nodes/Block.ts index 6f8c036fbf..c02d140ec3 100644 --- a/packages/core/src/extensions/Blocks/nodes/Block.ts +++ b/packages/core/src/extensions/Blocks/nodes/Block.ts @@ -5,16 +5,18 @@ import BlockAttributes from "../BlockAttributes"; import { getBlockInfoFromPos } from "../helpers/getBlockInfoFromPos"; import { PreviousBlockTypePlugin } from "../PreviousBlockTypePlugin"; import styles from "./Block.module.css"; -import { HeadingContentAttributes } from "./BlockTypes/HeadingBlock/HeadingContent"; -import { ListItemContentAttributes } from "./BlockTypes/ListItemBlock/ListItemContent"; +import { TextContentType } from "./BlockTypes/TextBlock/TextContent"; +import { HeadingContentType } from "./BlockTypes/HeadingBlock/HeadingContent"; +import { ListItemContentType } from "./BlockTypes/ListItemBlock/ListItemContent"; export interface IBlock { HTMLAttributes: Record; } -export type BlockContentAttributes = - | HeadingContentAttributes - | ListItemContentAttributes; +export type BlockContentType = + | TextContentType + | HeadingContentType + | ListItemContentType; declare module "@tiptap/core" { interface Commands { @@ -25,13 +27,11 @@ declare module "@tiptap/core" { BNSplitBlock: (posInBlock: number, keepType: boolean) => ReturnType; BNSetContentType: ( posInBlock: number, - type: string, - attributes?: BlockContentAttributes + type: BlockContentType ) => ReturnType; BNCreateBlockOrSetContentType: ( posInBlock: number, - type: string, - attributes?: BlockContentAttributes + type: BlockContentType ) => ReturnType; }; } @@ -284,7 +284,7 @@ export const Block = Node.create({ }, // Changes the content of a block at a given position to a given type. BNSetContentType: - (posInBlock, type, attributes) => + (posInBlock, type) => ({ state, dispatch }) => { const blockInfo = getBlockInfoFromPos(state.doc, posInBlock); if (blockInfo === undefined) { @@ -297,8 +297,8 @@ export const Block = Node.create({ state.tr.setBlockType( startPos + 1, startPos + contentNode.nodeSize + 1, - state.schema.node(type).type, - attributes + state.schema.node(type.name).type, + type.attrs ); } @@ -307,7 +307,7 @@ export const Block = Node.create({ // Changes the block at a given position to a given content type if it's empty, otherwise creates a new block of // that type below it. BNCreateBlockOrSetContentType: - (posInBlock, type, attributes) => + (posInBlock, type) => ({ state, chain }) => { const blockInfo = getBlockInfoFromPos(state.doc, posInBlock); if (blockInfo === undefined) { @@ -320,7 +320,7 @@ export const Block = Node.create({ const oldBlockContentPos = startPos + 1; return chain() - .BNSetContentType(posInBlock, type, attributes) + .BNSetContentType(posInBlock, type) .setTextSelection(oldBlockContentPos) .run(); } else { @@ -329,7 +329,7 @@ export const Block = Node.create({ return chain() .BNCreateBlock(newBlockInsertionPos) - .BNSetContentType(newBlockContentPos, type, attributes) + .BNSetContentType(newBlockContentPos, type) .setTextSelection(newBlockContentPos) .run(); } @@ -362,10 +362,9 @@ export const Block = Node.create({ const isTextBlock = contentType.name === "textContent"; if (selectionAtBlockStart && !isTextBlock) { - return commands.BNSetContentType( - state.selection.from, - "textContent" - ); + return commands.BNSetContentType(state.selection.from, { + name: "textContent", + }); } return false; @@ -506,41 +505,51 @@ export const Block = Node.create({ "Mod-Alt-1": () => this.editor.commands.BNSetContentType( this.editor.state.selection.anchor, - "headingContent", { - headingLevel: "1", + name: "headingContent", + attrs: { + headingLevel: "1", + }, } ), "Mod-Alt-2": () => this.editor.commands.BNSetContentType( this.editor.state.selection.anchor, - "headingContent", { - headingLevel: "2", + name: "headingContent", + attrs: { + headingLevel: "2", + }, } ), "Mod-Alt-3": () => this.editor.commands.BNSetContentType( this.editor.state.selection.anchor, - "headingContent", { - headingLevel: "3", + name: "headingContent", + attrs: { + headingLevel: "3", + }, } ), "Mod-Shift-7": () => this.editor.commands.BNSetContentType( this.editor.state.selection.anchor, - "listItemContent", { - listItemType: "unordered", + name: "listItemContent", + attrs: { + listItemType: "unordered", + }, } ), "Mod-Shift-8": () => this.editor.commands.BNSetContentType( this.editor.state.selection.anchor, - "listItemContent", { - listItemType: "ordered", + name: "listItemContent", + attrs: { + listItemType: "ordered", + }, } ), }; diff --git a/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts index 94fadeae7f..d49341c290 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockTypes/HeadingBlock/HeadingContent.ts @@ -1,8 +1,11 @@ import { InputRule, mergeAttributes, Node } from "@tiptap/core"; import styles from "../../Block.module.css"; -export type HeadingContentAttributes = { - headingLevel: string; +export type HeadingContentType = { + name: "headingContent"; + attrs?: { + headingLevel: string; + }; }; export const HeadingContent = Node.create({ @@ -33,8 +36,11 @@ export const HeadingContent = Node.create({ find: new RegExp(`^(#{${parseInt(level)}})\\s$`), handler: ({ state, chain, range }) => { chain() - .BNSetContentType(state.selection.from, "headingContent", { - headingLevel: level, + .BNSetContentType(state.selection.from, { + name: "headingContent", + attrs: { + headingLevel: level, + }, }) // Removes the "#" character(s) used to set the heading. .deleteRange({ from: range.from, to: range.to }); @@ -49,17 +55,17 @@ export const HeadingContent = Node.create({ { tag: "h1", attrs: { headingLevel: "1" }, - node: "block" + node: "block", }, { tag: "h2", attrs: { headingLevel: "2" }, - node: "block" + node: "block", }, { tag: "h3", attrs: { headingLevel: "3" }, - node: "block" + node: "block", }, ]; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts index 21cc0f1eea..08ba9ec6f0 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockTypes/ListItemBlock/ListItemContent.ts @@ -3,8 +3,11 @@ import { OrderedListItemIndexPlugin } from "./OrderedListItemIndexPlugin"; import { getBlockInfoFromPos } from "../../../helpers/getBlockInfoFromPos"; import styles from "../../Block.module.css"; -export type ListItemContentAttributes = { - listItemType: string; +export type ListItemContentType = { + name: "listItemContent"; + attrs?: { + listItemType: string; + }; }; export const ListItemContent = Node.create({ @@ -42,8 +45,11 @@ export const ListItemContent = Node.create({ find: new RegExp(`^[-+*]\\s$`), handler: ({ state, chain, range }) => { chain() - .BNSetContentType(state.selection.from, "listItemContent", { - listItemType: "unordered", + .BNSetContentType(state.selection.from, { + name: "listItemContent", + attrs: { + listItemType: "unordered", + }, }) // Removes the "-", "+", or "*" character used to set the list. .deleteRange({ from: range.from, to: range.to }); @@ -54,8 +60,11 @@ export const ListItemContent = Node.create({ find: new RegExp(`^1\\.\\s$`), handler: ({ state, chain, range }) => { chain() - .BNSetContentType(state.selection.from, "listItemContent", { - listItemType: "ordered", + .BNSetContentType(state.selection.from, { + name: "listItemContent", + attrs: { + listItemType: "ordered", + }, }) // Removes the "1." characters used to set the list. .deleteRange({ from: range.from, to: range.to }); @@ -83,10 +92,9 @@ export const ListItemContent = Node.create({ // Changes list item block to a text block if both the content is empty. commands.command(() => { if (node.textContent.length === 0) { - return commands.BNSetContentType( - state.selection.from, - "textContent" - ); + return commands.BNSetContentType(state.selection.from, { + name: "textContent", + }); } return false; @@ -151,7 +159,7 @@ export const ListItemContent = Node.create({ return false; }, - node: "block" + node: "block", }, ]; }, diff --git a/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts b/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts index 99dba95916..e6be56209a 100644 --- a/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts +++ b/packages/core/src/extensions/Blocks/nodes/BlockTypes/TextBlock/TextContent.ts @@ -1,6 +1,11 @@ import { Node } from "@tiptap/core"; import styles from "../../Block.module.css"; +export type TextContentType = { + name: "textContent"; + attrs?: {}; +}; + export const TextContent = Node.create({ name: "textContent", group: "blockContent", diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 66f61206aa..334e15b461 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -377,7 +377,7 @@ export class BlockMenuView { this.editor .chain() .BNCreateBlock(newBlockInsertionPos) - .BNSetContentType(newBlockContentPos, "textContent") + .BNSetContentType(newBlockContentPos, { name: "textContent" }) .setTextSelection(newBlockContentPos) .run(); } diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts index a9293d92df..fa055803ba 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarFactoryTypes.ts @@ -1,4 +1,5 @@ import { EditorElement, ElementFactory } from "../../shared/EditorElement"; +import { BlockContentType } from "../Blocks/nodes/Block"; export type FormattingToolbarStaticParams = { toggleBold: () => void; @@ -7,9 +8,7 @@ export type FormattingToolbarStaticParams = { toggleStrike: () => void; setHyperlink: (url: string, text?: string) => void; - setParagraph: () => void; - setHeading: (level: string) => void; - setListItem: (type: string) => void; + setBlockType: (type: BlockContentType) => void; }; export type FormattingToolbarDynamicParams = { @@ -21,11 +20,10 @@ export type FormattingToolbarDynamicParams = { activeHyperlinkUrl: string; activeHyperlinkText: string; - paragraphIsActive: boolean; - headingIsActive: boolean; - activeHeadingLevel: string; - listItemIsActive: boolean; - activeListItemType: string; + // BlockContentType is mostly used to set a block's type, so the attr field is optional as block content types have + // default values for attributes. However, it means that a block type's attributes field will never be undefined due to + // these default values, which the Required type enforces. + activeBlockType: Required; selectionBoundingBox: DOMRect; }; diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index f98013e06d..2b5f3bd97e 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -12,6 +12,7 @@ import { FormattingToolbarFactory, FormattingToolbarStaticParams, } from "./FormattingToolbarFactoryTypes"; +import { BlockContentType } from "../Blocks/nodes/Block"; // Same as TipTap bubblemenu plugin, but with these changes: // https://github.com/ueberdosis/tiptap/pull/2596/files @@ -264,31 +265,11 @@ export class FormattingToolbarView { ); this.editor.view.focus(); }, - setParagraph: () => { + setBlockType: (type: BlockContentType) => { this.editor.view.focus(); this.editor.commands.BNSetContentType( this.editor.state.selection.from, - "textContent" - ); - }, - setHeading: (level: string = "1") => { - this.editor.view.focus(); - this.editor.commands.BNSetContentType( - this.editor.state.selection.from, - "headingContent", - { - headingLevel: level, - } - ); - }, - setListItem: (type: string = "unordered") => { - this.editor.view.focus(); - this.editor.commands.BNSetContentType( - this.editor.state.selection.from, - "listItemContent", - { - listItemType: type, - } + type ); }, }; @@ -308,17 +289,10 @@ export class FormattingToolbarView { this.editor.state.selection.from, this.editor.state.selection.to ), - paragraphIsActive: - this.editor.state.selection.$from.node().type.name === "textContent", - headingIsActive: - this.editor.state.selection.$from.node().type.name === "headingContent", - activeHeadingLevel: - this.editor.state.selection.$from.node().attrs["headingLevel"], - listItemIsActive: - this.editor.state.selection.$from.node().type.name === - "listItemContent", - activeListItemType: - this.editor.state.selection.$from.node().attrs["listItemType"], + activeBlockType: { + name: this.editor.state.selection.$from.node().type.name, + attrs: this.editor.state.selection.$from.node().attrs, + } as Required, selectionBoundingBox: this.getSelectionBoundingBox(), }; } diff --git a/packages/core/src/extensions/SlashMenu/defaultCommands.tsx b/packages/core/src/extensions/SlashMenu/defaultCommands.tsx index cbd4c609e4..d5064b2f55 100644 --- a/packages/core/src/extensions/SlashMenu/defaultCommands.tsx +++ b/packages/core/src/extensions/SlashMenu/defaultCommands.tsx @@ -14,8 +14,11 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .chain() .focus() .deleteRange(range) - .BNCreateBlockOrSetContentType(range.from, "headingContent", { - headingLevel: "1", + .BNCreateBlockOrSetContentType(range.from, { + name: "headingContent", + attrs: { + headingLevel: "1", + }, }) .run(); }, @@ -33,8 +36,11 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .chain() .focus() .deleteRange(range) - .BNCreateBlockOrSetContentType(range.from, "headingContent", { - headingLevel: "2", + .BNCreateBlockOrSetContentType(range.from, { + name: "headingContent", + attrs: { + headingLevel: "2", + }, }) .run(); }, @@ -52,8 +58,11 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .chain() .focus() .deleteRange(range) - .BNCreateBlockOrSetContentType(range.from, "headingContent", { - headingLevel: "3", + .BNCreateBlockOrSetContentType(range.from, { + name: "headingContent", + attrs: { + headingLevel: "3", + }, }) .run(); }, @@ -71,8 +80,11 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .chain() .focus() .deleteRange(range) - .BNCreateBlockOrSetContentType(range.from, "listItemContent", { - listItemType: "ordered", + .BNCreateBlockOrSetContentType(range.from, { + name: "listItemContent", + attrs: { + listItemType: "ordered", + }, }) .run(); }, @@ -90,8 +102,11 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .chain() .focus() .deleteRange(range) - .BNCreateBlockOrSetContentType(range.from, "listItemContent", { - listItemType: "unordered", + .BNCreateBlockOrSetContentType(range.from, { + name: "listItemContent", + attrs: { + listItemType: "unordered", + }, }) .run(); }, @@ -109,7 +124,7 @@ const defaultCommands: { [key: string]: SlashMenuItem } = { .chain() .focus() .deleteRange(range) - .BNCreateBlockOrSetContentType(range.from, "textContent") + .BNCreateBlockOrSetContentType(range.from, { name: "textContent" }) .run(); }, ["p"], diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b915496fb5..f108f64c36 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,5 +1,6 @@ export * from "./BlockNoteEditor"; export * from "./BlockNoteExtensions"; +export type { BlockContentType } from "./extensions/Blocks/nodes/Block"; export * from "./extensions/FormattingToolbar/FormattingToolbarFactoryTypes"; export * from "./extensions/DraggableBlocks/BlockSideMenuFactoryTypes"; export * from "./extensions/HyperlinkToolbar/HyperlinkToolbarFactoryTypes"; diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx index 52ea29a3ec..2976950902 100644 --- a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx +++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx @@ -18,6 +18,7 @@ import { ToolbarDropdown } from "../../SharedComponents/Toolbar/components/Toolb import { Toolbar } from "../../SharedComponents/Toolbar/components/Toolbar"; import { formatKeyboardShortcut } from "../../utils"; import LinkToolbarButton from "./LinkToolbarButton"; +import { BlockContentType } from "@blocknote/core"; export type FormattingToolbarProps = { boldIsActive: boolean; @@ -33,14 +34,8 @@ export type FormattingToolbarProps = { activeHyperlinkText: string; setHyperlink: (url: string, text?: string) => void; - paragraphIsActive: boolean; - setParagraph: () => void; - headingIsActive: boolean; - activeHeadingLevel: string; - setHeading: (level: string) => void; - listItemIsActive: boolean; - activeListItemType: string; - setListItem: (type: string) => void; + activeBlockType: BlockContentType; + setBlockType: (type: BlockContentType) => void; }; // TODO: add list options, indentation @@ -58,22 +53,22 @@ export const FormattingToolbar = (props: FormattingToolbarProps) => { }; const getActiveBlock = () => { - if (props.headingIsActive) { - if (props.activeHeadingLevel === "1") { + if (props.activeBlockType.name === "headingContent") { + if (props.activeBlockType.attrs["headingLevel"] === "1") { return { text: "Heading 1", icon: RiH1, }; } - if (props.activeHeadingLevel === "2") { + if (props.activeBlockType.attrs["headingLevel"] === "2") { return { text: "Heading 2", icon: RiH2, }; } - if (props.activeHeadingLevel === "3") { + if (props.activeBlockType.attrs["headingLevel"] === "3") { return { text: "Heading 3", icon: RiH3, @@ -81,8 +76,8 @@ export const FormattingToolbar = (props: FormattingToolbarProps) => { } } - if (props.listItemIsActive) { - if (props.activeListItemType === "unordered") { + if (props.activeBlockType.name === "listItemContent") { + if (props.activeBlockType.attrs["listItemType"] === "unordered") { return { text: "Bullet List", icon: RiListUnordered, @@ -111,46 +106,70 @@ export const FormattingToolbar = (props: FormattingToolbarProps) => { icon={activeBlock!.icon} items={[ { - onClick: () => props.setParagraph(), + onClick: () => props.setBlockType({ name: "textContent" }), text: "Text", icon: RiText, - isSelected: props.paragraphIsActive, + isSelected: props.activeBlockType.name === "textContent", }, { - onClick: () => props.setHeading("1"), + onClick: () => + props.setBlockType({ + name: "headingContent", + attrs: { headingLevel: "1" }, + }), text: "Heading 1", icon: RiH1, isSelected: - props.headingIsActive && props.activeHeadingLevel === "1", + props.activeBlockType.name === "headingContent" && + props.activeBlockType.attrs["headingLevel"] === "1", }, { - onClick: () => props.setHeading("2"), + onClick: () => + props.setBlockType({ + name: "headingContent", + attrs: { headingLevel: "2" }, + }), text: "Heading 2", icon: RiH2, isSelected: - props.headingIsActive && props.activeHeadingLevel === "2", + props.activeBlockType.name === "headingContent" && + props.activeBlockType.attrs["headingLevel"] === "2", }, { - onClick: () => props.setHeading("3"), + onClick: () => + props.setBlockType({ + name: "headingContent", + attrs: { headingLevel: "3" }, + }), text: "Heading 3", icon: RiH3, isSelected: - props.headingIsActive && props.activeHeadingLevel === "3", + props.activeBlockType.name === "headingContent" && + props.activeBlockType.attrs["headingLevel"] === "3", }, { - onClick: () => props.setListItem("unordered"), + onClick: () => + props.setBlockType({ + name: "listItemContent", + attrs: { listItemType: "unordered" }, + }), text: "Bullet List", icon: RiListUnordered, isSelected: - props.listItemIsActive && - props.activeListItemType === "unordered", + props.activeBlockType.name === "listItemContent" && + props.activeBlockType.attrs["listItemType"] === "unordered", }, { - onClick: () => props.setListItem("ordered"), + onClick: () => + props.setBlockType({ + name: "listItemContent", + attrs: { listItemType: "ordered" }, + }), text: "Numbered List", icon: RiListOrdered, isSelected: - props.listItemIsActive && props.activeListItemType === "ordered", + props.activeBlockType.name === "listItemContent" && + props.activeBlockType.attrs["listItemType"] === "ordered", }, ]} /> From ff61295363a11d07b1e45ab849ac466bc25e4543 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Tue, 17 Jan 2023 15:06:56 +0100 Subject: [PATCH 53/55] Minor typing fix --- .../src/FormattingToolbar/components/FormattingToolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx index 2976950902..485fd4b575 100644 --- a/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx +++ b/packages/react/src/FormattingToolbar/components/FormattingToolbar.tsx @@ -34,7 +34,7 @@ export type FormattingToolbarProps = { activeHyperlinkText: string; setHyperlink: (url: string, text?: string) => void; - activeBlockType: BlockContentType; + activeBlockType: Required; setBlockType: (type: BlockContentType) => void; }; From bdde1e69decf5de11bc71c45be1bdf0e3a6eaff7 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Wed, 18 Jan 2023 12:31:43 +0100 Subject: [PATCH 54/55] Combined factory `update()` and `show()` functions into single `render()` function --- .../vanilla/src/ui/blockSideMenuFactory.ts | 13 +++----- .../src/ui/formattingToolbarFactory.ts | 12 +++---- .../vanilla/src/ui/hyperlinkToolbarFactory.ts | 17 ++++------ examples/vanilla/src/ui/slashMenuFactory.ts | 15 ++++----- .../DraggableBlocks/DraggableBlocksPlugin.ts | 4 +-- .../FormattingToolbarPlugin.ts | 4 +-- .../HyperlinkToolbarPlugin.ts | 4 +-- packages/core/src/shared/EditorElement.ts | 3 +- .../plugins/suggestion/SuggestionPlugin.ts | 4 +-- .../BlockSideMenu/BlockSideMenuFactory.tsx | 29 ++++++++--------- .../FormattingToolbarFactory.tsx | 32 ++++++++++--------- .../HyperlinkToolbarFactory.tsx | 16 ++++++---- .../react/src/SlashMenu/SlashMenuFactory.tsx | 32 ++++++++++--------- 13 files changed, 89 insertions(+), 96 deletions(-) diff --git a/examples/vanilla/src/ui/blockSideMenuFactory.ts b/examples/vanilla/src/ui/blockSideMenuFactory.ts index 575bf0c33d..6505cc7522 100644 --- a/examples/vanilla/src/ui/blockSideMenuFactory.ts +++ b/examples/vanilla/src/ui/blockSideMenuFactory.ts @@ -29,8 +29,11 @@ export const blockSideMenuFactory: BlockSideMenuFactory = (props) => { return { element: container, - show: (params) => { - container.style.display = "block"; + render: (params, isHidden) => { + if (isHidden) { + container.style.display = "block"; + } + console.log("show blockmenu", params); container.style.top = params.blockBoundingBox.y + "px"; container.style.left = @@ -39,11 +42,5 @@ export const blockSideMenuFactory: BlockSideMenuFactory = (props) => { hide: () => { container.style.display = "none"; }, - update: (params) => { - console.log("update blockmenu", params); - container.style.top = params.blockBoundingBox.y + "px"; - container.style.left = - params.blockBoundingBox.x - container.offsetWidth + "px"; - }, }; }; diff --git a/examples/vanilla/src/ui/formattingToolbarFactory.ts b/examples/vanilla/src/ui/formattingToolbarFactory.ts index 5fca01d15a..ff856303d1 100644 --- a/examples/vanilla/src/ui/formattingToolbarFactory.ts +++ b/examples/vanilla/src/ui/formattingToolbarFactory.ts @@ -27,8 +27,11 @@ export const formattingToolbarFactory: FormattingToolbarFactory = (props) => { return { element: container, - show: (params) => { - container.style.display = "block"; + render: (params, isHidden) => { + if (isHidden) { + container.style.display = "block"; + } + boldBtn.text = params.boldIsActive ? "unset bold" : "set bold"; container.style.top = params.selectionBoundingBox.y + "px"; container.style.left = params.selectionBoundingBox.x + "px"; @@ -36,10 +39,5 @@ export const formattingToolbarFactory: FormattingToolbarFactory = (props) => { hide: () => { container.style.display = "none"; }, - update: (params) => { - boldBtn.text = params.boldIsActive ? "unset bold" : "set bold"; - container.style.top = params.selectionBoundingBox.y + "px"; - container.style.left = params.selectionBoundingBox.x + "px"; - }, }; }; diff --git a/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts index 4c02614713..32fed9593a 100644 --- a/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts +++ b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts @@ -33,23 +33,20 @@ export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = (props) => { return { element: container, - show: (params) => { - url = params.url; - text = params.text; + render: (params, isHidden) => { + if (isHidden) { + url = params.url; + text = params.text; - container.style.display = "block"; - console.log("show", params); + container.style.display = "block"; + } + console.log("show", params); container.style.top = params.boundingBox.y + "px"; container.style.left = params.boundingBox.x + "px"; }, hide: () => { container.style.display = "none"; }, - update: (params) => { - console.log("update", params); - container.style.top = params.boundingBox.y + "px"; - container.style.left = params.boundingBox.x + "px"; - }, }; }; diff --git a/examples/vanilla/src/ui/slashMenuFactory.ts b/examples/vanilla/src/ui/slashMenuFactory.ts index 842d5bebdf..9099d54fde 100644 --- a/examples/vanilla/src/ui/slashMenuFactory.ts +++ b/examples/vanilla/src/ui/slashMenuFactory.ts @@ -38,22 +38,19 @@ export const slashMenuFactory: SuggestionsMenuFactory = ( return { element: container, - show: (params) => { + render: (params, isHidden) => { updateItems(params.items, props.itemCallback, params.selectedItemIndex); - container.style.display = "block"; - console.log("show", params); + if (isHidden) { + container.style.display = "block"; + } + + console.log("show", params); container.style.top = params.queryStartBoundingBox.y + "px"; container.style.left = params.queryStartBoundingBox.x + "px"; }, hide: () => { container.style.display = "none"; }, - update: (params) => { - console.log("update", params); - updateItems(params.items, props.itemCallback, params.selectedItemIndex); - container.style.top = params.queryStartBoundingBox.y + "px"; - container.style.left = params.queryStartBoundingBox.x + "px"; - }, }; }; diff --git a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts index 334e15b461..7fc8d3ba4b 100644 --- a/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts +++ b/packages/core/src/extensions/DraggableBlocks/DraggableBlocksPlugin.ts @@ -300,9 +300,9 @@ export class BlockMenuView { // Shows or updates elements. if (!this.menuOpen) { this.menuOpen = true; - this.blockMenu.show(this.getDynamicParams()); + this.blockMenu.render(this.getDynamicParams(), true); } else { - this.blockMenu.update(this.getDynamicParams()); + this.blockMenu.render(this.getDynamicParams(), false); } }, true diff --git a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts index 2b5f3bd97e..61541d1142 100644 --- a/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts +++ b/packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts @@ -157,7 +157,7 @@ export class FormattingToolbarView { !this.preventShow && (shouldShow || this.preventHide) ) { - this.formattingToolbar.show(this.getDynamicParams()); + this.formattingToolbar.render(this.getDynamicParams(), true); this.toolbarIsOpen = true; // TODO: Is this necessary? Also for other menu plugins. @@ -175,7 +175,7 @@ export class FormattingToolbarView { !this.preventShow && (shouldShow || this.preventHide) ) { - this.formattingToolbar.update(this.getDynamicParams()); + this.formattingToolbar.render(this.getDynamicParams(), false); return; } diff --git a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts index 963e27dc72..f5425b6616 100644 --- a/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts +++ b/packages/core/src/extensions/HyperlinkToolbar/HyperlinkToolbarPlugin.ts @@ -150,7 +150,7 @@ class HyperlinkToolbarView { // Shows menu. if (!prevHyperlinkMark) { - this.hyperlinkToolbar.show(this.getDynamicParams()); + this.hyperlinkToolbar.render(this.getDynamicParams(), true); this.hyperlinkToolbar.element?.addEventListener( "mouseleave", @@ -165,7 +165,7 @@ class HyperlinkToolbarView { } // Updates menu. - this.hyperlinkToolbar.update(this.getDynamicParams()); + this.hyperlinkToolbar.render(this.getDynamicParams(), false); } // Hides menu. diff --git a/packages/core/src/shared/EditorElement.ts b/packages/core/src/shared/EditorElement.ts index ce68afe778..5971f9e302 100644 --- a/packages/core/src/shared/EditorElement.ts +++ b/packages/core/src/shared/EditorElement.ts @@ -1,8 +1,7 @@ export type EditorElement> = { element: HTMLElement | undefined; - show: (params: ElementDynamicParams) => void; + render: (params: ElementDynamicParams, isHidden: boolean) => void; hide: () => void; - update: (params: ElementDynamicParams) => void; }; export type ElementFactory< diff --git a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts index 92247d2a23..adef7aa8b5 100644 --- a/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts +++ b/packages/core/src/shared/plugins/suggestion/SuggestionPlugin.ts @@ -174,11 +174,11 @@ class SuggestionPluginView { } if (changed) { - this.suggestionsMenu.update(this.getDynamicParams()); + this.suggestionsMenu.render(this.getDynamicParams(), false); } if (started) { - this.suggestionsMenu.show(this.getDynamicParams()); + this.suggestionsMenu.render(this.getDynamicParams(), true); // Listener stops focus moving to the menu on click. this.suggestionsMenu.element!.addEventListener("mousedown", (event) => diff --git a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx index 62f507e341..f284511642 100644 --- a/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx +++ b/packages/react/src/BlockSideMenu/BlockSideMenuFactory.tsx @@ -15,15 +15,15 @@ export const ReactBlockSideMenuFactory: BlockSideMenuFactory = ( ): BlockSideMenu => { // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other menu factories do the same. - const menuRootElement = document.createElement("div"); - // menuRootElement.className = rootStyles.bnRoot; - let menuRoot: Root | undefined; + const rootElement = document.createElement("div"); + // rootElement.className = rootStyles.bnRoot; + let root: Root | undefined; - function getMenuComponent(dynamicParams: BlockSideMenuDynamicParams) { + function getComponent(dynamicParams: BlockSideMenuDynamicParams) { return ( } duration={0} getReferenceClientRect={() => dynamicParams.blockBoundingBox} @@ -39,20 +39,19 @@ export const ReactBlockSideMenuFactory: BlockSideMenuFactory = ( } return { - element: menuRootElement, - show: (dynamicParams: BlockSideMenuDynamicParams) => { - document.body.appendChild(menuRootElement); - menuRoot = createRoot(menuRootElement); + element: rootElement, + render: (dynamicParams: BlockSideMenuDynamicParams, isHidden: boolean) => { + if (isHidden) { + document.body.appendChild(rootElement); + root = createRoot(rootElement); + } - menuRoot.render(getMenuComponent(dynamicParams)); + root!.render(getComponent(dynamicParams)); }, hide: () => { - menuRoot!.unmount(); + root!.unmount(); - menuRootElement.remove(); - }, - update: (dynamicParams: BlockSideMenuDynamicParams) => { - menuRoot!.render(getMenuComponent(dynamicParams)); + rootElement.remove(); }, }; }; diff --git a/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx index 7df5a8b76d..14a57ef4b7 100644 --- a/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx +++ b/packages/react/src/FormattingToolbar/FormattingToolbarFactory.tsx @@ -16,15 +16,15 @@ export const ReactFormattingToolbarFactory: FormattingToolbarFactory = ( ): FormattingToolbar => { // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other menu factories do the same. - const menuRootElement = document.createElement("div"); - // menuRootElement.className = rootStyles.bnRoot; - let menuRoot: Root | undefined; + const rootElement = document.createElement("div"); + // rootElement.className = rootStyles.bnRoot; + let root: Root | undefined; - function getMenuComponent(dynamicParams: FormattingToolbarDynamicParams) { + function getComponent(dynamicParams: FormattingToolbarDynamicParams) { return ( } @@ -41,20 +41,22 @@ export const ReactFormattingToolbarFactory: FormattingToolbarFactory = ( } return { - element: menuRootElement, - show: (dynamicParams: FormattingToolbarDynamicParams) => { - document.body.appendChild(menuRootElement); - menuRoot = createRoot(menuRootElement); + element: rootElement, + render: ( + dynamicParams: FormattingToolbarDynamicParams, + isHidden: boolean + ) => { + if (isHidden) { + document.body.appendChild(rootElement); + root = createRoot(rootElement); + } - menuRoot.render(getMenuComponent(dynamicParams)); + root!.render(getComponent(dynamicParams)); }, hide: () => { - menuRoot!.unmount(); + root!.unmount(); - menuRootElement.remove(); - }, - update: (dynamicParams: FormattingToolbarDynamicParams) => { - menuRoot!.render(getMenuComponent(dynamicParams)); + rootElement.remove(); }, }; }; diff --git a/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx b/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx index 5e3319abae..c8f6c1e6ec 100644 --- a/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx +++ b/packages/react/src/HyperlinkToolbar/HyperlinkToolbarFactory.tsx @@ -41,19 +41,21 @@ export const ReactHyperlinkToolbarFactory: HyperlinkToolbarFactory = ( return { element: rootElement, - show: (dynamicParams: HyperlinkToolbarDynamicParams) => { - document.body.appendChild(rootElement); - root = createRoot(rootElement); + render: ( + dynamicParams: HyperlinkToolbarDynamicParams, + isHidden: boolean + ) => { + if (isHidden) { + document.body.appendChild(rootElement); + root = createRoot(rootElement); + } - root.render(getComponent(dynamicParams)); + root!.render(getComponent(dynamicParams)); }, hide: () => { root!.unmount(); rootElement.remove(); }, - update: (dynamicParams: HyperlinkToolbarDynamicParams) => { - root!.render(getComponent(dynamicParams)); - }, }; }; diff --git a/packages/react/src/SlashMenu/SlashMenuFactory.tsx b/packages/react/src/SlashMenu/SlashMenuFactory.tsx index ea9a14af84..add31771ec 100644 --- a/packages/react/src/SlashMenu/SlashMenuFactory.tsx +++ b/packages/react/src/SlashMenu/SlashMenuFactory.tsx @@ -17,17 +17,17 @@ export const ReactSlashMenuFactory: SuggestionsMenuFactory = ( ): SuggestionsMenu => { // We don't use the document body as a root as it would cause multiple React roots to be created on a single element // if other menu factories do the same. - const menuRootElement = document.createElement("div"); - // menuRootElement.className = rootStyles.bnRoot; - let menuRoot: Root | undefined; + const rootElement = document.createElement("div"); + // rootElement.className = rootStyles.bnRoot; + let root: Root | undefined; - function getMenuComponent( + function getComponent( dynamicParams: SuggestionsMenuDynamicParams ) { return ( } duration={0} getReferenceClientRect={() => dynamicParams.queryStartBoundingBox} @@ -42,20 +42,22 @@ export const ReactSlashMenuFactory: SuggestionsMenuFactory = ( } return { - element: menuRootElement as HTMLElement, - show: (dynamicParams: SuggestionsMenuDynamicParams) => { - document.body.appendChild(menuRootElement); - menuRoot = createRoot(menuRootElement); + element: rootElement as HTMLElement, + render: ( + dynamicParams: SuggestionsMenuDynamicParams, + isHidden: boolean + ) => { + if (isHidden) { + document.body.appendChild(rootElement); + root = createRoot(rootElement); + } - menuRoot.render(getMenuComponent(dynamicParams)); + root!.render(getComponent(dynamicParams)); }, hide: () => { - menuRoot!.unmount(); + root!.unmount(); - menuRootElement.remove(); - }, - update: (dynamicParams: SuggestionsMenuDynamicParams) => { - menuRoot!.render(getMenuComponent(dynamicParams)); + rootElement.remove(); }, }; }; From 721121710effba3f7f062f1970ddc726f46904b0 Mon Sep 17 00:00:00 2001 From: yousefed Date: Wed, 18 Jan 2023 15:27:24 +0100 Subject: [PATCH 55/55] rename props -> staticParams in vanilla example --- examples/vanilla/src/ui/blockSideMenuFactory.ts | 8 ++++---- examples/vanilla/src/ui/formattingToolbarFactory.ts | 8 +++++--- examples/vanilla/src/ui/hyperlinkToolbarFactory.ts | 8 +++++--- examples/vanilla/src/ui/slashMenuFactory.ts | 8 ++++++-- packages/core/src/shared/EditorElement.ts | 2 +- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/examples/vanilla/src/ui/blockSideMenuFactory.ts b/examples/vanilla/src/ui/blockSideMenuFactory.ts index 6505cc7522..5c71b94f46 100644 --- a/examples/vanilla/src/ui/blockSideMenuFactory.ts +++ b/examples/vanilla/src/ui/blockSideMenuFactory.ts @@ -5,14 +5,14 @@ import { createButton } from "./util"; * This menu is drawn next to a block, when it's hovered over * It renders a drag handle and + button to create a new block */ -export const blockSideMenuFactory: BlockSideMenuFactory = (props) => { +export const blockSideMenuFactory: BlockSideMenuFactory = (staticParams) => { const container = document.createElement("div"); container.style.background = "gray"; container.style.position = "absolute"; container.style.padding = "10px"; container.style.opacity = "0.8"; const addBtn = createButton("+", () => { - props.addBlock(); + staticParams.addBlock(); }); container.appendChild(addBtn); @@ -20,8 +20,8 @@ export const blockSideMenuFactory: BlockSideMenuFactory = (props) => { // TODO: render a submenu with a delete option that calls "props.deleteBlock" }); - dragBtn.addEventListener("dragstart", props.blockDragStart); - dragBtn.addEventListener("dragend", props.blockDragEnd); + dragBtn.addEventListener("dragstart", staticParams.blockDragStart); + dragBtn.addEventListener("dragend", staticParams.blockDragEnd); container.style.display = "none"; container.appendChild(dragBtn); diff --git a/examples/vanilla/src/ui/formattingToolbarFactory.ts b/examples/vanilla/src/ui/formattingToolbarFactory.ts index ff856303d1..b78e4fa94d 100644 --- a/examples/vanilla/src/ui/formattingToolbarFactory.ts +++ b/examples/vanilla/src/ui/formattingToolbarFactory.ts @@ -5,19 +5,21 @@ import { createButton } from "./util"; * This menu is drawn when a piece of text is selected. We can use it to change formatting options * such as bold, italic, indentation, etc. */ -export const formattingToolbarFactory: FormattingToolbarFactory = (props) => { +export const formattingToolbarFactory: FormattingToolbarFactory = ( + staticParams +) => { const container = document.createElement("div"); container.style.background = "gray"; container.style.position = "absolute"; container.style.padding = "10px"; container.style.opacity = "0.8"; const boldBtn = createButton("set bold", () => { - props.toggleBold(); + staticParams.toggleBold(); }); container.appendChild(boldBtn); const linkBtn = createButton("set link", () => { - props.setHyperlink("https://www.google.com"); + staticParams.setHyperlink("https://www.google.com"); }); container.appendChild(boldBtn); diff --git a/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts index 32fed9593a..bfa7d67984 100644 --- a/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts +++ b/examples/vanilla/src/ui/hyperlinkToolbarFactory.ts @@ -5,7 +5,9 @@ import { createButton } from "./util"; * This menu is drawn when the cursor is moved to a hyperlink (using the keyboard), * or when the mouse is hovering over a hyperlink */ -export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = (props) => { +export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = ( + staticParams +) => { const container = document.createElement("div"); container.style.background = "gray"; container.style.position = "absolute"; @@ -17,13 +19,13 @@ export const hyperlinkToolbarFactory: HyperlinkToolbarFactory = (props) => { const editBtn = createButton("edit", () => { const newUrl = prompt("new url") || url; - props.editHyperlink(newUrl, text); + staticParams.editHyperlink(newUrl, text); }); container.appendChild(editBtn); const removeBtn = createButton("remove", () => { - props.deleteHyperlink(); + staticParams.deleteHyperlink(); }); container.appendChild(editBtn); diff --git a/examples/vanilla/src/ui/slashMenuFactory.ts b/examples/vanilla/src/ui/slashMenuFactory.ts index 9099d54fde..6dca65bea4 100644 --- a/examples/vanilla/src/ui/slashMenuFactory.ts +++ b/examples/vanilla/src/ui/slashMenuFactory.ts @@ -6,7 +6,7 @@ import { createButton } from "./util"; * or when the mouse is hovering over a hyperlink */ export const slashMenuFactory: SuggestionsMenuFactory = ( - props + staticParams ) => { const container = document.createElement("div"); container.style.background = "gray"; @@ -39,7 +39,11 @@ export const slashMenuFactory: SuggestionsMenuFactory = ( return { element: container, render: (params, isHidden) => { - updateItems(params.items, props.itemCallback, params.selectedItemIndex); + updateItems( + params.items, + staticParams.itemCallback, + params.selectedItemIndex + ); if (isHidden) { container.style.display = "block"; diff --git a/packages/core/src/shared/EditorElement.ts b/packages/core/src/shared/EditorElement.ts index 5971f9e302..57071e2db9 100644 --- a/packages/core/src/shared/EditorElement.ts +++ b/packages/core/src/shared/EditorElement.ts @@ -7,4 +7,4 @@ export type EditorElement> = { export type ElementFactory< ElementStaticParams extends Record, ElementDynamicParams extends Record -> = (params: ElementStaticParams) => EditorElement; +> = (staticParams: ElementStaticParams) => EditorElement;