From 57a31985e8b97d90bef632f6e6f0897f99850fed Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Tue, 18 Jun 2024 01:00:19 +0500 Subject: [PATCH 01/41] shows the emojis, keyboard navigation yet to be setup --- package-lock.json | 102 ++++++++++++++++++ package.json | 5 + packages/core/src/blocks/Emoji.tsx | 24 +++++ packages/core/src/blocks/defaultBlocks.ts | 2 + .../SuggestionMenu/SuggestionMenuWrapper.tsx | 2 + .../components/SuggestionMenu/emojisMenu.jsx | 35 ++++++ .../react/src/editor/BlockNoteDefaultUI.tsx | 41 +++++++ 7 files changed, 211 insertions(+) create mode 100644 packages/core/src/blocks/Emoji.tsx create mode 100644 packages/react/src/components/SuggestionMenu/emojisMenu.jsx diff --git a/package-lock.json b/package-lock.json index fcf83232a4..25c1fb78c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,11 @@ "tests", "docs" ], + "dependencies": { + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", + "emoji-mart": "^5.6.0" + }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.5.0", "@typescript-eslint/parser": "^5.5.0", @@ -2595,6 +2600,20 @@ "node": ">=10.0.0" } }, + "node_modules/@emoji-mart/data": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", + "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==" + }, + "node_modules/@emoji-mart/react": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz", + "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==", + "peerDependencies": { + "emoji-mart": "^5.2", + "react": "^16.8 || ^17 || ^18" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -11545,11 +11564,48 @@ "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==" }, + "node_modules/emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==" + }, + "node_modules/emoji-picker-react": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.10.0.tgz", + "integrity": "sha512-EfvOsGbyweMNcJ1F99XUv+XPdfkpa2NRAYkhwdIeYS6DWeISu3kHWX+iwvFLUVAc533aWbsGpETbxwbhzsiMnw==", + "dependencies": { + "flairup": "0.0.39" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/emojibase": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-15.3.1.tgz", + "integrity": "sha512-GNsjHnG2J3Ktg684Fs/vZR/6XpOSkZPMAv85EHrr6br2RN2cJNwdS4am/3YSK3y+/gOv2kmoK3GGdahXdMxg2g==", + "funding": { + "type": "ko-fi", + "url": "https://ko-fi.com/milesjohnson" + } + }, + "node_modules/emojis-keywords": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/emojis-keywords/-/emojis-keywords-2.0.0.tgz", + "integrity": "sha512-arBNEGi5UnCOmG5U1qJMPBvG6lEAV0zimJsPHJKRpPxeiUzGuu41E/pkm2lzaxA9JHHGX+jy6hdfN8CXy9I0Ug==", + "engines": { + "node": ">= 0.10.0", + "npm": ">= 1.4.0" + } + }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -12801,6 +12857,11 @@ "micromatch": "^4.0.2" } }, + "node_modules/flairup": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-0.0.39.tgz", + "integrity": "sha512-UVPkzZmZeBWBx1+Ovo++kYKk9Wi32Jxt+c7HsxnEY80ExwFV54w+NyquFziqMLS0BnGVE43yGD4OvIwaAm/WiQ==" + }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -16425,6 +16486,11 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" + }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -16436,6 +16502,11 @@ "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -21684,6 +21755,27 @@ "react": "^18.3.1" } }, + "node_modules/react-emoji-render": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-2.0.1.tgz", + "integrity": "sha512-SKtsdwgEf2BFNiE9y4UBFZBWjkRcyWmhMprVly52+J77/zxThcfaQ3sCA0+2LtGJIRMpm4DGWSvyLg72fd1rXQ==", + "dependencies": { + "classnames": "^2.2.5", + "emoji-regex": "^8.0.0", + "lodash.flatten": "^4.4.0", + "prop-types": "^15.5.8", + "string-replace-to-array": "^1.0.1" + }, + "peerDependencies": { + "react": ">=0.14.0", + "react-dom": ">=0.14.0" + } + }, + "node_modules/react-emoji-render/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/react-github-btn": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz", @@ -23631,6 +23723,16 @@ "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", "dev": true }, + "node_modules/string-replace-to-array": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz", + "integrity": "sha512-QIdKvmfXbdFvblXAAz6IIjR7A+C6SU6m2A+e7fE/0EYDC5yfeWNMJQ193fPsW7nG+9q52dv/UjnVrDaNVZXpmQ==", + "dependencies": { + "invariant": "^2.2.1", + "lodash.flatten": "^4.2.0", + "lodash.isstring": "^4.0.1" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/package.json b/package.json index e7a4205f27..a0fce56da6 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,10 @@ "prepublishOnly": "npm run build && cp README.md packages/core/README.md && cp README.md packages/react/README.md", "postpublish": "rm -rf packages/core/README.md && rm -rf packages/react/README.md", "clean": "lerna run --stream clean" + }, + "dependencies": { + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", + "emoji-mart": "^5.6.0" } } diff --git a/packages/core/src/blocks/Emoji.tsx b/packages/core/src/blocks/Emoji.tsx new file mode 100644 index 0000000000..83c0db58fb --- /dev/null +++ b/packages/core/src/blocks/Emoji.tsx @@ -0,0 +1,24 @@ +import { createReactInlineContentSpec } from "@blocknote/react"; + +// The Mention inline content. +export const Emoji = createReactInlineContentSpec( + { + type: "emoji", + propSchema: { + emoji: { + default: "Unknown", + }, + }, + content: "none", + }, + { + render: (props) => { + return( + + + {props.inlineContent.props.emoji} + + )}, + } +); + \ No newline at end of file diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index e2b9b9e8dc..5faa44ef3a 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -30,6 +30,7 @@ import { FileBlock } from "./FileBlockContent/FileBlockContent"; import { ImageBlock } from "./ImageBlockContent/ImageBlockContent"; import { VideoBlock } from "./VideoBlockContent/VideoBlockContent"; import { AudioBlock } from "./AudioBlockContent/AudioBlockContent"; +import { Emoji } from "./Emoji" export const defaultBlockSpecs = { paragraph: Paragraph, @@ -71,6 +72,7 @@ export type DefaultStyleSchema = _DefaultStyleSchema; export const defaultInlineContentSpecs = { text: { config: "text", implementation: {} as any }, link: { config: "link", implementation: {} as any }, + emoji: Emoji } satisfies InlineContentSpecs; export const defaultInlineContentSchema = getInlineContentSchemaFromSpecs( diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index d0e2d72a88..c8d091607a 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -93,8 +93,10 @@ export function SuggestionMenuWrapper(props: { return ( ); diff --git a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx new file mode 100644 index 0000000000..003f4cce30 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx @@ -0,0 +1,35 @@ +import {insertOrUpdateBlock} from '@blocknote/core' +import data from '@emoji-mart/data' +import Picker from '@emoji-mart/react' + import { init, SearchIndex } from 'emoji-mart' +import { useEffect, useState } from 'react' + +export default function EmojiMenu({items, clearQuery, editor}){ + + useEffect(()=>{ + console.log('YAHOO') + console.log(items) + }, [items]) + const emojiInsert = (emojiToInsert) => { + + clearQuery() + editor.insertInlineContent([ + { + type: "emoji", + props: { + emoji : emojiToInsert + }, + }, + " ", // add a space after the emoji + ]); + } + + return (
+{ + items.map(item=>( +

(emojiInsert(item))}>{item}

+ )) +} +
+ ) +} \ No newline at end of file diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 8df2624cb3..8d0ad20c12 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -5,6 +5,8 @@ import { SideMenuController } from "../components/SideMenu/SideMenuController"; import { SuggestionMenuController } from "../components/SuggestionMenu/SuggestionMenuController"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; +import EmojiMenu from '../components/SuggestionMenu/emojisMenu.jsx' +import { Data, SearchIndex, init } from "emoji-mart"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -15,8 +17,40 @@ export type BlockNoteDefaultUIProps = { tableHandles?: boolean; }; +init({ Data }) + +async function search(value) { + const response = await fetch( + 'https://cdn.jsdelivr.net/npm/@emoji-mart/data', + ) + let allemojis = await response.json() + let emojisToShow = [] + + const emojis = await SearchIndex.search(value); + console.log(emojis) + const results = (emojis || []).map((emoji) => { + return emoji.id; + }) + Object.values(allemojis.emojis).forEach(({id, skins})=>{ + //add all emojis if not yet searched for an emoji + results.includes(id) || !emojis ? emojisToShow.push(skins[0].native) : null + }); + console.log(emojisToShow) + return emojisToShow; + +} + + export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { const editor = useBlockNoteEditor(); + async function emojiChangeHandler(query){ + + //now do the emojis + console.log('IM HERE') + //return a promise + return search(query); + // setEmojisToHide(results) +} if (!editor) { throw new Error( @@ -29,7 +63,14 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { {props.formattingToolbar !== false && } {props.linkToolbar !== false && } {props.slashMenu !== false && ( + <> + + )} {props.sideMenu !== false && } {editor.filePanel && props.filePanel !== false && } From 489019899b860d2761277d1992adc368db9576b6 Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Tue, 18 Jun 2024 01:37:50 +0500 Subject: [PATCH 02/41] fixed lint and styled emoji picker --- package-lock.json | 78 ------------------- .../components/SuggestionMenu/emojisMenu.jsx | 4 +- .../react/src/editor/BlockNoteDefaultUI.tsx | 13 ++-- 3 files changed, 8 insertions(+), 87 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25c1fb78c5..906da0b5db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11569,43 +11569,11 @@ "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==" }, - "node_modules/emoji-picker-react": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.10.0.tgz", - "integrity": "sha512-EfvOsGbyweMNcJ1F99XUv+XPdfkpa2NRAYkhwdIeYS6DWeISu3kHWX+iwvFLUVAc533aWbsGpETbxwbhzsiMnw==", - "dependencies": { - "flairup": "0.0.39" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16" - } - }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, - "node_modules/emojibase": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/emojibase/-/emojibase-15.3.1.tgz", - "integrity": "sha512-GNsjHnG2J3Ktg684Fs/vZR/6XpOSkZPMAv85EHrr6br2RN2cJNwdS4am/3YSK3y+/gOv2kmoK3GGdahXdMxg2g==", - "funding": { - "type": "ko-fi", - "url": "https://ko-fi.com/milesjohnson" - } - }, - "node_modules/emojis-keywords": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/emojis-keywords/-/emojis-keywords-2.0.0.tgz", - "integrity": "sha512-arBNEGi5UnCOmG5U1qJMPBvG6lEAV0zimJsPHJKRpPxeiUzGuu41E/pkm2lzaxA9JHHGX+jy6hdfN8CXy9I0Ug==", - "engines": { - "node": ">= 0.10.0", - "npm": ">= 1.4.0" - } - }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -12857,11 +12825,6 @@ "micromatch": "^4.0.2" } }, - "node_modules/flairup": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/flairup/-/flairup-0.0.39.tgz", - "integrity": "sha512-UVPkzZmZeBWBx1+Ovo++kYKk9Wi32Jxt+c7HsxnEY80ExwFV54w+NyquFziqMLS0BnGVE43yGD4OvIwaAm/WiQ==" - }, "node_modules/flat": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", @@ -16486,11 +16449,6 @@ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==" - }, "node_modules/lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", @@ -16502,11 +16460,6 @@ "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -21755,27 +21708,6 @@ "react": "^18.3.1" } }, - "node_modules/react-emoji-render": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/react-emoji-render/-/react-emoji-render-2.0.1.tgz", - "integrity": "sha512-SKtsdwgEf2BFNiE9y4UBFZBWjkRcyWmhMprVly52+J77/zxThcfaQ3sCA0+2LtGJIRMpm4DGWSvyLg72fd1rXQ==", - "dependencies": { - "classnames": "^2.2.5", - "emoji-regex": "^8.0.0", - "lodash.flatten": "^4.4.0", - "prop-types": "^15.5.8", - "string-replace-to-array": "^1.0.1" - }, - "peerDependencies": { - "react": ">=0.14.0", - "react-dom": ">=0.14.0" - } - }, - "node_modules/react-emoji-render/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "node_modules/react-github-btn": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/react-github-btn/-/react-github-btn-1.4.0.tgz", @@ -23723,16 +23655,6 @@ "integrity": "sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==", "dev": true }, - "node_modules/string-replace-to-array": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string-replace-to-array/-/string-replace-to-array-1.0.3.tgz", - "integrity": "sha512-QIdKvmfXbdFvblXAAz6IIjR7A+C6SU6m2A+e7fE/0EYDC5yfeWNMJQ193fPsW7nG+9q52dv/UjnVrDaNVZXpmQ==", - "dependencies": { - "invariant": "^2.2.1", - "lodash.flatten": "^4.2.0", - "lodash.isstring": "^4.0.1" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", diff --git a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx index 003f4cce30..36ae9a0783 100644 --- a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx +++ b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx @@ -24,10 +24,10 @@ export default function EmojiMenu({items, clearQuery, editor}){ ]); } - return (
+ return (
{ items.map(item=>( -

(emojiInsert(item))}>{item}

+

(emojiInsert(item))}>{item}

)) }
diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 8d0ad20c12..6cc94ff618 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -23,19 +23,19 @@ async function search(value) { const response = await fetch( 'https://cdn.jsdelivr.net/npm/@emoji-mart/data', ) - let allemojis = await response.json() - let emojisToShow = [] + const allemojis = await response.json() + const emojisToShow = [] const emojis = await SearchIndex.search(value); - console.log(emojis) const results = (emojis || []).map((emoji) => { return emoji.id; }) Object.values(allemojis.emojis).forEach(({id, skins})=>{ //add all emojis if not yet searched for an emoji - results.includes(id) || !emojis ? emojisToShow.push(skins[0].native) : null + if( results.includes(id) || !emojis){ + emojisToShow.push(skins[0].native) + } }); - console.log(emojisToShow) return emojisToShow; } @@ -43,10 +43,9 @@ async function search(value) { export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { const editor = useBlockNoteEditor(); - async function emojiChangeHandler(query){ + async function emojiChangeHandler(query: string){ //now do the emojis - console.log('IM HERE') //return a promise return search(query); // setEmojisToHide(results) From 9ace67d12e484598b32b5feafc478fd4673e09ed Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Tue, 18 Jun 2024 02:15:42 +0500 Subject: [PATCH 03/41] added support for keyboard shortcuts, and cleanup --- .../SuggestionMenuController.tsx | 3 +- .../SuggestionMenu/SuggestionMenuWrapper.tsx | 18 +++++++++++- .../components/SuggestionMenu/emojisMenu.jsx | 29 +++---------------- .../useSuggestionMenuKeyboardNavigation.ts | 21 ++++++++++++-- .../react/src/editor/BlockNoteDefaultUI.tsx | 1 + 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 6a6feafd54..a77094cefc 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -55,7 +55,7 @@ export function SuggestionMenuController< const { triggerCharacter, suggestionMenuComponent } = props; - const { onItemClick, getItems } = props; + const { onItemClick, isEmoji, getItems } = props; const onItemClickOrDefault = useMemo(() => { return ( @@ -128,6 +128,7 @@ export function SuggestionMenuController< query={state.query} closeMenu={callbacks.closeMenu} clearQuery={callbacks.clearQuery} + isEmoji={isEmoji} getItems={getItemsOrDefault} suggestionMenuComponent={suggestionMenuComponent || SuggestionMenu} onItemClick={onItemClickOrDefault} diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index c8d091607a..1ec71de237 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -24,6 +24,19 @@ export function SuggestionMenuWrapper(props: { StyleSchema >(); + const emojiInsert = (emojiToInsert) => { + clearQuery() + editor.insertInlineContent([ + { + type: "emoji", + props: { + emoji : emojiToInsert + }, + }, + " ", // add a space after the emoji + ]); + } + const { getItems, suggestionMenuComponent, @@ -31,6 +44,7 @@ export function SuggestionMenuWrapper(props: { clearQuery, closeMenu, onItemClick, + isEmoji } = props; const onItemClickCloseMenu = useCallback( @@ -53,7 +67,8 @@ export function SuggestionMenuWrapper(props: { editor, query, items, - onItemClickCloseMenu + isEmoji ? emojiInsert : onItemClickCloseMenu, + isEmoji ); // set basic aria attributes when the menu is open @@ -94,6 +109,7 @@ export function SuggestionMenuWrapper(props: { { - console.log('YAHOO') - console.log(items) - }, [items]) - const emojiInsert = (emojiToInsert) => { - - clearQuery() - editor.insertInlineContent([ - { - type: "emoji", - props: { - emoji : emojiToInsert - }, - }, - " ", // add a space after the emoji - ]); - } +export default function EmojiMenu({items, emojiInsert, selectedIndex}){ return (
{ - items.map(item=>( -

(emojiInsert(item))}>{item}

+ items.map((item, index)=>( +

(emojiInsert(item))}>{item}

)) }
diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index 0306914c5e..d7cf017786 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -7,7 +7,8 @@ export function useSuggestionMenuKeyboardNavigation( editor: BlockNoteEditor, query: string, items: Item[], - onItemClick?: (item: Item) => void + onItemClick?: (item: Item) => void, + isEmojiMenu ) { const [selectedIndex, setSelectedIndex] = useState(0); @@ -17,7 +18,7 @@ export function useSuggestionMenuKeyboardNavigation( event.preventDefault(); if (items.length) { - setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); + isEmojiMenu ? setSelectedIndex(selectedIndex - 10 >= 0 ? selectedIndex - 10 : 0) : setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); } return true; @@ -27,12 +28,26 @@ export function useSuggestionMenuKeyboardNavigation( event.preventDefault(); if (items.length) { - setSelectedIndex((selectedIndex + 1) % items!.length); + isEmojiMenu ? setSelectedIndex(selectedIndex + 10 < items!.length ? selectedIndex + 10 : items!.length -1) : setSelectedIndex((selectedIndex + 1) % items!.length); } return true; } + if(event.key === 'ArrowRight' && isEmojiMenu){ + event.preventDefault() + if(items.length){ + setSelectedIndex((selectedIndex + 1 + items!.length) % items!.length); + } + } + + if(event.key === 'ArrowLeft' && isEmojiMenu){ + event.preventDefault() + if(items.length){ + setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); + } + } + if (event.key === "Enter") { event.preventDefault(); diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 6cc94ff618..a6ce78caa2 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -65,6 +65,7 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { <> Date: Tue, 18 Jun 2024 02:19:57 +0500 Subject: [PATCH 04/41] add more reasonable color to emojiMenu --- packages/react/src/components/SuggestionMenu/emojisMenu.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx index 396e183fcf..19217b7f5e 100644 --- a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx +++ b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx @@ -3,10 +3,10 @@ import { useEffect, useState } from 'react' export default function EmojiMenu({items, emojiInsert, selectedIndex}){ - return (
+ return (
{ items.map((item, index)=>( -

(emojiInsert(item))}>{item}

+

(emojiInsert(item))}>{item}

)) }
From 5795d62c631c6b5885ad7345b71cd01a0bc24a11 Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Tue, 18 Jun 2024 18:32:56 +0500 Subject: [PATCH 05/41] added emoji option to slashmenu --- examples/01-basic/01-minimal/App.tsx | 3 +- packages/core/src/blocks/Emoji.tsx | 2 +- packages/core/src/blocks/EmojiSlash.tsx | 40 +++++++++++++++++++ packages/core/src/blocks/defaultBlocks.ts | 4 +- .../SuggestionMenuController.tsx | 27 ++++++++++++- 5 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/blocks/EmojiSlash.tsx diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index c545b7b4dd..c3a40dc15d 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -4,8 +4,7 @@ import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; export default function App() { - // Creates a new editor instance. - const editor = useCreateBlockNote(); +const editor = useCreateBlockNote() // Renders the editor instance using a React component. return ; diff --git a/packages/core/src/blocks/Emoji.tsx b/packages/core/src/blocks/Emoji.tsx index 83c0db58fb..f2c0645d53 100644 --- a/packages/core/src/blocks/Emoji.tsx +++ b/packages/core/src/blocks/Emoji.tsx @@ -1,6 +1,6 @@ import { createReactInlineContentSpec } from "@blocknote/react"; -// The Mention inline content. + export const Emoji = createReactInlineContentSpec( { type: "emoji", diff --git a/packages/core/src/blocks/EmojiSlash.tsx b/packages/core/src/blocks/EmojiSlash.tsx new file mode 100644 index 0000000000..1f71878988 --- /dev/null +++ b/packages/core/src/blocks/EmojiSlash.tsx @@ -0,0 +1,40 @@ +import { createReactInlineContentSpec, createReactBlockSpec } from "@blocknote/react"; + +// The Mention inline content. +export const EmojiSlash = createReactInlineContentSpec( + { + type: "emojiSlash", + propSchema: { + editor: { + default: 'none' + } + }, + content: "inline", + }, + { + render: (props) => { +// Create a new KeyboardEvent +// const keyboardEvent = new KeyboardEvent('keydown', { +// key: ':', // key identifier (DOMString), optional +// code: 'Colon', // key code identifier (DOMString), optional +// keyCode: 186, // key code value (unsigned long), optional +// which: 186, // legacy keyCode, optional +// charCode: 0, // character code value (unsigned long), optional +// bubbles: false, // bubbles flag (boolean), optional +// cancelable: false, // cancelable flag (boolean), optional +// composed: false, // composed flag (boolean), optional +// ctrlKey: false, // control key flag (boolean), optional +// altKey: false, // alt key flag (boolean), optional +// shiftKey: false, // shift key flag (boolean), optional +// metaKey: false // meta key flag (boolean), optional +// }); + +// Dispatch the event on the document +// props.inlineContent.props.editor.domElement.dispatchEvent(keyboardEvent); + + return( + : + + )}, + } +); \ No newline at end of file diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 5faa44ef3a..228fb7f71e 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -31,6 +31,7 @@ import { ImageBlock } from "./ImageBlockContent/ImageBlockContent"; import { VideoBlock } from "./VideoBlockContent/VideoBlockContent"; import { AudioBlock } from "./AudioBlockContent/AudioBlockContent"; import { Emoji } from "./Emoji" +import { EmojiSlash } from "./EmojiSlash"; export const defaultBlockSpecs = { paragraph: Paragraph, @@ -72,7 +73,8 @@ export type DefaultStyleSchema = _DefaultStyleSchema; export const defaultInlineContentSpecs = { text: { config: "text", implementation: {} as any }, link: { config: "link", implementation: {} as any }, - emoji: Emoji + emoji: Emoji, + emojiSlash: EmojiSlash } satisfies InlineContentSpecs; export const defaultInlineContentSchema = getInlineContentSchemaFromSpecs( diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index a77094cefc..6858186e90 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -15,6 +15,7 @@ import { SuggestionMenu } from "./SuggestionMenu"; import { SuggestionMenuWrapper } from "./SuggestionMenuWrapper"; import { getDefaultReactSlashMenuItems } from "./getDefaultReactSlashMenuItems"; import { DefaultReactSuggestionItem, SuggestionMenuProps } from "./types"; +import { MdEmojiEmotions } from "react-icons/md"; type ArrayElement = A extends readonly (infer T)[] ? T : never; @@ -66,12 +67,36 @@ export function SuggestionMenuController< ); }, [editor, onItemClick]); + const insertSlashMenuOption = (editor) => ({ + title: "Emoji", + onItemClick: () => { + console.log('called 1'); + editor.insertInlineContent( + + [ { + type: 'emojiSlash', + props: { + editor + } + }] + + ) + }, + aliases: [ + "emoji", + "emote", + "face", + ], + group: "Other", + icon: , + }); + const getItemsOrDefault = useMemo(() => { return ( getItems || ((async (query: string) => filterSuggestionItems( - getDefaultReactSlashMenuItems(editor), + [...getDefaultReactSlashMenuItems(editor), insertSlashMenuOption(editor)], query )) as any as typeof getItems) ); From 02318b8bc8ac6444b4d16aa1492948ad38f45bd2 Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Tue, 18 Jun 2024 19:40:49 +0500 Subject: [PATCH 06/41] optimized emojipicker for slower network --- .../react/src/editor/BlockNoteDefaultUI.tsx | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index a6ce78caa2..b97585ba1e 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -7,6 +7,7 @@ import { TableHandlesController } from "../components/TableHandles/TableHandlesC import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; import EmojiMenu from '../components/SuggestionMenu/emojisMenu.jsx' import { Data, SearchIndex, init } from "emoji-mart"; +import { useEffect } from "react"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -17,31 +18,40 @@ export type BlockNoteDefaultUIProps = { tableHandles?: boolean; }; + + + +export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { + + + +let allemojis = []; init({ Data }) async function search(value) { - const response = await fetch( - 'https://cdn.jsdelivr.net/npm/@emoji-mart/data', - ) - const allemojis = await response.json() + if(value == ''){ + console.log(Data) + return Object.values(Data.emojis).map(emoji=>( + emoji.skins[0].native + )) + } const emojisToShow = [] + // const emojis = []; + //begin the search + Object.values(Data.emojis).forEach((emoji)=>{ + for(let a = 0; a < emoji.keywords.length; a++){ + let keyword = emoji.keywords[a]; + if(keyword.includes(value)){ + emojisToShow.push(emoji.skins[0].native); + break; + } + } - const emojis = await SearchIndex.search(value); - const results = (emojis || []).map((emoji) => { - return emoji.id; }) - Object.values(allemojis.emojis).forEach(({id, skins})=>{ - //add all emojis if not yet searched for an emoji - if( results.includes(id) || !emojis){ - emojisToShow.push(skins[0].native) - } - }); return emojisToShow; } - -export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { const editor = useBlockNoteEditor(); async function emojiChangeHandler(query: string){ From 4da8fd71cb184f993343dea949c01c51b1a7cdcd Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 19 Jun 2024 18:51:43 +0500 Subject: [PATCH 07/41] finally made the emoji menu accessible through the slash menu --- packages/core/src/blocks/EmojiSlash.tsx | 40 ------------------- packages/core/src/blocks/defaultBlocks.ts | 4 +- .../SuggestionMenu/SuggestionPlugin.ts | 28 ++++++++++++- .../SuggestionMenuController.tsx | 27 ++++++++----- .../react/src/editor/BlockNoteDefaultUI.tsx | 5 +-- 5 files changed, 45 insertions(+), 59 deletions(-) delete mode 100644 packages/core/src/blocks/EmojiSlash.tsx diff --git a/packages/core/src/blocks/EmojiSlash.tsx b/packages/core/src/blocks/EmojiSlash.tsx deleted file mode 100644 index 1f71878988..0000000000 --- a/packages/core/src/blocks/EmojiSlash.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createReactInlineContentSpec, createReactBlockSpec } from "@blocknote/react"; - -// The Mention inline content. -export const EmojiSlash = createReactInlineContentSpec( - { - type: "emojiSlash", - propSchema: { - editor: { - default: 'none' - } - }, - content: "inline", - }, - { - render: (props) => { -// Create a new KeyboardEvent -// const keyboardEvent = new KeyboardEvent('keydown', { -// key: ':', // key identifier (DOMString), optional -// code: 'Colon', // key code identifier (DOMString), optional -// keyCode: 186, // key code value (unsigned long), optional -// which: 186, // legacy keyCode, optional -// charCode: 0, // character code value (unsigned long), optional -// bubbles: false, // bubbles flag (boolean), optional -// cancelable: false, // cancelable flag (boolean), optional -// composed: false, // composed flag (boolean), optional -// ctrlKey: false, // control key flag (boolean), optional -// altKey: false, // alt key flag (boolean), optional -// shiftKey: false, // shift key flag (boolean), optional -// metaKey: false // meta key flag (boolean), optional -// }); - -// Dispatch the event on the document -// props.inlineContent.props.editor.domElement.dispatchEvent(keyboardEvent); - - return( - : - - )}, - } -); \ No newline at end of file diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 228fb7f71e..f293306616 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -30,8 +30,7 @@ import { FileBlock } from "./FileBlockContent/FileBlockContent"; import { ImageBlock } from "./ImageBlockContent/ImageBlockContent"; import { VideoBlock } from "./VideoBlockContent/VideoBlockContent"; import { AudioBlock } from "./AudioBlockContent/AudioBlockContent"; -import { Emoji } from "./Emoji" -import { EmojiSlash } from "./EmojiSlash"; +import { Emoji } from "./Emoji"; export const defaultBlockSpecs = { paragraph: Paragraph, @@ -74,7 +73,6 @@ export const defaultInlineContentSpecs = { text: { config: "text", implementation: {} as any }, link: { config: "link", implementation: {} as any }, emoji: Emoji, - emojiSlash: EmojiSlash } satisfies InlineContentSpecs; export const defaultInlineContentSchema = getInlineContentSchemaFromSpecs( diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index a557b165d3..622092872f 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -27,7 +27,7 @@ class SuggestionMenuView< private readonly editor: BlockNoteEditor, emitUpdate: (menuName: string, state: SuggestionMenuState) => void ) { - this.pluginState = undefined; + this.pluginState = undefined; this.emitUpdate = (menuName: string) => { if (!this.state) { @@ -248,6 +248,30 @@ export class SuggestionMenuProseMirrorPlugin< }, props: { + handleKeyDown(view, event) { + console.log('KEY DOWN ', view, event) + + if (event.key == ':') { + const suggestionPluginState: SuggestionPluginState = ( + this as Plugin + ).getState(view.state); + console.log('TEXT INPUT HANDLER CALLED WITH TRIGGER ', ':') + console.log(suggestionMenuPluginKey) + view.dispatch( + view.state.tr + .insertText(':') + .scrollIntoView() + .setMeta(suggestionMenuPluginKey, { + triggerCharacter: ':', + alreadyShownMenu: true + }) + ); + + return true; + } + return; + }, + handleTextInput(view, _from, _to, text) { const suggestionPluginState: SuggestionPluginState = ( this as Plugin @@ -303,7 +327,7 @@ export class SuggestionMenuProseMirrorPlugin< return DecorationSet.create(state.doc, [ Decoration.inline( suggestionPluginState.queryStartPos! - - suggestionPluginState.triggerCharacter!.length, + suggestionPluginState.triggerCharacter!.length, suggestionPluginState.queryStartPos!, { nodeName: "span", diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 6858186e90..803b530cfc 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -69,18 +69,23 @@ export function SuggestionMenuController< const insertSlashMenuOption = (editor) => ({ title: "Emoji", + //we are simulating a : keydown event to trigger the emoji menu onItemClick: () => { - console.log('called 1'); - editor.insertInlineContent( - - [ { - type: 'emojiSlash', - props: { - editor - } - }] - - ) + const keyboardEvent = new KeyboardEvent('keydown', { + key: ':', // key identifier (DOMString), optional + code: 'Colon', // key code identifier (DOMString), optional + keyCode: 186, // key code value (unsigned long), optional + which: 186, // legacy keyCode, optional + charCode: 0, // character code value (unsigned long), optional + bubbles: false, // bubbles flag (boolean), optional + cancelable: false, // cancelable flag (boolean), optional + composed: false, // composed flag (boolean), optional + ctrlKey: false, // control key flag (boolean), optional + altKey: false, // alt key flag (boolean), optional + shiftKey: false, // shift key flag (boolean), optional + metaKey: false // meta key flag (boolean), optional + }); + editor.domElement.dispatchEvent(keyboardEvent); }, aliases: [ "emoji", diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index b97585ba1e..6668143a0c 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -6,8 +6,7 @@ import { SuggestionMenuController } from "../components/SuggestionMenu/Suggestio import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; import EmojiMenu from '../components/SuggestionMenu/emojisMenu.jsx' -import { Data, SearchIndex, init } from "emoji-mart"; -import { useEffect } from "react"; +import { Data, init } from "emoji-mart"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -30,7 +29,7 @@ init({ Data }) async function search(value) { if(value == ''){ - console.log(Data) + return Object.values(Data.emojis).map(emoji=>( emoji.skins[0].native )) From 6de43abd4279a4892d0e8a2aa82389684d8c79cf Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 19 Jun 2024 20:07:26 +0500 Subject: [PATCH 08/41] cleanup and add comments --- packages/core/src/blocks/Emoji.tsx | 1 + packages/core/src/blocks/defaultBlocks.ts | 1 + .../SuggestionMenu/SuggestionPlugin.ts | 20 +++++++++---------- .../SuggestionMenuController.tsx | 1 + .../SuggestionMenu/SuggestionMenuWrapper.tsx | 4 ++++ .../components/SuggestionMenu/emojisMenu.jsx | 18 +++++++++-------- .../react/src/editor/BlockNoteDefaultUI.tsx | 15 +++++++------- 7 files changed, 35 insertions(+), 25 deletions(-) diff --git a/packages/core/src/blocks/Emoji.tsx b/packages/core/src/blocks/Emoji.tsx index f2c0645d53..6610b49109 100644 --- a/packages/core/src/blocks/Emoji.tsx +++ b/packages/core/src/blocks/Emoji.tsx @@ -2,6 +2,7 @@ import { createReactInlineContentSpec } from "@blocknote/react"; export const Emoji = createReactInlineContentSpec( + //STEP 4: this component recieves an emoji, and insets it in the line { type: "emoji", propSchema: { diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index f293306616..6d02e76091 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -72,6 +72,7 @@ export type DefaultStyleSchema = _DefaultStyleSchema; export const defaultInlineContentSpecs = { text: { config: "text", implementation: {} as any }, link: { config: "link", implementation: {} as any }, + //for rendering emojis inline emoji: Emoji, } satisfies InlineContentSpecs; diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 622092872f..f80385d916 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -27,7 +27,7 @@ class SuggestionMenuView< private readonly editor: BlockNoteEditor, emitUpdate: (menuName: string, state: SuggestionMenuState) => void ) { - this.pluginState = undefined; + this.pluginState = undefined; this.emitUpdate = (menuName: string) => { if (!this.state) { @@ -129,12 +129,12 @@ class SuggestionMenuView< type SuggestionPluginState = | { - triggerCharacter: string; - fromUserInput: boolean; - queryStartPos: number; - query: string; - decorationId: string; - } + triggerCharacter: string; + fromUserInput: boolean; + queryStartPos: number; + query: string; + decorationId: string; + } | undefined; export const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin"); @@ -248,15 +248,14 @@ export class SuggestionMenuProseMirrorPlugin< }, props: { + //STEP 6: the final thing is to listen to the simulated keydown event generated by the onClick of emoji option in slash menu handleKeyDown(view, event) { - console.log('KEY DOWN ', view, event) if (event.key == ':') { + //only proceed if it was the trigger character (:) const suggestionPluginState: SuggestionPluginState = ( this as Plugin ).getState(view.state); - console.log('TEXT INPUT HANDLER CALLED WITH TRIGGER ', ':') - console.log(suggestionMenuPluginKey) view.dispatch( view.state.tr .insertText(':') @@ -264,6 +263,7 @@ export class SuggestionMenuProseMirrorPlugin< .setMeta(suggestionMenuPluginKey, { triggerCharacter: ':', alreadyShownMenu: true + //and this finally opens the emojiMenu thru slashmenu option }) ); diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 803b530cfc..c08b376d87 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -101,6 +101,7 @@ export function SuggestionMenuController< getItems || ((async (query: string) => filterSuggestionItems( + //STEP 6: SHOW THE EMOJI OPTION IN THE SLASH MENU [...getDefaultReactSlashMenuItems(editor), insertSlashMenuOption(editor)], query )) as any as typeof getItems) diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index 1ec71de237..9240d91768 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -25,11 +25,15 @@ export function SuggestionMenuWrapper(props: { >(); const emojiInsert = (emojiToInsert) => { + //STEP 3: handle onclick, this function is called whenever an emoji is clicked or enter key is pressed on an emoji + clearQuery() editor.insertInlineContent([ { + //call the inlinecontent of type of emoji declared in defaultBlocks.ts type: "emoji", props: { + //pass the emoji as a prop so it can be inserted in the text emoji : emojiToInsert }, }, diff --git a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx index 19217b7f5e..2e5d590a57 100644 --- a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx +++ b/packages/react/src/components/SuggestionMenu/emojisMenu.jsx @@ -1,14 +1,16 @@ -import {useSuggestionMenuKeyboardNavigation} from './hooks/useSuggestionMenuKeyboardNavigation' -import { useEffect, useState } from 'react' + export default function EmojiMenu({items, emojiInsert, selectedIndex}){ + //this is the component which renders emoji picker with the emojis searched. + - return (
-{ - items.map((item, index)=>( -

(emojiInsert(item))}>{item}

- )) -} + return ( +
+ { + items.map((item, index)=>( +

(emojiInsert(item))}>{item}

+ )) + }
) } \ No newline at end of file diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 6668143a0c..01bd9dc35b 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -25,39 +25,39 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { let allemojis = []; +//this makes sure emjois aren't fetched at every render init({ Data }) async function search(value) { if(value == ''){ - + //Don't do unnecessary linear search if no search query return Object.values(Data.emojis).map(emoji=>( emoji.skins[0].native )) } const emojisToShow = [] - // const emojis = []; - //begin the search + //begin the linear search Object.values(Data.emojis).forEach((emoji)=>{ + //check for every keyword, until a keyword contains the query for(let a = 0; a < emoji.keywords.length; a++){ let keyword = emoji.keywords[a]; if(keyword.includes(value)){ emojisToShow.push(emoji.skins[0].native); + //dont go further if emoji satisfies query break; } } }) + //return the emojis return emojisToShow; } const editor = useBlockNoteEditor(); async function emojiChangeHandler(query: string){ - - //now do the emojis - //return a promise + //STEP 2: call the search function, which returns an array of emojis as items for the component return search(query); - // setEmojisToHide(results) } if (!editor) { @@ -73,6 +73,7 @@ async function search(value) { {props.slashMenu !== false && ( <> + {/*STEP 1: the suggestion menu for the emojis, with custom component and getItems */} Date: Thu, 20 Jun 2024 07:13:05 +0500 Subject: [PATCH 09/41] add the appropriate typescript code, removed ts errors --- packages/core/src/blocks/Emoji.tsx | 19 ++++++++++--------- .../SuggestionMenu/SuggestionPlugin.ts | 3 --- .../SuggestionMenuController.tsx | 7 ++++--- .../SuggestionMenu/SuggestionMenuWrapper.tsx | 11 +++++------ .../{emojisMenu.jsx => emojisMenu.tsx} | 10 +++++++--- .../useSuggestionMenuKeyboardNavigation.ts | 6 +++--- .../src/components/SuggestionMenu/types.tsx | 1 + .../react/src/editor/BlockNoteDefaultUI.tsx | 11 +++++------ 8 files changed, 35 insertions(+), 33 deletions(-) rename packages/react/src/components/SuggestionMenu/{emojisMenu.jsx => emojisMenu.tsx} (70%) diff --git a/packages/core/src/blocks/Emoji.tsx b/packages/core/src/blocks/Emoji.tsx index 6610b49109..ff7d5c5400 100644 --- a/packages/core/src/blocks/Emoji.tsx +++ b/packages/core/src/blocks/Emoji.tsx @@ -1,7 +1,7 @@ -import { createReactInlineContentSpec } from "@blocknote/react"; +import { createInlineContentSpec } from "../schema"; -export const Emoji = createReactInlineContentSpec( +export const Emoji = createInlineContentSpec( //STEP 4: this component recieves an emoji, and insets it in the line { type: "emoji", @@ -13,13 +13,14 @@ export const Emoji = createReactInlineContentSpec( content: "none", }, { - render: (props) => { - return( - - - {props.inlineContent.props.emoji} - - )}, + render: (props: any) => { + const dom = document.createElement("span"); + dom.appendChild(document.createTextNode(props.props.emoji)); + + return { + dom, + }; + }, } ); \ No newline at end of file diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index f80385d916..494d2e68e9 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -253,9 +253,6 @@ export class SuggestionMenuProseMirrorPlugin< if (event.key == ':') { //only proceed if it was the trigger character (:) - const suggestionPluginState: SuggestionPluginState = ( - this as Plugin - ).getState(view.state); view.dispatch( view.state.tr .insertText(':') diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index c08b376d87..69ff65c7e8 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -31,13 +31,14 @@ export function SuggestionMenuController< >( props: { triggerCharacter: string; + isEmoji?: boolean; getItems?: GetItemsType; } & (ItemType extends DefaultReactSuggestionItem ? { // can be undefined suggestionMenuComponent?: FC< SuggestionMenuProps> - >; + > | FC; onItemClick?: (item: ItemType) => void; } : { @@ -67,7 +68,7 @@ export function SuggestionMenuController< ); }, [editor, onItemClick]); - const insertSlashMenuOption = (editor) => ({ + const insertSlashMenuOption = (editor: any) => ({ title: "Emoji", //we are simulating a : keydown event to trigger the emoji menu onItemClick: () => { @@ -159,7 +160,7 @@ export function SuggestionMenuController< query={state.query} closeMenu={callbacks.closeMenu} clearQuery={callbacks.clearQuery} - isEmoji={isEmoji} + isEmoji={isEmoji || false} getItems={getItemsOrDefault} suggestionMenuComponent={suggestionMenuComponent || SuggestionMenu} onItemClick={onItemClickOrDefault} diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index 9240d91768..a5ea82ebb9 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -15,6 +15,7 @@ export function SuggestionMenuWrapper(props: { getItems: (query: string) => Promise; onItemClick?: (item: Item) => void; suggestionMenuComponent: FC>; + isEmoji: boolean }) { const ctx = useBlockNoteContext(); const setContentEditableProps = ctx!.setContentEditableProps!; @@ -24,9 +25,9 @@ export function SuggestionMenuWrapper(props: { StyleSchema >(); - const emojiInsert = (emojiToInsert) => { + const emojiInsert = (item : never) => { //STEP 3: handle onclick, this function is called whenever an emoji is clicked or enter key is pressed on an emoji - + //I had to make it never since it's not supporting any other type under the hood clearQuery() editor.insertInlineContent([ { @@ -34,7 +35,7 @@ export function SuggestionMenuWrapper(props: { type: "emoji", props: { //pass the emoji as a prop so it can be inserted in the text - emoji : emojiToInsert + emoji : item }, }, " ", // add a space after the emoji @@ -71,8 +72,8 @@ export function SuggestionMenuWrapper(props: { editor, query, items, + isEmoji, isEmoji ? emojiInsert : onItemClickCloseMenu, - isEmoji ); // set basic aria attributes when the menu is open @@ -112,11 +113,9 @@ export function SuggestionMenuWrapper(props: { return ( ); diff --git a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx b/packages/react/src/components/SuggestionMenu/emojisMenu.tsx similarity index 70% rename from packages/react/src/components/SuggestionMenu/emojisMenu.jsx rename to packages/react/src/components/SuggestionMenu/emojisMenu.tsx index 2e5d590a57..383ecf9386 100644 --- a/packages/react/src/components/SuggestionMenu/emojisMenu.jsx +++ b/packages/react/src/components/SuggestionMenu/emojisMenu.tsx @@ -1,6 +1,10 @@ - -export default function EmojiMenu({items, emojiInsert, selectedIndex}){ +type Props= { + items: string[], + emojiInsert: (item : never) => void, + selectedIndex: number +} + export default function EmojiMenu({items, emojiInsert, selectedIndex} : Props) : JSX.Element { //this is the component which renders emoji picker with the emojis searched. @@ -8,7 +12,7 @@ export default function EmojiMenu({items, emojiInsert, selectedIndex}){
{ items.map((item, index)=>( -

(emojiInsert(item))}>{item}

+

(emojiInsert(item as never))}>{item}

)) }
diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index d7cf017786..35157d8a3c 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -7,8 +7,8 @@ export function useSuggestionMenuKeyboardNavigation( editor: BlockNoteEditor, query: string, items: Item[], - onItemClick?: (item: Item) => void, - isEmojiMenu + isEmojiMenu: any, + onItemClick?:( (item: Item) => void )| ((item: never) => void) ) { const [selectedIndex, setSelectedIndex] = useState(0); @@ -52,7 +52,7 @@ export function useSuggestionMenuKeyboardNavigation( event.preventDefault(); if (items.length) { - onItemClick?.(items[selectedIndex]); + onItemClick?.(items[selectedIndex] as never); } return true; diff --git a/packages/react/src/components/SuggestionMenu/types.tsx b/packages/react/src/components/SuggestionMenu/types.tsx index a3d7968331..ddbbfa5b4f 100644 --- a/packages/react/src/components/SuggestionMenu/types.tsx +++ b/packages/react/src/components/SuggestionMenu/types.tsx @@ -18,4 +18,5 @@ export type SuggestionMenuProps = { loadingState: "loading-initial" | "loading" | "loaded"; selectedIndex: number | undefined; onItemClick?: (item: T) => void; + emojiInsert?: (emoji: never) => void }; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 01bd9dc35b..cb7cde5b61 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -5,7 +5,7 @@ import { SideMenuController } from "../components/SideMenu/SideMenuController"; import { SuggestionMenuController } from "../components/SuggestionMenu/SuggestionMenuController"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; -import EmojiMenu from '../components/SuggestionMenu/emojisMenu.jsx' +import EmojiMenu from '../components/SuggestionMenu/emojisMenu.js' import { Data, init } from "emoji-mart"; export type BlockNoteDefaultUIProps = { @@ -24,20 +24,19 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { -let allemojis = []; //this makes sure emjois aren't fetched at every render init({ Data }) -async function search(value) { +async function search(value: string) { if(value == ''){ //Don't do unnecessary linear search if no search query - return Object.values(Data.emojis).map(emoji=>( + return Object.values(Data.emojis).map((emoji : any)=>( emoji.skins[0].native )) } - const emojisToShow = [] + const emojisToShow : any[] = [] //begin the linear search - Object.values(Data.emojis).forEach((emoji)=>{ + Object.values(Data.emojis).forEach((emoji : any)=>{ //check for every keyword, until a keyword contains the query for(let a = 0; a < emoji.keywords.length; a++){ let keyword = emoji.keywords[a]; From d7f248bb03cf498f28d68a43546539af4484eec2 Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 26 Jun 2024 21:13:23 +0500 Subject: [PATCH 10/41] added dark mode, moved styles to css file --- packages/mantine/src/style.css | 2 +- .../components/SuggestionMenu/emojisMenu.tsx | 4 +-- .../react/src/editor/BlockNoteDefaultUI.tsx | 19 ++++--------- packages/react/src/editor/styles.css | 27 +++++++++++++++++++ 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 4dcddf02db..607bc95def 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -236,7 +236,7 @@ own. */ /* https://github.com/mantinedev/mantine/blob/e3e3bb834de1f2f75a27dbc757dc0a2fc6a6cba8/packages/%40mantine/core/src/components/Menu/Menu.module.css */ .bn-suggestion-menu { - max-height: 100%; + max-height: 10% !important; position: relative; box-shadow: var(--mantine-shadow-md); border: calc(0.0625rem * var(--mantine-scale)) solid diff --git a/packages/react/src/components/SuggestionMenu/emojisMenu.tsx b/packages/react/src/components/SuggestionMenu/emojisMenu.tsx index 383ecf9386..3d49a45541 100644 --- a/packages/react/src/components/SuggestionMenu/emojisMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/emojisMenu.tsx @@ -9,10 +9,10 @@ type Props= { return ( -
+
{ items.map((item, index)=>( -

(emojiInsert(item as never))}>{item}

+

(emojiInsert(item as never))}>{item}

)) }
diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index cb7cde5b61..986eef23b0 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -6,7 +6,7 @@ import { SuggestionMenuController } from "../components/SuggestionMenu/Suggestio import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; import EmojiMenu from '../components/SuggestionMenu/emojisMenu.js' -import { Data, init } from "emoji-mart"; +import { Data, SearchIndex, init } from "emoji-mart"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -34,22 +34,13 @@ async function search(value: string) { emoji.skins[0].native )) } - const emojisToShow : any[] = [] //begin the linear search - Object.values(Data.emojis).forEach((emoji : any)=>{ - //check for every keyword, until a keyword contains the query - for(let a = 0; a < emoji.keywords.length; a++){ - let keyword = emoji.keywords[a]; - if(keyword.includes(value)){ - emojisToShow.push(emoji.skins[0].native); - //dont go further if emoji satisfies query - break; - } - } + let emojisToShow = await SearchIndex.search(value); - }) //return the emojis - return emojisToShow; + return emojisToShow.map((emoji : any)=>( + emoji.skins[0].native + )); } diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css index 9afd24dff7..05ba88c7fc 100644 --- a/packages/react/src/editor/styles.css +++ b/packages/react/src/editor/styles.css @@ -239,3 +239,30 @@ .bn-side-menu[data-url="false"] { height: 54px; } + +/* styles for the emoji menu */ +.bn-emoji-menu{ + display: grid; + justify-items: center; + gap: 7; + padding: 20; + max-height: 30vh; + min-width: 40vw; + overflow-y: scroll; + border-radius: var(--bn-border-radius-large); + background: var(--bn-colors-menu-background); + box-shadow: var(--bn-shadow-medium); +} + +.bn-emoji-menu p{ + border-radius: var(--bn-border-radius-large); + padding: 4px; + margin: 2px; + font-size: larger; + cursor: pointer; +} + +.emoji-selected{ + background: var(--bn-colors-selected-background); +} + From b8d08eaca4513b8aa1574d7fb26b98b6e02bdcf1 Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 26 Jun 2024 22:03:34 +0500 Subject: [PATCH 11/41] fixed lint --- .../core/src/extensions/SuggestionMenu/SuggestionPlugin.ts | 2 +- .../hooks/useSuggestionMenuKeyboardNavigation.ts | 2 +- packages/react/src/editor/BlockNoteDefaultUI.tsx | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 494d2e68e9..6ed6b4138a 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -251,7 +251,7 @@ export class SuggestionMenuProseMirrorPlugin< //STEP 6: the final thing is to listen to the simulated keydown event generated by the onClick of emoji option in slash menu handleKeyDown(view, event) { - if (event.key == ':') { + if (event.key === ':') { //only proceed if it was the trigger character (:) view.dispatch( view.state.tr diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index 35157d8a3c..bfb589861c 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -74,7 +74,7 @@ export function useSuggestionMenuKeyboardNavigation( true ); }; - }, [editor.domElement, items, selectedIndex, onItemClick]); + }, [editor.domElement, items, selectedIndex, onItemClick, isEmojiMenu]); // Resets index when items change useEffect(() => { diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 986eef23b0..a930aed7a8 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -28,14 +28,14 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { init({ Data }) async function search(value: string) { - if(value == ''){ + if(value === ''){ //Don't do unnecessary linear search if no search query return Object.values(Data.emojis).map((emoji : any)=>( emoji.skins[0].native )) } //begin the linear search - let emojisToShow = await SearchIndex.search(value); + const emojisToShow = await SearchIndex.search(value); //return the emojis return emojisToShow.map((emoji : any)=>( From 9f0051adaea3d0e21618c467a0b28d6d6628a06d Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 26 Jun 2024 22:34:40 +0500 Subject: [PATCH 12/41] resolve conflict --- packages/mantine/src/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 607bc95def..c043bb09ab 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -235,8 +235,8 @@ /* Unfortunately necessary, as we can't use a Menu.Dropdown component on its own. */ /* https://github.com/mantinedev/mantine/blob/e3e3bb834de1f2f75a27dbc757dc0a2fc6a6cba8/packages/%40mantine/core/src/components/Menu/Menu.module.css */ -.bn-suggestion-menu { - max-height: 10% !important; +.bn-mantine .bn-suggestion-menu { + max-height: 100%; position: relative; box-shadow: var(--mantine-shadow-md); border: calc(0.0625rem * var(--mantine-scale)) solid From 6114681b0386abe98524b0eb74974ea2d8447794 Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Fri, 28 Jun 2024 21:14:21 +0200 Subject: [PATCH 13/41] Started refactor --- .../SuggestionMenuController.tsx | 49 +++------- .../SuggestionMenu/SuggestionMenuWrapper.tsx | 32 ++----- .../components/SuggestionMenu/emojisMenu.tsx | 35 +++---- ...useGridSuggestionMenuKeyboardNavigation.ts | 91 +++++++++++++++++++ .../useSuggestionMenuKeyboardNavigation.ts | 25 +---- .../react/src/editor/BlockNoteDefaultUI.tsx | 74 ++++++++------- 6 files changed, 174 insertions(+), 132 deletions(-) create mode 100644 packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 69ff65c7e8..3f566ae468 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -15,7 +15,6 @@ import { SuggestionMenu } from "./SuggestionMenu"; import { SuggestionMenuWrapper } from "./SuggestionMenuWrapper"; import { getDefaultReactSlashMenuItems } from "./getDefaultReactSlashMenuItems"; import { DefaultReactSuggestionItem, SuggestionMenuProps } from "./types"; -import { MdEmojiEmotions } from "react-icons/md"; type ArrayElement
= A extends readonly (infer T)[] ? T : never; @@ -31,14 +30,14 @@ export function SuggestionMenuController< >( props: { triggerCharacter: string; - isEmoji?: boolean; getItems?: GetItemsType; + grid?: boolean; } & (ItemType extends DefaultReactSuggestionItem ? { // can be undefined suggestionMenuComponent?: FC< SuggestionMenuProps> - > | FC; + >; onItemClick?: (item: ItemType) => void; } : { @@ -55,9 +54,13 @@ export function SuggestionMenuController< StyleSchema >(); - const { triggerCharacter, suggestionMenuComponent } = props; - - const { onItemClick, isEmoji, getItems } = props; + const { + triggerCharacter, + suggestionMenuComponent, + grid, + onItemClick, + getItems, + } = props; const onItemClickOrDefault = useMemo(() => { return ( @@ -68,42 +71,12 @@ export function SuggestionMenuController< ); }, [editor, onItemClick]); - const insertSlashMenuOption = (editor: any) => ({ - title: "Emoji", - //we are simulating a : keydown event to trigger the emoji menu - onItemClick: () => { - const keyboardEvent = new KeyboardEvent('keydown', { - key: ':', // key identifier (DOMString), optional - code: 'Colon', // key code identifier (DOMString), optional - keyCode: 186, // key code value (unsigned long), optional - which: 186, // legacy keyCode, optional - charCode: 0, // character code value (unsigned long), optional - bubbles: false, // bubbles flag (boolean), optional - cancelable: false, // cancelable flag (boolean), optional - composed: false, // composed flag (boolean), optional - ctrlKey: false, // control key flag (boolean), optional - altKey: false, // alt key flag (boolean), optional - shiftKey: false, // shift key flag (boolean), optional - metaKey: false // meta key flag (boolean), optional - }); - editor.domElement.dispatchEvent(keyboardEvent); - }, - aliases: [ - "emoji", - "emote", - "face", - ], - group: "Other", - icon: , - }); - const getItemsOrDefault = useMemo(() => { return ( getItems || ((async (query: string) => filterSuggestionItems( - //STEP 6: SHOW THE EMOJI OPTION IN THE SLASH MENU - [...getDefaultReactSlashMenuItems(editor), insertSlashMenuOption(editor)], + getDefaultReactSlashMenuItems(editor), query )) as any as typeof getItems) ); @@ -160,8 +133,8 @@ export function SuggestionMenuController< query={state.query} closeMenu={callbacks.closeMenu} clearQuery={callbacks.clearQuery} - isEmoji={isEmoji || false} getItems={getItemsOrDefault} + grid={grid} suggestionMenuComponent={suggestionMenuComponent || SuggestionMenu} onItemClick={onItemClickOrDefault} /> diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index a5ea82ebb9..20427f21b3 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -4,6 +4,7 @@ import { FC, useCallback, useEffect } from "react"; import { useBlockNoteContext } from "../../editor/BlockNoteContext"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; import { useCloseSuggestionMenuNoItems } from "./hooks/useCloseSuggestionMenuNoItems"; +import { useGridSuggestionMenuKeyboardNavigation } from "./hooks/useGridSuggestionMenuKeyboardNavigation"; import { useLoadSuggestionMenuItems } from "./hooks/useLoadSuggestionMenuItems"; import { useSuggestionMenuKeyboardNavigation } from "./hooks/useSuggestionMenuKeyboardNavigation"; import { SuggestionMenuProps } from "./types"; @@ -13,9 +14,9 @@ export function SuggestionMenuWrapper(props: { closeMenu: () => void; clearQuery: () => void; getItems: (query: string) => Promise; + grid?: boolean; onItemClick?: (item: Item) => void; suggestionMenuComponent: FC>; - isEmoji: boolean }) { const ctx = useBlockNoteContext(); const setContentEditableProps = ctx!.setContentEditableProps!; @@ -25,23 +26,6 @@ export function SuggestionMenuWrapper(props: { StyleSchema >(); - const emojiInsert = (item : never) => { - //STEP 3: handle onclick, this function is called whenever an emoji is clicked or enter key is pressed on an emoji - //I had to make it never since it's not supporting any other type under the hood - clearQuery() - editor.insertInlineContent([ - { - //call the inlinecontent of type of emoji declared in defaultBlocks.ts - type: "emoji", - props: { - //pass the emoji as a prop so it can be inserted in the text - emoji : item - }, - }, - " ", // add a space after the emoji - ]); - } - const { getItems, suggestionMenuComponent, @@ -49,7 +33,7 @@ export function SuggestionMenuWrapper(props: { clearQuery, closeMenu, onItemClick, - isEmoji + grid, } = props; const onItemClickCloseMenu = useCallback( @@ -68,12 +52,15 @@ export function SuggestionMenuWrapper(props: { useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu); - const { selectedIndex } = useSuggestionMenuKeyboardNavigation( + const useKeyboardNavigation = grid + ? useGridSuggestionMenuKeyboardNavigation + : useSuggestionMenuKeyboardNavigation; + + const { selectedIndex } = useKeyboardNavigation( editor, query, items, - isEmoji, - isEmoji ? emojiInsert : onItemClickCloseMenu, + onItemClickCloseMenu ); // set basic aria attributes when the menu is open @@ -113,7 +100,6 @@ export function SuggestionMenuWrapper(props: { return ( void, - selectedIndex: number -} - export default function EmojiMenu({items, emojiInsert, selectedIndex} : Props) : JSX.Element { +export default function EmojiMenu( + props: SuggestionMenuProps +): JSX.Element { //this is the component which renders emoji picker with the emojis searched. + const { items, selectedIndex, onItemClick } = props; - - return ( -
- { - items.map((item, index)=>( -

(emojiInsert(item as never))}>{item}

- )) - } + return ( +
+ {items.map((item, index) => ( +

onItemClick?.(item)}> + {item} +

+ ))}
- ) -} \ No newline at end of file + ); +} diff --git a/packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts new file mode 100644 index 0000000000..5e62588833 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts @@ -0,0 +1,91 @@ +import { BlockNoteEditor } from "@blocknote/core"; +import { useEffect, useState } from "react"; + +// Hook which handles keyboard navigation of a suggestion menu. Arrow keys are +// used to select a menu item, enter to execute it. This version uses the left +// & right arrow keys in addition to up & down to navigate a grid. +export function useGridSuggestionMenuKeyboardNavigation( + editor: BlockNoteEditor, + query: string, + items: Item[], + onItemClick?: ((item: Item) => void) | ((item: never) => void) +) { + const [selectedIndex, setSelectedIndex] = useState(0); + + useEffect(() => { + const handleMenuNavigationKeys = (event: KeyboardEvent) => { + if (event.key === "ArrowUp") { + event.preventDefault(); + + if (items.length) { + setSelectedIndex(selectedIndex - 10 >= 0 ? selectedIndex - 10 : 0); + } + + return true; + } + + if (event.key === "ArrowDown") { + event.preventDefault(); + + if (items.length) { + setSelectedIndex( + selectedIndex + 10 < items!.length + ? selectedIndex + 10 + : items!.length - 1 + ); + } + + return true; + } + + if (event.key === "ArrowRight") { + event.preventDefault(); + if (items.length) { + setSelectedIndex((selectedIndex + 1 + items!.length) % items!.length); + } + } + + if (event.key === "ArrowLeft") { + event.preventDefault(); + if (items.length) { + setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); + } + } + + if (event.key === "Enter") { + event.preventDefault(); + + if (items.length) { + onItemClick?.(items[selectedIndex] as never); + } + + return true; + } + + return false; + }; + + editor.domElement.addEventListener( + "keydown", + handleMenuNavigationKeys, + true + ); + + return () => { + editor.domElement.removeEventListener( + "keydown", + handleMenuNavigationKeys, + true + ); + }; + }, [editor.domElement, items, selectedIndex, onItemClick]); + + // Resets index when items change + useEffect(() => { + setSelectedIndex(0); + }, [query]); + + return { + selectedIndex: items.length === 0 ? undefined : selectedIndex, + }; +} diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index bfb589861c..0306914c5e 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -7,8 +7,7 @@ export function useSuggestionMenuKeyboardNavigation( editor: BlockNoteEditor, query: string, items: Item[], - isEmojiMenu: any, - onItemClick?:( (item: Item) => void )| ((item: never) => void) + onItemClick?: (item: Item) => void ) { const [selectedIndex, setSelectedIndex] = useState(0); @@ -18,7 +17,7 @@ export function useSuggestionMenuKeyboardNavigation( event.preventDefault(); if (items.length) { - isEmojiMenu ? setSelectedIndex(selectedIndex - 10 >= 0 ? selectedIndex - 10 : 0) : setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); + setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); } return true; @@ -28,31 +27,17 @@ export function useSuggestionMenuKeyboardNavigation( event.preventDefault(); if (items.length) { - isEmojiMenu ? setSelectedIndex(selectedIndex + 10 < items!.length ? selectedIndex + 10 : items!.length -1) : setSelectedIndex((selectedIndex + 1) % items!.length); + setSelectedIndex((selectedIndex + 1) % items!.length); } return true; } - if(event.key === 'ArrowRight' && isEmojiMenu){ - event.preventDefault() - if(items.length){ - setSelectedIndex((selectedIndex + 1 + items!.length) % items!.length); - } - } - - if(event.key === 'ArrowLeft' && isEmojiMenu){ - event.preventDefault() - if(items.length){ - setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); - } - } - if (event.key === "Enter") { event.preventDefault(); if (items.length) { - onItemClick?.(items[selectedIndex] as never); + onItemClick?.(items[selectedIndex]); } return true; @@ -74,7 +59,7 @@ export function useSuggestionMenuKeyboardNavigation( true ); }; - }, [editor.domElement, items, selectedIndex, onItemClick, isEmojiMenu]); + }, [editor.domElement, items, selectedIndex, onItemClick]); // Resets index when items change useEffect(() => { diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index a930aed7a8..2aa6a73034 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -3,9 +3,9 @@ import { LinkToolbarController } from "../components/LinkToolbar/LinkToolbarCont import { FilePanelController } from "../components/FilePanel/FilePanelController"; import { SideMenuController } from "../components/SideMenu/SideMenuController"; import { SuggestionMenuController } from "../components/SuggestionMenu/SuggestionMenuController"; +import EmojiMenu from "../components/SuggestionMenu/emojisMenu.js"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; -import EmojiMenu from '../components/SuggestionMenu/emojisMenu.js' import { Data, SearchIndex, init } from "emoji-mart"; export type BlockNoteDefaultUIProps = { @@ -17,38 +17,29 @@ export type BlockNoteDefaultUIProps = { tableHandles?: boolean; }; - - - export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { + //this makes sure emjois aren't fetched at every render + init({ Data }); + async function search(value: string) { + if (value === "") { + //Don't do unnecessary linear search if no search query + return Object.values(Data.emojis).map( + (emoji: any) => emoji.skins[0].native + ); + } + //begin the linear search + const emojisToShow = await SearchIndex.search(value); - -//this makes sure emjois aren't fetched at every render -init({ Data }) - -async function search(value: string) { - if(value === ''){ - //Don't do unnecessary linear search if no search query - return Object.values(Data.emojis).map((emoji : any)=>( - emoji.skins[0].native - )) + //return the emojis + return emojisToShow.map((emoji: any) => emoji.skins[0].native); } - //begin the linear search - const emojisToShow = await SearchIndex.search(value); - - //return the emojis - return emojisToShow.map((emoji : any)=>( - emoji.skins[0].native - )); - -} const editor = useBlockNoteEditor(); - async function emojiChangeHandler(query: string){ - //STEP 2: call the search function, which returns an array of emojis as items for the component - return search(query); -} + async function emojiChangeHandler(query: string) { + //STEP 2: call the search function, which returns an array of emojis as items for the component + return search(query); + } if (!editor) { throw new Error( @@ -62,14 +53,27 @@ async function search(value: string) { {props.linkToolbar !== false && } {props.slashMenu !== false && ( <> - - {/*STEP 1: the suggestion menu for the emojis, with custom component and getItems */} - + + + editor.insertInlineContent([ + { + type: "emoji", + props: { + emoji: item as any, + }, + }, + { + type: "text", + text: " ", + }, + ]) + } + /> )} {props.sideMenu !== false && } From 1f19707b750bcc76b4885ff1d8104e61635f488b Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Sat, 29 Jun 2024 02:40:02 +0500 Subject: [PATCH 14/41] added the empji item to slash menu and setup onclick, only eng locale for now --- packages/core/src/blocks/Emoji.tsx | 2 +- .../getDefaultSlashMenuItems.ts | 20 +++++++++++++++++++ packages/core/src/i18n/locales/en.ts | 6 ++++++ .../getDefaultReactSlashMenuItems.tsx | 2 ++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/core/src/blocks/Emoji.tsx b/packages/core/src/blocks/Emoji.tsx index ff7d5c5400..97da365019 100644 --- a/packages/core/src/blocks/Emoji.tsx +++ b/packages/core/src/blocks/Emoji.tsx @@ -7,7 +7,7 @@ export const Emoji = createInlineContentSpec( type: "emoji", propSchema: { emoji: { - default: "Unknown", + default: "", }, }, content: "none", diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 36fc89ffbf..0a5dd7c5b9 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -1,4 +1,6 @@ import { Block, PartialBlock } from "../../blocks/defaultBlocks"; +import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin"; + import { checkDefaultBlockTypeInSchema } from "../../blocks/defaultBlockTypeGuards"; import { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { @@ -80,6 +82,8 @@ export function getDefaultSlashMenuItems< >(editor: BlockNoteEditor) { const items: DefaultSuggestionItem[] = []; + + if (checkDefaultBlockTypeInSchema("heading", editor)) { items.push( { @@ -270,6 +274,22 @@ export function getDefaultSlashMenuItems< }); } + items.push({ + onItemClick: () => { + editor.prosemirrorView.focus(); + editor.prosemirrorView.dispatch( + editor.prosemirrorView.state.tr + .scrollIntoView() + .setMeta(suggestionMenuPluginKey, { + triggerCharacter: ":", + fromUserInput: false, + }) + ); + }, + key: "emoji", + ...editor.dictionary.slash_menu.emoji, + }) + return items; } diff --git a/packages/core/src/i18n/locales/en.ts b/packages/core/src/i18n/locales/en.ts index 9edbb6df20..ecc2f4fe45 100644 --- a/packages/core/src/i18n/locales/en.ts +++ b/packages/core/src/i18n/locales/en.ts @@ -104,6 +104,12 @@ export const en = { aliases: ["file", "upload", "embed", "media", "url"], group: "Media", }, + emoji: { + title: "Emoji", + subtext: "Used for inserting an emoji", + aliases: ["emoji", "emote", "emotion", "face"], + group: "Others", + }, }, placeholders: { default: "Enter text or type '/' for commands", diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx index 88fbe29bab..721c9e57e7 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx @@ -20,6 +20,7 @@ import { RiVolumeUpFill, } from "react-icons/ri"; import { DefaultReactSuggestionItem } from "./types"; +import { MdFace } from "react-icons/md"; const icons = { heading: RiH1, @@ -34,6 +35,7 @@ const icons = { video: RiFilmLine, audio: RiVolumeUpFill, file: RiFile2Line, + emoji: MdFace }; export function getDefaultReactSlashMenuItems< From 1c0fd74b6d51d0d44c27c749f2e7974023c34c9f Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Sat, 29 Jun 2024 03:20:52 +0500 Subject: [PATCH 15/41] get the emoji.tsx to render the emoji inline --- .../react/src/editor/BlockNoteDefaultUI.tsx | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 2aa6a73034..58c812f3b4 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -60,18 +60,17 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { suggestionMenuComponent={EmojiMenu} getItems={emojiChangeHandler} onItemClick={(item) => - editor.insertInlineContent([ - { - type: "emoji", - props: { - emoji: item as any, - }, - }, - { - type: "text", - text: " ", - }, - ]) + { + editor.insertInlineContent([ + { + type: 'emoji', + props: { + emoji: item + } + }, " " + ]) + + } } /> From 1941739154f9c38e77e12675a2face43b446085f Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Sat, 29 Jun 2024 03:41:47 +0500 Subject: [PATCH 16/41] add option to show or hide emoji suggestion menu --- .../react/src/editor/BlockNoteDefaultUI.tsx | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 58c812f3b4..3b9e24131d 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -15,6 +15,7 @@ export type BlockNoteDefaultUIProps = { sideMenu?: boolean; filePanel?: boolean; tableHandles?: boolean; + emojiPicker?: boolean }; export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { @@ -52,29 +53,31 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { {props.formattingToolbar !== false && } {props.linkToolbar !== false && } {props.slashMenu !== false && ( - <> + )} + { + props.emojiPicker !== false && ( - { - editor.insertInlineContent([ - { - type: 'emoji', - props: { - emoji: item - } - }, " " - ]) - + triggerCharacter={":"} + grid={true} + suggestionMenuComponent={EmojiMenu} + getItems={emojiChangeHandler} + onItemClick={(item) => + { + editor.insertInlineContent([ + { + type: 'emoji', + props: { + emoji: item + } + }, " " + ]) + + } } - } - /> - - )} + /> + ) + } {props.sideMenu !== false && } {editor.filePanel && props.filePanel !== false && } {editor.tableHandles && props.tableHandles !== false && ( From 873176b215ca25f6e287e917e13799686e2263a5 Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Sat, 29 Jun 2024 04:02:22 +0500 Subject: [PATCH 17/41] added locales and fixed type issue --- packages/core/src/i18n/locales/ar.ts | 6 ++++++ packages/core/src/i18n/locales/fr.ts | 7 +++++++ packages/core/src/i18n/locales/is.ts | 6 ++++++ packages/core/src/i18n/locales/ja.ts | 7 +++++++ packages/core/src/i18n/locales/ko.ts | 8 ++++++++ packages/core/src/i18n/locales/nl.ts | 7 +++++++ packages/core/src/i18n/locales/pl.ts | 7 +++++++ packages/core/src/i18n/locales/pt.ts | 7 +++++++ packages/core/src/i18n/locales/vi.ts | 7 +++++++ packages/core/src/i18n/locales/zh.ts | 7 +++++++ packages/react/src/editor/BlockNoteDefaultUI.tsx | 2 +- 11 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/core/src/i18n/locales/ar.ts b/packages/core/src/i18n/locales/ar.ts index 4dbae5fdbf..e91ac8ed44 100644 --- a/packages/core/src/i18n/locales/ar.ts +++ b/packages/core/src/i18n/locales/ar.ts @@ -131,6 +131,12 @@ export const ar: Dictionary = { aliases: ["file", "upload", "embed", "media", "url", "ملف", "رفع"], group: "الوسائط", }, + emoji: { + title: "الرموز التعبيرية", + subtext: "تُستخدم لإدراج رمز تعبيري", + aliases: ["رمز تعبيري", "إيموجي", "إيموت", "عاطفة", "وجه"], + group: "آخرون", + }, }, placeholders: { default: "أدخل النص أو اكتب '/' للأوامر", diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index c4345c6631..e458540274 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -105,6 +105,13 @@ export const fr: Dictionary = { aliases: ["fichier", "téléverser", "intégrer", "média", "url"], group: "Média", }, + emoji: { + title: "Emoji", + subtext: "Utilisé pour insérer un emoji", + aliases: ["emoji", "émoticône", "émotion", "visage"], + group: "Autres", + }, + }, placeholders: { default: "Entrez du texte ou tapez '/' pour les commandes", diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index 729dc91ddc..f37697e886 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -98,6 +98,12 @@ export const is: Dictionary = { aliases: ["skrá", "hlaða upp", "fella inn", "miðill", "url"], group: "Miðlar", }, + emoji: { + title: "Emoji", + subtext: "Notað til að setja inn smámynd", + aliases: ["emoji", "andlitsávísun", "tilfinningar", "andlit"], + group: "Annað", + }, }, placeholders: { default: "Sláðu inn texta eða skrifaðu '/' fyrir skipanir", diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index 59dbc267ad..eae375a08a 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -125,6 +125,13 @@ export const ja: Dictionary = { aliases: ["file", "upload", "embed", "media", "url", "ファイル"], group: "メディア", }, + emoji: { + title: "絵文字", + subtext: "絵文字を挿入するために使用します", + aliases: ["絵文字", "顔文字", "感情表現", "顔"], + group: "その他", + }, + }, placeholders: { default: "テキストを入力するか'/' を入力してコマンド選択", diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 20127fc976..91d71781e6 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -109,6 +109,14 @@ export const ko: Dictionary = { aliases: ["file", "upload", "embed", "media", "파일", "url"], group: "미디어", }, + emoji: { + title: "이모지", + subtext: "이모지 삽입용으로 사용됩니다", + aliases: ["이모지", "emoji", "감정 표현", "emotion expression", "표정", "face expression", "얼굴", "face"], + group: "기타", + }, + + }, placeholders: { default: "텍스트를 입력하거나 /를 입력하여 명령을 입력하세요.", diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index 1cd8cf747a..ebe4cb5427 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -100,6 +100,13 @@ export const nl: Dictionary = { aliases: ["bestand", "upload", "insluiten", "media", "url"], group: "Media", }, + emoji: { + title: "Emoji", + subtext: "Gebruikt voor het invoegen van een emoji", + aliases: ["emoji", "emotie-uitdrukking", "gezichtsuitdrukking", "gezicht"], + group: "Overig", + }, + }, placeholders: { default: "Voer tekst in of type '/' voor commando's", diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index e5647c1b31..5763da4876 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -90,6 +90,13 @@ export const pl: Dictionary = { aliases: ["plik", "wrzuć", "wstaw", "media", "url"], group: "Media", }, + emoji: { + title: "Emoji", + subtext: "Używane do wstawiania emoji", + aliases: ["emoji", "emotka", "wyrażenie emocji", "twarz"], + group: "Inne", + }, + }, placeholders: { default: "Wprowadź tekst lub wpisz '/' aby użyć poleceń", diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 946489553d..212df838e6 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -97,6 +97,13 @@ export const pt: Dictionary = { aliases: ["arquivo", "upload", "incorporar", "mídia", "url"], group: "Mídia", }, + emoji: { + title: "Emoji", + subtext: "Usado para inserir um emoji", + aliases: ["emoji", "emoticon", "expressão emocional", "rosto"], + group: "Outros", + }, + }, placeholders: { default: "Digite texto ou use '/' para comandos", diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index 054076f321..b00751875b 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -97,6 +97,13 @@ export const vi: Dictionary = { aliases: ["tep", "tai-len", "nhung", "media", "url"], group: "Phương tiện", }, + emoji: { + title: "Biểu tượng cảm xúc", + subtext: "Dùng để chèn biểu tượng cảm xúc", + aliases: ["biểu tượng cảm xúc", "emoji", "emoticon", "cảm xúc expression", "khuôn mặt", "face"], + group: "Khác", + }, + }, placeholders: { default: "Nhập văn bản hoặc gõ '/' để thêm định dạng", diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index eb9364a89d..da9f33d75f 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -130,6 +130,13 @@ export const zh: Dictionary = { aliases: ["文件", "上传", "file", "嵌入", "媒体", "url"], group: "媒体", }, + emoji: { + title: "表情符号", + subtext: "用于插入表情符号", + aliases: ["表情符号", "emoji", "face", "emote", "表情", "表情表达", "表情"], + group: "其他", + }, + }, placeholders: { default: "输入 '/' 以使用命令", diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 3b9e24131d..c492c5ddea 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -68,7 +68,7 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { { type: 'emoji', props: { - emoji: item + emoji: item as any } }, " " ]) From e62f21c21748b50bbb1024527d8e7b62d051c48e Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Sat, 29 Jun 2024 11:52:28 +0500 Subject: [PATCH 18/41] generalize emojimenu component and move dependencies to react dir --- package.json | 5 ----- packages/react/package.json | 5 ++++- .../{emojisMenu.tsx => gridSuggestionMenu.tsx} | 6 +++--- packages/react/src/editor/BlockNoteDefaultUI.tsx | 4 ++-- packages/react/src/editor/styles.css | 6 +++--- 5 files changed, 12 insertions(+), 14 deletions(-) rename packages/react/src/components/SuggestionMenu/{emojisMenu.tsx => gridSuggestionMenu.tsx} (75%) diff --git a/package.json b/package.json index a0fce56da6..e7a4205f27 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,5 @@ "prepublishOnly": "npm run build && cp README.md packages/core/README.md && cp README.md packages/react/README.md", "postpublish": "rm -rf packages/core/README.md && rm -rf packages/react/README.md", "clean": "lerna run --stream clean" - }, - "dependencies": { - "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", - "emoji-mart": "^5.6.0" } } diff --git a/packages/react/package.json b/packages/react/package.json index e6cc38d9a4..1cfeaa0217 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -59,7 +59,10 @@ "react": "^18", "react-dom": "^18", "react-icons": "^5.2.1", - "use-prefers-color-scheme": "^1.1.3" + "use-prefers-color-scheme": "^1.1.3", + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", + "emoji-mart": "^5.6.0" }, "devDependencies": { "@types/lodash.foreach": "^4.5.9", diff --git a/packages/react/src/components/SuggestionMenu/emojisMenu.tsx b/packages/react/src/components/SuggestionMenu/gridSuggestionMenu.tsx similarity index 75% rename from packages/react/src/components/SuggestionMenu/emojisMenu.tsx rename to packages/react/src/components/SuggestionMenu/gridSuggestionMenu.tsx index 2225bbda3f..87e49f3a25 100644 --- a/packages/react/src/components/SuggestionMenu/emojisMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/gridSuggestionMenu.tsx @@ -1,6 +1,6 @@ import { SuggestionMenuProps } from "./types"; -export default function EmojiMenu( +export default function GridSuggestionMenu( props: SuggestionMenuProps ): JSX.Element { //this is the component which renders emoji picker with the emojis searched. @@ -8,12 +8,12 @@ export default function EmojiMenu( return (
{items.map((item, index) => (

onItemClick?.(item)}> {item}

diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index c492c5ddea..57dbffd3d0 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -3,7 +3,7 @@ import { LinkToolbarController } from "../components/LinkToolbar/LinkToolbarCont import { FilePanelController } from "../components/FilePanel/FilePanelController"; import { SideMenuController } from "../components/SideMenu/SideMenuController"; import { SuggestionMenuController } from "../components/SuggestionMenu/SuggestionMenuController"; -import EmojiMenu from "../components/SuggestionMenu/emojisMenu.js"; +import GridSuggestionMenu from "../components/SuggestionMenu/gridSuggestionMenu.js"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; import { Data, SearchIndex, init } from "emoji-mart"; @@ -60,7 +60,7 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { { diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css index 05ba88c7fc..b16f893820 100644 --- a/packages/react/src/editor/styles.css +++ b/packages/react/src/editor/styles.css @@ -241,7 +241,7 @@ } /* styles for the emoji menu */ -.bn-emoji-menu{ +.bn-grid-suggestion-menu{ display: grid; justify-items: center; gap: 7; @@ -254,7 +254,7 @@ box-shadow: var(--bn-shadow-medium); } -.bn-emoji-menu p{ +.bn-grid-suggestion-menu p{ border-radius: var(--bn-border-radius-large); padding: 4px; margin: 2px; @@ -262,7 +262,7 @@ cursor: pointer; } -.emoji-selected{ +.grid-item-selected{ background: var(--bn-colors-selected-background); } From 3fa49202c4c739623be6d694666b93adfb3937aa Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Sat, 29 Jun 2024 11:58:29 +0500 Subject: [PATCH 19/41] create a new dir inlineContent and retent at and structure it --- packages/core/src/blocks/defaultBlocks.ts | 2 +- .../src/{blocks => inlineContent/emojiInlineContent}/Emoji.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/core/src/{blocks => inlineContent/emojiInlineContent}/Emoji.tsx (88%) diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 6d02e76091..595ecbff44 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -30,7 +30,7 @@ import { FileBlock } from "./FileBlockContent/FileBlockContent"; import { ImageBlock } from "./ImageBlockContent/ImageBlockContent"; import { VideoBlock } from "./VideoBlockContent/VideoBlockContent"; import { AudioBlock } from "./AudioBlockContent/AudioBlockContent"; -import { Emoji } from "./Emoji"; +import { Emoji } from "../inlineContent/emojiInlineContent/Emoji"; export const defaultBlockSpecs = { paragraph: Paragraph, diff --git a/packages/core/src/blocks/Emoji.tsx b/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx similarity index 88% rename from packages/core/src/blocks/Emoji.tsx rename to packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx index 97da365019..28ab9cf98b 100644 --- a/packages/core/src/blocks/Emoji.tsx +++ b/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx @@ -1,4 +1,4 @@ -import { createInlineContentSpec } from "../schema"; +import { createInlineContentSpec } from "../../schema"; export const Emoji = createInlineContentSpec( From b0a218610460a5862a8b33a39ad332324980e4b4 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 2 Jul 2024 23:19:19 +0200 Subject: [PATCH 20/41] Added grid suggestion menu to component context + refactor --- package-lock.json | 18 +++- packages/ariakit/src/index.tsx | 6 ++ packages/ariakit/src/style.css | 32 +++++++ .../src/suggestionMenu/GridSuggestionMenu.tsx | 18 ++++ .../suggestionMenu/GridSuggestionMenuItem.tsx | 42 +++++++++ packages/core/package.json | 4 + .../core/src/blocks/defaultBlockTypeGuards.ts | 27 +++++- .../DefaultGridSuggestionItem.ts | 3 + .../getDefaultEmojiPickerItems.ts | 33 +++++++ .../getDefaultSlashMenuItems.ts | 4 +- packages/core/src/index.ts | 2 + packages/mantine/src/index.tsx | 8 +- packages/mantine/src/style.css | 32 +++++++ .../src/suggestionMenu/GridSuggestionMenu.tsx | 18 ++++ .../suggestionMenu/GridSuggestionMenuItem.tsx | 44 +++++++++ packages/react/package.json | 5 +- .../SuggestionMenu/GridSuggestionMenu.tsx | 75 +++++++++++++++ .../SuggestionMenu/SuggestionMenu.tsx | 1 + .../SuggestionMenu/SuggestionMenuWrapper.tsx | 8 +- .../getDefaultReactEmojiPickerItems.tsx | 21 +++++ .../SuggestionMenu/gridSuggestionMenu.tsx | 23 ----- ...useGridSuggestionMenuKeyboardNavigation.ts | 91 ------------------- .../useSuggestionMenuKeyboardNavigation.ts | 34 ++++++- .../src/components/SuggestionMenu/types.tsx | 10 +- .../react/src/editor/BlockNoteDefaultUI.tsx | 74 ++++++--------- .../react/src/editor/ComponentsContext.tsx | 31 ++++++- packages/react/src/editor/styles.css | 27 ------ packages/shadcn/src/index.tsx | 6 ++ packages/shadcn/src/style.css | 32 +++++++ .../src/suggestionMenu/GridSuggestionMenu.tsx | 18 ++++ .../suggestionMenu/GridSuggestionMenuItem.tsx | 42 +++++++++ 31 files changed, 576 insertions(+), 213 deletions(-) create mode 100644 packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx create mode 100644 packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx create mode 100644 packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts create mode 100644 packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts create mode 100644 packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx create mode 100644 packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx create mode 100644 packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx create mode 100644 packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx delete mode 100644 packages/react/src/components/SuggestionMenu/gridSuggestionMenu.tsx delete mode 100644 packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts create mode 100644 packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx create mode 100644 packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx diff --git a/package-lock.json b/package-lock.json index 906da0b5db..0d31339577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,6 @@ "tests", "docs" ], - "dependencies": { - "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", - "emoji-mart": "^5.6.0" - }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.5.0", "@typescript-eslint/parser": "^5.5.0", @@ -7773,6 +7768,15 @@ "@types/ms": "*" } }, + "node_modules/@types/emoji-mart": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.14.tgz", + "integrity": "sha512-/vMkVnet466bK37ugf2jry9ldCZklFPXYMB2m+qNo3vkP2I7L0cvtNFPKAjfcHgPg9Z8pbYqVqZn7AgsC0qf+g==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -27264,9 +27268,12 @@ "license": "MPL-2.0", "dependencies": { "@blocknote/core": "^0.14.1", + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", "@floating-ui/react": "^0.26.4", "@tiptap/core": "^2.4.0", "@tiptap/react": "^2.4.0", + "emoji-mart": "^5.6.0", "lodash.merge": "^4.6.2", "react": "^18", "react-dom": "^18", @@ -27274,6 +27281,7 @@ "use-prefers-color-scheme": "^1.1.3" }, "devDependencies": { + "@types/emoji-mart": "^3.0.14", "@types/lodash.foreach": "^4.5.9", "@types/lodash.groupby": "^4.6.9", "@types/lodash.merge": "^4.6.9", diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx index 5afaab6c91..ec057f7e8a 100644 --- a/packages/ariakit/src/index.tsx +++ b/packages/ariakit/src/index.tsx @@ -7,6 +7,8 @@ import { import { ComponentProps } from "react"; import { Form } from "./input/Form"; +import { GridSuggestionMenu } from "./suggestionMenu/GridSuggestionMenu"; +import { GridSuggestionMenuItem } from "./suggestionMenu/GridSuggestionMenuItem"; import { TextInput } from "./input/TextInput"; import { Menu, @@ -49,6 +51,10 @@ export const components: Components = { TabPanel: PanelTab, TextInput: PanelTextInput, }, + GridSuggestionMenu: { + Root: GridSuggestionMenu, + Item: GridSuggestionMenuItem, + }, LinkToolbar: { Root: Toolbar, Button: ToolbarButton, diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index 54825e8bbe..f9573ce76a 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -85,6 +85,38 @@ padding-inline: 4px; } +.bn-grid-suggestion-menu { + background: var(--bn-colors-menu-background); + border-radius: var(--bn-border-radius-large); + box-shadow: var(--bn-shadow-medium); + display: grid; + gap: 7px; + grid-template-columns: repeat(10, 1fr); + justify-items: center; + max-height: 30vh; + min-width: 40vw; + overflow-y: scroll; + padding: 20px; +} + +.bn-grid-suggestion-menu-item { + align-items: center; + border-radius: var(--bn-border-radius-large); + cursor: pointer; + display: flex; + font-size: 24px; + height: 32px; + justify-content: center; + margin: 2px; + padding: 4px; + width: 32px; +} + +.bn-grid-suggestion-menu-item[aria-selected="true"], +.bn-grid-suggestion-menu-item:hover { + background-color: var(--bn-colors-hovered-background); +} + .bn-side-menu { align-items: center; display: flex; diff --git a/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx new file mode 100644 index 0000000000..3f492b19f6 --- /dev/null +++ b/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx @@ -0,0 +1,18 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenu = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Root"] +>((props, ref) => { + const { className, children, id, ...rest } = props; + + assertEmpty(rest); + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx new file mode 100644 index 0000000000..0c4b98f80a --- /dev/null +++ b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx @@ -0,0 +1,42 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps, elementOverflow, mergeRefs } from "@blocknote/react"; +import { forwardRef, useEffect, useRef } from "react"; + +export const GridSuggestionMenuItem = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Item"] +>((props, ref) => { + const { className, isSelected, onClick, item, id, ...rest } = props; + + assertEmpty(rest); + + const itemRef = useRef(null); + + useEffect(() => { + if (!itemRef.current || !isSelected) { + return; + } + + const overflow = elementOverflow(itemRef.current); + + if (overflow === "top") { + itemRef.current.scrollIntoView(true); + } else if (overflow === "bottom") { + itemRef.current.scrollIntoView(false); + } + }, [isSelected]); + + return ( +

onItemClick?.(item)} + aria-selected={isSelected || undefined}> + {item.icon} +

+ ); +}); diff --git a/packages/core/package.json b/packages/core/package.json index d2c0e480e1..fcd699bf06 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,6 +54,8 @@ "clean": "rimraf dist && rimraf types" }, "dependencies": { + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", "@tiptap/core": "^2.4.0", "@tiptap/extension-bold": "^2.4.0", "@tiptap/extension-code": "^2.4.0", @@ -74,6 +76,7 @@ "@tiptap/extension-text": "^2.4.0", "@tiptap/extension-underline": "^2.4.0", "@tiptap/pm": "^2.4.0", + "emoji-mart": "^5.6.0", "hast-util-from-dom": "^4.2.0", "prosemirror-model": "^1.21.0", "prosemirror-state": "^1.4.3", @@ -95,6 +98,7 @@ "yjs": "^13.6.15" }, "devDependencies": { + "@types/emoji-mart": "^3.0.14", "@types/hast": "^2.3.4", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", diff --git a/packages/core/src/blocks/defaultBlockTypeGuards.ts b/packages/core/src/blocks/defaultBlockTypeGuards.ts index 09c93cb89e..d16bddd67f 100644 --- a/packages/core/src/blocks/defaultBlockTypeGuards.ts +++ b/packages/core/src/blocks/defaultBlockTypeGuards.ts @@ -6,7 +6,13 @@ import { InlineContentSchema, StyleSchema, } from "../schema"; -import { Block, DefaultBlockSchema, defaultBlockSchema } from "./defaultBlocks"; +import { + Block, + DefaultBlockSchema, + defaultBlockSchema, + defaultInlineContentSchema, + DefaultInlineContentSchema, +} from "./defaultBlocks"; import { defaultProps } from "./defaultProps"; export function checkDefaultBlockTypeInSchema< @@ -23,6 +29,25 @@ export function checkDefaultBlockTypeInSchema< ); } +export function checkDefaultInlineContentTypeInSchema< + InlineContentType extends keyof DefaultInlineContentSchema, + B extends BlockSchema, + S extends StyleSchema +>( + inlineContentType: InlineContentType, + editor: BlockNoteEditor +): editor is BlockNoteEditor< + B, + { Type: DefaultInlineContentSchema[InlineContentType] }, + S +> { + return ( + inlineContentType in editor.schema.inlineContentSchema && + editor.schema.inlineContentSchema[inlineContentType] === + defaultInlineContentSchema[inlineContentType] + ); +} + export function checkBlockIsDefaultType< BlockType extends keyof DefaultBlockSchema, I extends InlineContentSchema, diff --git a/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts b/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts new file mode 100644 index 0000000000..7f90bada89 --- /dev/null +++ b/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts @@ -0,0 +1,3 @@ +export type DefaultGridSuggestionItem = { + id: string; +}; diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts new file mode 100644 index 0000000000..9a95549f33 --- /dev/null +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts @@ -0,0 +1,33 @@ +import data, { Emoji, EmojiMartData } from "@emoji-mart/data"; +import { init, SearchIndex } from "emoji-mart"; + +import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; +import { BlockNoteEditor } from "../../editor/BlockNoteEditor"; +import { checkDefaultInlineContentTypeInSchema } from "../../blocks/defaultBlockTypeGuards"; +import { DefaultGridSuggestionItem } from "./DefaultGridSuggestionItem"; + +const emojiMartData = data as EmojiMartData; +init({ emojiMartData }); + +export async function getDefaultEmojiPickerItems< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + query: string +): Promise<(DefaultGridSuggestionItem & { emoji: string })[]> { + if (!checkDefaultInlineContentTypeInSchema("emoji", editor)) { + return []; + } + + const emojisToShow = + query === "" + ? Object.values(emojiMartData.emojis) + : ((await SearchIndex.search(query)) as Emoji[]); + + return emojisToShow.map((emoji: Emoji) => ({ + id: emoji.id, + emoji: emoji.skins[0].native, + })); +} diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index 0a5dd7c5b9..e244d70870 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -82,8 +82,6 @@ export function getDefaultSlashMenuItems< >(editor: BlockNoteEditor) { const items: DefaultSuggestionItem[] = []; - - if (checkDefaultBlockTypeInSchema("heading", editor)) { items.push( { @@ -288,7 +286,7 @@ export function getDefaultSlashMenuItems< }, key: "emoji", ...editor.dictionary.slash_menu.emoji, - }) + }); return items; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 88b0f6186a..730d5ba8df 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,8 +22,10 @@ export * from "./extensions/FormattingToolbar/FormattingToolbarPlugin"; export * from "./extensions/LinkToolbar/LinkToolbarPlugin"; export * from "./extensions/SideMenu/SideMenuPlugin"; export * from "./extensions/SuggestionMenu/DefaultSuggestionItem"; +export * from "./extensions/SuggestionMenu/DefaultGridSuggestionItem"; export * from "./extensions/SuggestionMenu/SuggestionPlugin"; export * from "./extensions/SuggestionMenu/getDefaultSlashMenuItems"; +export * from "./extensions/SuggestionMenu/getDefaultEmojiPickerItems"; export * from "./extensions/TableHandles/TableHandlesPlugin"; export * from "./schema"; export * from "./util/browser"; diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index 98c567cd6d..da60d2eaa4 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -14,7 +14,8 @@ import { applyBlockNoteCSSVariablesFromTheme, removeBlockNoteCSSVariables, } from "./BlockNoteTheme"; -import { TextInput } from "./form/TextInput"; +import { GridSuggestionMenu } from "./suggestionMenu/GridSuggestionMenu"; +import { GridSuggestionMenuItem } from "./suggestionMenu/GridSuggestionMenuItem"; import { Menu, MenuDivider, @@ -37,6 +38,7 @@ import { SuggestionMenuItem } from "./suggestionMenu/SuggestionMenuItem"; import { SuggestionMenuLabel } from "./suggestionMenu/SuggestionMenuLabel"; import { SuggestionMenuLoader } from "./suggestionMenu/SuggestionMenuLoader"; import { TableHandle } from "./tableHandle/TableHandle"; +import { TextInput } from "./form/TextInput"; import { Toolbar } from "./toolbar/Toolbar"; import { ToolbarButton } from "./toolbar/ToolbarButton"; import { ToolbarSelect } from "./toolbar/ToolbarSelect"; @@ -59,6 +61,10 @@ export const components: Components = { TabPanel: PanelTab, TextInput: PanelTextInput, }, + GridSuggestionMenu: { + Root: GridSuggestionMenu, + Item: GridSuggestionMenuItem, + }, LinkToolbar: { Root: Toolbar, Button: ToolbarButton, diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index c043bb09ab..9e9bbd6b78 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -359,6 +359,38 @@ background-color: var(--bn-colors-side-menu); } +.bn-grid-suggestion-menu { + background: var(--bn-colors-menu-background); + border-radius: var(--bn-border-radius-large); + box-shadow: var(--bn-shadow-medium); + display: grid; + gap: 7px; + grid-template-columns: repeat(10, 1fr); + justify-items: center; + max-height: 30vh; + min-width: 40vw; + overflow-y: scroll; + padding: 20px; +} + +.bn-grid-suggestion-menu-item { + align-items: center; + border-radius: var(--bn-border-radius-large); + cursor: pointer; + display: flex; + font-size: 24px; + height: 32px; + justify-content: center; + margin: 2px; + padding: 4px; + width: 32px; +} + +.bn-grid-suggestion-menu-item[aria-selected="true"], +.bn-grid-suggestion-menu-item:hover { + background-color: var(--bn-colors-hovered-background); +} + /* Side Menu styling */ .bn-side-menu { background-color: transparent; diff --git a/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx new file mode 100644 index 0000000000..3f492b19f6 --- /dev/null +++ b/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx @@ -0,0 +1,18 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenu = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Root"] +>((props, ref) => { + const { className, children, id, ...rest } = props; + + assertEmpty(rest); + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx b/packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx new file mode 100644 index 0000000000..dcb08e0eea --- /dev/null +++ b/packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx @@ -0,0 +1,44 @@ +import { mergeRefs } from "@mantine/hooks"; + +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps, elementOverflow } from "@blocknote/react"; +import { forwardRef, useEffect, useRef } from "react"; + +export const GridSuggestionMenuItem = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Item"] +>((props, ref) => { + const { className, isSelected, onClick, item, id, ...rest } = props; + + assertEmpty(rest); + + const itemRef = useRef(null); + + useEffect(() => { + if (!itemRef.current || !isSelected) { + return; + } + + const overflow = elementOverflow(itemRef.current); + + if (overflow === "top") { + itemRef.current.scrollIntoView(true); + } else if (overflow === "bottom") { + itemRef.current.scrollIntoView(false); + } + }, [isSelected]); + + return ( +

onItemClick?.(item)} + aria-selected={isSelected || undefined}> + {item.icon} +

+ ); +}); diff --git a/packages/react/package.json b/packages/react/package.json index 1cfeaa0217..e6cc38d9a4 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -59,10 +59,7 @@ "react": "^18", "react-dom": "^18", "react-icons": "^5.2.1", - "use-prefers-color-scheme": "^1.1.3", - "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", - "emoji-mart": "^5.6.0" + "use-prefers-color-scheme": "^1.1.3" }, "devDependencies": { "@types/lodash.foreach": "^4.5.9", diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx new file mode 100644 index 0000000000..68c8cb3747 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx @@ -0,0 +1,75 @@ +import { useMemo } from "react"; + +import { useComponentsContext } from "../../editor/ComponentsContext"; +// import { useDictionary } from "../../i18n/dictionary"; +import { DefaultReactGridSuggestionItem, SuggestionMenuProps } from "./types"; + +export default function GridSuggestionMenu< + T extends DefaultReactGridSuggestionItem +>(props: SuggestionMenuProps): JSX.Element { + const Components = useComponentsContext()!; + // const dict = useDictionary(); + + const { + items, + // loadingState, + selectedIndex, + onItemClick, + } = props; + + // const loader = + // loadingState === "loading-initial" || loadingState === "loading" ? ( + // + // {dict.suggestion_menu.loading} + // + // ) : null; + + const renderedItems = useMemo(() => { + // let currentGroup: string | undefined = undefined; + const renderedItems = []; + + for (let i = 0; i < items.length; i++) { + const item = items[i]; + // if (item.group !== currentGroup) { + // currentGroup = item.group; + // renderedItems.push( + // + // {currentGroup} + // + // ); + // } + + renderedItems.push( + onItemClick?.(item)} + /> + ); + } + + return renderedItems; + }, [Components, items, onItemClick, selectedIndex]); + + return ( + + {renderedItems} + {/*{renderedItems.length === 0 &&*/} + {/* (props.loadingState === "loading" ||*/} + {/* props.loadingState === "loaded") && (*/} + {/* */} + {/* {dict.suggestion_menu.no_items_title}*/} + {/* */} + {/* )}*/} + {/*{loader}*/} + + ); +} diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenu.tsx index cff67da17f..b5750e825a 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenu.tsx @@ -1,4 +1,5 @@ import { useMemo } from "react"; + import { useComponentsContext } from "../../editor/ComponentsContext"; import { useDictionary } from "../../i18n/dictionary"; import { DefaultReactSuggestionItem, SuggestionMenuProps } from "./types"; diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index 20427f21b3..e2a9425499 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -4,7 +4,6 @@ import { FC, useCallback, useEffect } from "react"; import { useBlockNoteContext } from "../../editor/BlockNoteContext"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor"; import { useCloseSuggestionMenuNoItems } from "./hooks/useCloseSuggestionMenuNoItems"; -import { useGridSuggestionMenuKeyboardNavigation } from "./hooks/useGridSuggestionMenuKeyboardNavigation"; import { useLoadSuggestionMenuItems } from "./hooks/useLoadSuggestionMenuItems"; import { useSuggestionMenuKeyboardNavigation } from "./hooks/useSuggestionMenuKeyboardNavigation"; import { SuggestionMenuProps } from "./types"; @@ -52,14 +51,11 @@ export function SuggestionMenuWrapper(props: { useCloseSuggestionMenuNoItems(items, usedQuery, closeMenu); - const useKeyboardNavigation = grid - ? useGridSuggestionMenuKeyboardNavigation - : useSuggestionMenuKeyboardNavigation; - - const { selectedIndex } = useKeyboardNavigation( + const { selectedIndex } = useSuggestionMenuKeyboardNavigation( editor, query, items, + grid, onItemClickCloseMenu ); diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx new file mode 100644 index 0000000000..62a7e563b4 --- /dev/null +++ b/packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx @@ -0,0 +1,21 @@ +import { + BlockNoteEditor, + BlockSchema, + getDefaultEmojiPickerItems, + InlineContentSchema, + StyleSchema, +} from "@blocknote/core"; +import { DefaultReactGridSuggestionItem } from "./types"; + +export async function getDefaultReactEmojiPickerItems< + BSchema extends BlockSchema, + I extends InlineContentSchema, + S extends StyleSchema +>( + editor: BlockNoteEditor, + query: string +): Promise<(DefaultReactGridSuggestionItem & { emoji: string })[]> { + return (await getDefaultEmojiPickerItems(editor, query)).map( + ({ id, emoji }) => ({ id, emoji, icon: emoji as any }) + ); +} diff --git a/packages/react/src/components/SuggestionMenu/gridSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/gridSuggestionMenu.tsx deleted file mode 100644 index 87e49f3a25..0000000000 --- a/packages/react/src/components/SuggestionMenu/gridSuggestionMenu.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { SuggestionMenuProps } from "./types"; - -export default function GridSuggestionMenu( - props: SuggestionMenuProps -): JSX.Element { - //this is the component which renders emoji picker with the emojis searched. - const { items, selectedIndex, onItemClick } = props; - - return ( -
- {items.map((item, index) => ( -

onItemClick?.(item)}> - {item} -

- ))} -
- ); -} diff --git a/packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts deleted file mode 100644 index 5e62588833..0000000000 --- a/packages/react/src/components/SuggestionMenu/hooks/useGridSuggestionMenuKeyboardNavigation.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { BlockNoteEditor } from "@blocknote/core"; -import { useEffect, useState } from "react"; - -// Hook which handles keyboard navigation of a suggestion menu. Arrow keys are -// used to select a menu item, enter to execute it. This version uses the left -// & right arrow keys in addition to up & down to navigate a grid. -export function useGridSuggestionMenuKeyboardNavigation( - editor: BlockNoteEditor, - query: string, - items: Item[], - onItemClick?: ((item: Item) => void) | ((item: never) => void) -) { - const [selectedIndex, setSelectedIndex] = useState(0); - - useEffect(() => { - const handleMenuNavigationKeys = (event: KeyboardEvent) => { - if (event.key === "ArrowUp") { - event.preventDefault(); - - if (items.length) { - setSelectedIndex(selectedIndex - 10 >= 0 ? selectedIndex - 10 : 0); - } - - return true; - } - - if (event.key === "ArrowDown") { - event.preventDefault(); - - if (items.length) { - setSelectedIndex( - selectedIndex + 10 < items!.length - ? selectedIndex + 10 - : items!.length - 1 - ); - } - - return true; - } - - if (event.key === "ArrowRight") { - event.preventDefault(); - if (items.length) { - setSelectedIndex((selectedIndex + 1 + items!.length) % items!.length); - } - } - - if (event.key === "ArrowLeft") { - event.preventDefault(); - if (items.length) { - setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); - } - } - - if (event.key === "Enter") { - event.preventDefault(); - - if (items.length) { - onItemClick?.(items[selectedIndex] as never); - } - - return true; - } - - return false; - }; - - editor.domElement.addEventListener( - "keydown", - handleMenuNavigationKeys, - true - ); - - return () => { - editor.domElement.removeEventListener( - "keydown", - handleMenuNavigationKeys, - true - ); - }; - }, [editor.domElement, items, selectedIndex, onItemClick]); - - // Resets index when items change - useEffect(() => { - setSelectedIndex(0); - }, [query]); - - return { - selectedIndex: items.length === 0 ? undefined : selectedIndex, - }; -} diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index 0306914c5e..47ccb97c34 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -1,23 +1,47 @@ import { BlockNoteEditor } from "@blocknote/core"; import { useEffect, useState } from "react"; -// Hook which handles keyboard navigation of a suggestion menu. Arrow keys are -// used to select a menu item, enter to execute it +// Hook which handles keyboard navigation of a suggestion menu. Up & down arrow +// keys are used to select a menu item, enter is used to execute it. Can also +// use the left & right arrow keys in addition to up & down to navigate a grid. export function useSuggestionMenuKeyboardNavigation( editor: BlockNoteEditor, query: string, items: Item[], + grid = false, onItemClick?: (item: Item) => void ) { const [selectedIndex, setSelectedIndex] = useState(0); useEffect(() => { const handleMenuNavigationKeys = (event: KeyboardEvent) => { + if (grid) { + if (event.key === "ArrowRight") { + event.preventDefault(); + if (items.length) { + setSelectedIndex( + (selectedIndex + 1 + items!.length) % items!.length + ); + } + } + + if (event.key === "ArrowLeft") { + event.preventDefault(); + if (items.length) { + setSelectedIndex( + (selectedIndex - 1 + items!.length) % items!.length + ); + } + } + } + if (event.key === "ArrowUp") { event.preventDefault(); if (items.length) { - setSelectedIndex((selectedIndex - 1 + items!.length) % items!.length); + setSelectedIndex( + (selectedIndex - (grid ? 10 : 1) + items!.length) % items!.length + ); } return true; @@ -27,7 +51,7 @@ export function useSuggestionMenuKeyboardNavigation( event.preventDefault(); if (items.length) { - setSelectedIndex((selectedIndex + 1) % items!.length); + setSelectedIndex((selectedIndex + (grid ? 10 : 1)) % items!.length); } return true; @@ -59,7 +83,7 @@ export function useSuggestionMenuKeyboardNavigation( true ); }; - }, [editor.domElement, items, selectedIndex, onItemClick]); + }, [editor.domElement, items, selectedIndex, onItemClick, grid]); // Resets index when items change useEffect(() => { diff --git a/packages/react/src/components/SuggestionMenu/types.tsx b/packages/react/src/components/SuggestionMenu/types.tsx index ddbbfa5b4f..e9aa3e6ad5 100644 --- a/packages/react/src/components/SuggestionMenu/types.tsx +++ b/packages/react/src/components/SuggestionMenu/types.tsx @@ -1,4 +1,7 @@ -import { DefaultSuggestionItem } from "@blocknote/core"; +import { + DefaultGridSuggestionItem, + DefaultSuggestionItem, +} from "@blocknote/core"; /** * Although any arbitrary data can be passed as suggestion items, the built-in @@ -7,6 +10,9 @@ import { DefaultSuggestionItem } from "@blocknote/core"; export type DefaultReactSuggestionItem = Omit & { icon?: JSX.Element; }; +export type DefaultReactGridSuggestionItem = DefaultGridSuggestionItem & { + icon?: JSX.Element; +}; /** * Props passed to a suggestion menu component @@ -18,5 +24,5 @@ export type SuggestionMenuProps = { loadingState: "loading-initial" | "loading" | "loaded"; selectedIndex: number | undefined; onItemClick?: (item: T) => void; - emojiInsert?: (emoji: never) => void + emojiInsert?: (emoji: never) => void; }; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 57dbffd3d0..764f7a8326 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -1,12 +1,14 @@ +import { useCallback } from "react"; + import { FormattingToolbarController } from "../components/FormattingToolbar/FormattingToolbarController"; import { LinkToolbarController } from "../components/LinkToolbar/LinkToolbarController"; import { FilePanelController } from "../components/FilePanel/FilePanelController"; import { SideMenuController } from "../components/SideMenu/SideMenuController"; import { SuggestionMenuController } from "../components/SuggestionMenu/SuggestionMenuController"; -import GridSuggestionMenu from "../components/SuggestionMenu/gridSuggestionMenu.js"; +import GridSuggestionMenu from "../components/SuggestionMenu/GridSuggestionMenu"; +import { getDefaultReactEmojiPickerItems } from "../components/SuggestionMenu/getDefaultReactEmojiPickerItems"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; -import { Data, SearchIndex, init } from "emoji-mart"; export type BlockNoteDefaultUIProps = { formattingToolbar?: boolean; @@ -15,32 +17,11 @@ export type BlockNoteDefaultUIProps = { sideMenu?: boolean; filePanel?: boolean; tableHandles?: boolean; - emojiPicker?: boolean + emojiPicker?: boolean; }; export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { - //this makes sure emjois aren't fetched at every render - init({ Data }); - - async function search(value: string) { - if (value === "") { - //Don't do unnecessary linear search if no search query - return Object.values(Data.emojis).map( - (emoji: any) => emoji.skins[0].native - ); - } - //begin the linear search - const emojisToShow = await SearchIndex.search(value); - - //return the emojis - return emojisToShow.map((emoji: any) => emoji.skins[0].native); - } - const editor = useBlockNoteEditor(); - async function emojiChangeHandler(query: string) { - //STEP 2: call the search function, which returns an array of emojis as items for the component - return search(query); - } if (!editor) { throw new Error( @@ -48,36 +29,41 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { ); } + const getEmojiPickerItems = useCallback( + (query: string) => getDefaultReactEmojiPickerItems(editor, query), + [editor] + ); + const emojiPickerOnItemClick = useCallback( + (item: { id: string; emoji: string }) => { + editor.insertInlineContent([ + { + type: "emoji", + props: { + emoji: item.emoji, + }, + }, + " ", + ]); + }, + [editor] + ); + return ( <> {props.formattingToolbar !== false && } {props.linkToolbar !== false && } {props.slashMenu !== false && ( - + )} - { - props.emojiPicker !== false && ( - - { - editor.insertInlineContent([ - { - type: 'emoji', - props: { - emoji: item as any - } - }, " " - ]) - - } - } + getItems={getEmojiPickerItems} + onItemClick={emojiPickerOnItemClick} /> - ) - } + )} {props.sideMenu !== false && } {editor.filePanel && props.filePanel !== false && } {editor.tableHandles && props.tableHandles !== false && ( diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index d6ce638f11..fddd033fb9 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -9,7 +9,10 @@ import { useContext, } from "react"; -import { DefaultReactSuggestionItem } from "../components/SuggestionMenu/types"; +import { + DefaultReactGridSuggestionItem, + DefaultReactSuggestionItem, +} from "../components/SuggestionMenu/types"; export type ComponentProps = { FormattingToolbar: { @@ -142,6 +145,32 @@ export type ComponentProps = { children?: ReactNode; }; }; + GridSuggestionMenu: { + Root: { + id: string; + className?: string; + children?: ReactNode; + }; + // EmptyItem: { + // className?: string; + // children?: ReactNode; + // }; + Item: { + className?: string; + id: string; + isSelected: boolean; + onClick: () => void; + item: DefaultReactGridSuggestionItem; + }; + // Label: { + // className?: string; + // children?: ReactNode; + // }; + // Loader: { + // className?: string; + // children?: ReactNode; + // }; + }; TableHandle: { Root: { className?: string; diff --git a/packages/react/src/editor/styles.css b/packages/react/src/editor/styles.css index b16f893820..9afd24dff7 100644 --- a/packages/react/src/editor/styles.css +++ b/packages/react/src/editor/styles.css @@ -239,30 +239,3 @@ .bn-side-menu[data-url="false"] { height: 54px; } - -/* styles for the emoji menu */ -.bn-grid-suggestion-menu{ - display: grid; - justify-items: center; - gap: 7; - padding: 20; - max-height: 30vh; - min-width: 40vw; - overflow-y: scroll; - border-radius: var(--bn-border-radius-large); - background: var(--bn-colors-menu-background); - box-shadow: var(--bn-shadow-medium); -} - -.bn-grid-suggestion-menu p{ - border-radius: var(--bn-border-radius-large); - padding: 4px; - margin: 2px; - font-size: larger; - cursor: pointer; -} - -.grid-item-selected{ - background: var(--bn-colors-selected-background); -} - diff --git a/packages/shadcn/src/index.tsx b/packages/shadcn/src/index.tsx index b3ddc673e4..a751459e33 100644 --- a/packages/shadcn/src/index.tsx +++ b/packages/shadcn/src/index.tsx @@ -12,6 +12,8 @@ import { ShadCNDefaultComponents, } from "./ShadCNComponentsContext"; import { Form } from "./form/Form"; +import { GridSuggestionMenu } from "@/suggestionMenu/GridSuggestionMenu"; +import { GridSuggestionMenuItem } from "@/suggestionMenu/GridSuggestionMenuItem"; import { Menu, MenuDivider, @@ -67,6 +69,10 @@ export const components: Components = { Label: SuggestionMenuLabel, Loader: SuggestionMenuLoader, }, + GridSuggestionMenu: { + Root: GridSuggestionMenu, + Item: GridSuggestionMenuItem, + }, TableHandle: { Root: TableHandle, }, diff --git a/packages/shadcn/src/style.css b/packages/shadcn/src/style.css index 386e523ec1..24887eade4 100644 --- a/packages/shadcn/src/style.css +++ b/packages/shadcn/src/style.css @@ -108,3 +108,35 @@ .bn-suggestion-menu-item:hover { background-color: hsl(var(--accent)); } + +.bn-grid-suggestion-menu { + background: var(--bn-colors-menu-background); + border-radius: var(--bn-border-radius-large); + box-shadow: var(--bn-shadow-medium); + display: grid; + gap: 7px; + grid-template-columns: repeat(10, 1fr); + justify-items: center; + max-height: 30vh; + min-width: 40vw; + overflow-y: scroll; + padding: 20px; +} + +.bn-grid-suggestion-menu-item { + align-items: center; + border-radius: var(--bn-border-radius-large); + cursor: pointer; + display: flex; + font-size: 24px; + height: 32px; + justify-content: center; + margin: 2px; + padding: 4px; + width: 32px; +} + +.bn-grid-suggestion-menu-item[aria-selected="true"], +.bn-grid-suggestion-menu-item:hover { + background-color: var(--bn-colors-hovered-background); +} diff --git a/packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx new file mode 100644 index 0000000000..3f492b19f6 --- /dev/null +++ b/packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx @@ -0,0 +1,18 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenu = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Root"] +>((props, ref) => { + const { className, children, id, ...rest } = props; + + assertEmpty(rest); + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx new file mode 100644 index 0000000000..0c4b98f80a --- /dev/null +++ b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx @@ -0,0 +1,42 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps, elementOverflow, mergeRefs } from "@blocknote/react"; +import { forwardRef, useEffect, useRef } from "react"; + +export const GridSuggestionMenuItem = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Item"] +>((props, ref) => { + const { className, isSelected, onClick, item, id, ...rest } = props; + + assertEmpty(rest); + + const itemRef = useRef(null); + + useEffect(() => { + if (!itemRef.current || !isSelected) { + return; + } + + const overflow = elementOverflow(itemRef.current); + + if (overflow === "top") { + itemRef.current.scrollIntoView(true); + } else if (overflow === "bottom") { + itemRef.current.scrollIntoView(false); + } + }, [isSelected]); + + return ( +

onItemClick?.(item)} + aria-selected={isSelected || undefined}> + {item.icon} +

+ ); +}); From da5ee450dd4b223b4ca1435516ced903f5a9c2e0 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 2 Jul 2024 23:22:35 +0200 Subject: [PATCH 21/41] Updated package lock --- package-lock.json | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/package-lock.json b/package-lock.json index adb6448abe..777cf78a19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2595,6 +2595,20 @@ "node": ">=10.0.0" } }, + "node_modules/@emoji-mart/data": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", + "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==" + }, + "node_modules/@emoji-mart/react": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz", + "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==", + "peerDependencies": { + "emoji-mart": "^5.2", + "react": "^16.8 || ^17 || ^18" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -7754,6 +7768,15 @@ "@types/ms": "*" } }, + "node_modules/@types/emoji-mart": { + "version": "3.0.14", + "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.14.tgz", + "integrity": "sha512-/vMkVnet466bK37ugf2jry9ldCZklFPXYMB2m+qNo3vkP2I7L0cvtNFPKAjfcHgPg9Z8pbYqVqZn7AgsC0qf+g==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -11545,6 +11568,11 @@ "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==" }, + "node_modules/emoji-mart": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", + "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -27095,6 +27123,8 @@ "version": "0.14.2", "license": "MPL-2.0", "dependencies": { + "@emoji-mart/data": "^1.2.1", + "@emoji-mart/react": "^1.1.1", "@tiptap/core": "^2.4.0", "@tiptap/extension-bold": "^2.4.0", "@tiptap/extension-code": "^2.4.0", @@ -27115,6 +27145,7 @@ "@tiptap/extension-text": "^2.4.0", "@tiptap/extension-underline": "^2.4.0", "@tiptap/pm": "^2.4.0", + "emoji-mart": "^5.6.0", "hast-util-from-dom": "^4.2.0", "prosemirror-model": "^1.21.0", "prosemirror-state": "^1.4.3", @@ -27136,6 +27167,7 @@ "yjs": "^13.6.15" }, "devDependencies": { + "@types/emoji-mart": "^3.0.14", "@types/hast": "^2.3.4", "@types/uuid": "^8.3.4", "eslint": "^8.10.0", From 92358e2bf5d018cb4ebbfdea9a2177df6b9a0115 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 2 Jul 2024 23:47:24 +0200 Subject: [PATCH 22/41] Small fixes --- examples/01-basic/01-minimal/App.tsx | 3 +- packages/core/src/blocks/defaultBlocks.ts | 4 +- .../SuggestionMenu/SuggestionPlugin.ts | 35 +- packages/core/src/i18n/locales/fr.ts | 1 - packages/core/src/i18n/locales/is.ts | 2 +- packages/core/src/i18n/locales/ja.ts | 1 - packages/core/src/i18n/locales/ko.ts | 13 +- packages/core/src/i18n/locales/nl.ts | 8 +- packages/core/src/i18n/locales/pl.ts | 1 - packages/core/src/i18n/locales/pt.ts | 1 - packages/core/src/i18n/locales/ru.ts | 631 +++++++++--------- packages/core/src/i18n/locales/vi.ts | 10 +- packages/core/src/i18n/locales/zh.ts | 11 +- .../emojiInlineContent/Emoji.tsx | 3 - packages/mantine/src/style.css | 8 +- .../getDefaultReactSlashMenuItems.tsx | 4 +- packages/shadcn/src/style.css | 8 +- 17 files changed, 377 insertions(+), 367 deletions(-) diff --git a/examples/01-basic/01-minimal/App.tsx b/examples/01-basic/01-minimal/App.tsx index c3a40dc15d..c545b7b4dd 100644 --- a/examples/01-basic/01-minimal/App.tsx +++ b/examples/01-basic/01-minimal/App.tsx @@ -4,7 +4,8 @@ import { BlockNoteView } from "@blocknote/mantine"; import "@blocknote/mantine/style.css"; export default function App() { -const editor = useCreateBlockNote() + // Creates a new editor instance. + const editor = useCreateBlockNote(); // Renders the editor instance using a React component. return ; diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index 595ecbff44..e91a0845e0 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -20,6 +20,8 @@ import { getStyleSchemaFromSpecs, } from "../schema"; +import { Emoji } from "../inlineContent/emojiInlineContent/Emoji"; + import { Heading } from "./HeadingBlockContent/HeadingBlockContent"; import { BulletListItem } from "./ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; import { NumberedListItem } from "./ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; @@ -30,7 +32,6 @@ import { FileBlock } from "./FileBlockContent/FileBlockContent"; import { ImageBlock } from "./ImageBlockContent/ImageBlockContent"; import { VideoBlock } from "./VideoBlockContent/VideoBlockContent"; import { AudioBlock } from "./AudioBlockContent/AudioBlockContent"; -import { Emoji } from "../inlineContent/emojiInlineContent/Emoji"; export const defaultBlockSpecs = { paragraph: Paragraph, @@ -72,7 +73,6 @@ export type DefaultStyleSchema = _DefaultStyleSchema; export const defaultInlineContentSpecs = { text: { config: "text", implementation: {} as any }, link: { config: "link", implementation: {} as any }, - //for rendering emojis inline emoji: Emoji, } satisfies InlineContentSpecs; diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index 2c2a3aa4c1..d64bd83984 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -131,12 +131,12 @@ class SuggestionMenuView< type SuggestionPluginState = | { - triggerCharacter: string; - fromUserInput: boolean; - queryStartPos: number; - query: string; - decorationId: string; - } + triggerCharacter: string; + fromUserInput: boolean; + queryStartPos: number; + query: string; + decorationId: string; + } | undefined; export const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin"); @@ -250,27 +250,6 @@ export class SuggestionMenuProseMirrorPlugin< }, props: { - //STEP 6: the final thing is to listen to the simulated keydown event generated by the onClick of emoji option in slash menu - handleKeyDown(view, event) { - - if (event.key === ':') { - //only proceed if it was the trigger character (:) - view.dispatch( - view.state.tr - .insertText(':') - .scrollIntoView() - .setMeta(suggestionMenuPluginKey, { - triggerCharacter: ':', - alreadyShownMenu: true - //and this finally opens the emojiMenu thru slashmenu option - }) - ); - - return true; - } - return; - }, - handleTextInput(view, _from, _to, text) { const suggestionPluginState: SuggestionPluginState = ( this as Plugin @@ -326,7 +305,7 @@ export class SuggestionMenuProseMirrorPlugin< return DecorationSet.create(state.doc, [ Decoration.inline( suggestionPluginState.queryStartPos! - - suggestionPluginState.triggerCharacter!.length, + suggestionPluginState.triggerCharacter!.length, suggestionPluginState.queryStartPos!, { nodeName: "span", diff --git a/packages/core/src/i18n/locales/fr.ts b/packages/core/src/i18n/locales/fr.ts index e458540274..bd9e073aa0 100644 --- a/packages/core/src/i18n/locales/fr.ts +++ b/packages/core/src/i18n/locales/fr.ts @@ -111,7 +111,6 @@ export const fr: Dictionary = { aliases: ["emoji", "émoticône", "émotion", "visage"], group: "Autres", }, - }, placeholders: { default: "Entrez du texte ou tapez '/' pour les commandes", diff --git a/packages/core/src/i18n/locales/is.ts b/packages/core/src/i18n/locales/is.ts index f37697e886..4b4ee544d7 100644 --- a/packages/core/src/i18n/locales/is.ts +++ b/packages/core/src/i18n/locales/is.ts @@ -103,7 +103,7 @@ export const is: Dictionary = { subtext: "Notað til að setja inn smámynd", aliases: ["emoji", "andlitsávísun", "tilfinningar", "andlit"], group: "Annað", - }, + }, }, placeholders: { default: "Sláðu inn texta eða skrifaðu '/' fyrir skipanir", diff --git a/packages/core/src/i18n/locales/ja.ts b/packages/core/src/i18n/locales/ja.ts index eae375a08a..91c7d983de 100644 --- a/packages/core/src/i18n/locales/ja.ts +++ b/packages/core/src/i18n/locales/ja.ts @@ -131,7 +131,6 @@ export const ja: Dictionary = { aliases: ["絵文字", "顔文字", "感情表現", "顔"], group: "その他", }, - }, placeholders: { default: "テキストを入力するか'/' を入力してコマンド選択", diff --git a/packages/core/src/i18n/locales/ko.ts b/packages/core/src/i18n/locales/ko.ts index 91d71781e6..9c7f6bf92a 100644 --- a/packages/core/src/i18n/locales/ko.ts +++ b/packages/core/src/i18n/locales/ko.ts @@ -112,11 +112,18 @@ export const ko: Dictionary = { emoji: { title: "이모지", subtext: "이모지 삽입용으로 사용됩니다", - aliases: ["이모지", "emoji", "감정 표현", "emotion expression", "표정", "face expression", "얼굴", "face"], + aliases: [ + "이모지", + "emoji", + "감정 표현", + "emotion expression", + "표정", + "face expression", + "얼굴", + "face", + ], group: "기타", }, - - }, placeholders: { default: "텍스트를 입력하거나 /를 입력하여 명령을 입력하세요.", diff --git a/packages/core/src/i18n/locales/nl.ts b/packages/core/src/i18n/locales/nl.ts index ebe4cb5427..782645c1a1 100644 --- a/packages/core/src/i18n/locales/nl.ts +++ b/packages/core/src/i18n/locales/nl.ts @@ -103,10 +103,14 @@ export const nl: Dictionary = { emoji: { title: "Emoji", subtext: "Gebruikt voor het invoegen van een emoji", - aliases: ["emoji", "emotie-uitdrukking", "gezichtsuitdrukking", "gezicht"], + aliases: [ + "emoji", + "emotie-uitdrukking", + "gezichtsuitdrukking", + "gezicht", + ], group: "Overig", }, - }, placeholders: { default: "Voer tekst in of type '/' voor commando's", diff --git a/packages/core/src/i18n/locales/pl.ts b/packages/core/src/i18n/locales/pl.ts index 5763da4876..89aedc0a54 100644 --- a/packages/core/src/i18n/locales/pl.ts +++ b/packages/core/src/i18n/locales/pl.ts @@ -96,7 +96,6 @@ export const pl: Dictionary = { aliases: ["emoji", "emotka", "wyrażenie emocji", "twarz"], group: "Inne", }, - }, placeholders: { default: "Wprowadź tekst lub wpisz '/' aby użyć poleceń", diff --git a/packages/core/src/i18n/locales/pt.ts b/packages/core/src/i18n/locales/pt.ts index 212df838e6..6ee76d1666 100644 --- a/packages/core/src/i18n/locales/pt.ts +++ b/packages/core/src/i18n/locales/pt.ts @@ -103,7 +103,6 @@ export const pt: Dictionary = { aliases: ["emoji", "emoticon", "expressão emocional", "rosto"], group: "Outros", }, - }, placeholders: { default: "Digite texto ou use '/' para comandos", diff --git a/packages/core/src/i18n/locales/ru.ts b/packages/core/src/i18n/locales/ru.ts index d1ae8d9fb9..51a8f2653f 100644 --- a/packages/core/src/i18n/locales/ru.ts +++ b/packages/core/src/i18n/locales/ru.ts @@ -1,323 +1,336 @@ import { Dictionary } from "../dictionary"; - export const ru: Dictionary = { - slash_menu: { - heading: { - title: "Заголовок 1 уровня", - subtext: "Используется для заголовка верхнего уровня", - aliases: ["h", "heading1", "h1", "заголовок1"], - group: "Заголовки", - }, - heading_2: { - title: "Заголовок 2 уровня", - subtext: "Используется для ключевых разделов", - aliases: ["h2", "heading2", "subheading", "заголовок2", "подзаголовок"], - group: "Заголовки", - }, - heading_3: { - title: "Заголовок 3 уровня", - subtext: "Используется для подразделов и групп", - aliases: ["h3", "heading3", "subheading", "заголовок3", "подзаголовок"], - group: "Заголовки", - }, - numbered_list: { - title: "Нумерованный список", - subtext: "Используется для отображения нумерованного списка", - aliases: [ - "ol", - "li", - "list", - "numberedlist", - "numbered list", - "список", - "нумерованный список", - ], - group: "Базовые блоки", - }, - bullet_list: { - title: "Маркированный список", - subtext: "Для отображения неупорядоченного списка.", - aliases: ["ul", "li", "list", "bulletlist", "bullet list", "список", "маркированный список"], - group: "Базовые блоки", - }, - check_list: { - title: "Контрольный список", - subtext: "Для отображения списка с флажками", - aliases: [ - "ul", - "li", - "list", - "checklist", - "check list", - "checked list", - "checkbox", - "список", - ], - group: "Базовые блоки", - }, - paragraph: { - title: "Параграф", - subtext: "Основной текст", - aliases: ["p", "paragraph", "параграф"], - group: "Базовые блоки", - }, - table: { - title: "Таблица", - subtext: "Используется для таблиц", - aliases: ["table", "таблица"], - group: "Продвинутый", - }, - image: { - title: "Картинка", - subtext: "Вставить изображение", - aliases: [ - "image", - "imageUpload", - "upload", - "img", - "picture", - "media", - "url", - "загрузка", - "картинка", - "рисунок", - ], - group: "Медиа", - }, - video: { - title: "Видео", - subtext: "Вставить видео", - aliases: [ - "video", - "videoUpload", - "upload", - "mp4", - "film", - "media", - "url", - "загрузка", - "видео", - ], - group: "Медиа", - }, - audio: { - title: "Аудио", - subtext: "Вставить аудио", - aliases: [ - "audio", - "audioUpload", - "upload", - "mp3", - "sound", - "media", - "url", - "загрузка", - "аудио", - "звук", - "музыка", - ], - group: "Медиа", - }, - file: { - title: "Файл", - subtext: "Вставить файл", - aliases: ["file", "upload", "embed", "media", "url", "загрузка", "файл"], - group: "Медиа", - }, +export const ru: Dictionary = { + slash_menu: { + heading: { + title: "Заголовок 1 уровня", + subtext: "Используется для заголовка верхнего уровня", + aliases: ["h", "heading1", "h1", "заголовок1"], + group: "Заголовки", }, - placeholders: { - default: "Ведите текст или введите «/» для команд", - heading: "Заголовок", - bulletListItem: "Список", - numberedListItem: "Список", - checkListItem: "Список", + heading_2: { + title: "Заголовок 2 уровня", + subtext: "Используется для ключевых разделов", + aliases: ["h2", "heading2", "subheading", "заголовок2", "подзаголовок"], + group: "Заголовки", }, - file_blocks: { - image: { - add_button_text: "Добавить изображение", - }, - video: { - add_button_text: "Добавить видео", - }, - audio: { - add_button_text: "Добавить аудио", - }, - file: { - add_button_text: "Добавить файл", - }, + heading_3: { + title: "Заголовок 3 уровня", + subtext: "Используется для подразделов и групп", + aliases: ["h3", "heading3", "subheading", "заголовок3", "подзаголовок"], + group: "Заголовки", }, - // from react package: - side_menu: { - add_block_label: "Добавить блок", - drag_handle_label: "Открыть меню блока", - }, - drag_handle: { - delete_menuitem: "Удалить", - colors_menuitem: "Цвета", - }, - table_handle: { - delete_column_menuitem: "Удалить столбец", - delete_row_menuitem: "Удалить строку", - add_left_menuitem: "Добавить столбец слева", - add_right_menuitem: "Добавить столбец справа", - add_above_menuitem: "Добавить строку выше", - add_below_menuitem: "Добавить строку ниже", - }, - suggestion_menu: { - no_items_title: "ничего не найдено", - loading: "Загрузка…", - }, - color_picker: { - text_title: "Текст", - background_title: "Задний фон", - colors: { - default: "По умолчинию", - gray: "Серый", - brown: "Коричневый", - red: "Красный", - orange: "Оранжевый", - yellow: "Жёлтый", - green: "Зелёный", - blue: "Голубой", - purple: "Фиолетовый", - pink: "Розовый", - }, + numbered_list: { + title: "Нумерованный список", + subtext: "Используется для отображения нумерованного списка", + aliases: [ + "ol", + "li", + "list", + "numberedlist", + "numbered list", + "список", + "нумерованный список", + ], + group: "Базовые блоки", }, - - formatting_toolbar: { - bold: { - tooltip: "Жирный", - secondary_tooltip: "Mod+B", - }, - italic: { - tooltip: "Курсив", - secondary_tooltip: "Mod+I", - }, - underline: { - tooltip: "Подчёркнутый", - secondary_tooltip: "Mod+U", - }, - strike: { - tooltip: "Зачёркнутый", - secondary_tooltip: "Mod+Shift+X", - }, - code: { - tooltip: "Код", - secondary_tooltip: "", - }, - colors: { - tooltip: "Цвета", - }, - link: { - tooltip: "Создать ссылку", - secondary_tooltip: "Mod+K", - }, - file_caption: { - tooltip: "Изменить подпись", - input_placeholder: "Изменить подпись", - }, - file_replace: { - tooltip: { - image: "Заменить изображение", - video: "Заменить видео", - audio: "Заменить аудио", - file: "Заменить файл", - }, - }, - file_rename: { - tooltip: { - image: "Переименовать изображение", - video: "Переименовать видео", - audio: "Переименовать аудио", - file: "Переименовать файл", - }, - input_placeholder: { - image: "Переименовать изображение", - video: "Переименовать видео", - audio: "Переименовать аудио", - file: "Переименовать файл", - }, - }, - file_download: { - tooltip: { - image: "Скачать картинку", - video: "Скачать видео", - audio: "Скачать аудио", - file: "Скачать файл", - }, - }, - file_delete: { - tooltip: { - image: "Удалить картинку", - video: "Удалить видео", - audio: "Удалить аудио", - file: "Скачать файл", - }, - }, - file_preview_toggle: { - tooltip: "Переключить предварительный просмотр", - }, - nest: { - tooltip: "Сдвинуть вправо", - secondary_tooltip: "Tab", - }, - unnest: { - tooltip: "Сдвинуть влево", - secondary_tooltip: "Shift+Tab", - }, - align_left: { - tooltip: "Текст по левому краю", - }, - align_center: { - tooltip: "Текст по середине", - }, - align_right: { - tooltip: "Текст по правому краю", - }, - align_justify: { - tooltip: "По середине текст", - }, + bullet_list: { + title: "Маркированный список", + subtext: "Для отображения неупорядоченного списка.", + aliases: [ + "ul", + "li", + "list", + "bulletlist", + "bullet list", + "список", + "маркированный список", + ], + group: "Базовые блоки", }, - file_panel: { - upload: { - title: "Загрузить", - file_placeholder: { - image: "Загрузить картинки", - video: "Загрузить видео", - audio: "Загрузить аудио", - file: "Загрузить файл", - }, - upload_error: "Ошибка: не удалось загрузить", - }, - embed: { - title: "Вставить", - embed_button: { - image: "Вставить картинку", - video: "Вставить видео", - audio: "Вставить аудио", - file: "Вставить файл", - }, - url_placeholder: "Введите URL", - }, + check_list: { + title: "Контрольный список", + subtext: "Для отображения списка с флажками", + aliases: [ + "ul", + "li", + "list", + "checklist", + "check list", + "checked list", + "checkbox", + "список", + ], + group: "Базовые блоки", + }, + paragraph: { + title: "Параграф", + subtext: "Основной текст", + aliases: ["p", "paragraph", "параграф"], + group: "Базовые блоки", + }, + table: { + title: "Таблица", + subtext: "Используется для таблиц", + aliases: ["table", "таблица"], + group: "Продвинутый", + }, + image: { + title: "Картинка", + subtext: "Вставить изображение", + aliases: [ + "image", + "imageUpload", + "upload", + "img", + "picture", + "media", + "url", + "загрузка", + "картинка", + "рисунок", + ], + group: "Медиа", + }, + video: { + title: "Видео", + subtext: "Вставить видео", + aliases: [ + "video", + "videoUpload", + "upload", + "mp4", + "film", + "media", + "url", + "загрузка", + "видео", + ], + group: "Медиа", + }, + audio: { + title: "Аудио", + subtext: "Вставить аудио", + aliases: [ + "audio", + "audioUpload", + "upload", + "mp3", + "sound", + "media", + "url", + "загрузка", + "аудио", + "звук", + "музыка", + ], + group: "Медиа", + }, + file: { + title: "Файл", + subtext: "Вставить файл", + aliases: ["file", "upload", "embed", "media", "url", "загрузка", "файл"], + group: "Медиа", + }, + emoji: { + title: "Эмодзи", + subtext: "Используется для вставки эмодзи", + aliases: ["эмодзи", "смайлик", "выражение эмоций", "лицо"], + group: "Прочее", + }, + }, + placeholders: { + default: "Ведите текст или введите «/» для команд", + heading: "Заголовок", + bulletListItem: "Список", + numberedListItem: "Список", + checkListItem: "Список", + }, + file_blocks: { + image: { + add_button_text: "Добавить изображение", + }, + video: { + add_button_text: "Добавить видео", + }, + audio: { + add_button_text: "Добавить аудио", + }, + file: { + add_button_text: "Добавить файл", + }, + }, + // from react package: + side_menu: { + add_block_label: "Добавить блок", + drag_handle_label: "Открыть меню блока", + }, + drag_handle: { + delete_menuitem: "Удалить", + colors_menuitem: "Цвета", + }, + table_handle: { + delete_column_menuitem: "Удалить столбец", + delete_row_menuitem: "Удалить строку", + add_left_menuitem: "Добавить столбец слева", + add_right_menuitem: "Добавить столбец справа", + add_above_menuitem: "Добавить строку выше", + add_below_menuitem: "Добавить строку ниже", + }, + suggestion_menu: { + no_items_title: "ничего не найдено", + loading: "Загрузка…", + }, + color_picker: { + text_title: "Текст", + background_title: "Задний фон", + colors: { + default: "По умолчинию", + gray: "Серый", + brown: "Коричневый", + red: "Красный", + orange: "Оранжевый", + yellow: "Жёлтый", + green: "Зелёный", + blue: "Голубой", + purple: "Фиолетовый", + pink: "Розовый", + }, + }, + + formatting_toolbar: { + bold: { + tooltip: "Жирный", + secondary_tooltip: "Mod+B", + }, + italic: { + tooltip: "Курсив", + secondary_tooltip: "Mod+I", + }, + underline: { + tooltip: "Подчёркнутый", + secondary_tooltip: "Mod+U", + }, + strike: { + tooltip: "Зачёркнутый", + secondary_tooltip: "Mod+Shift+X", + }, + code: { + tooltip: "Код", + secondary_tooltip: "", }, - link_toolbar: { - delete: { - tooltip: "Удалить ссылку", + colors: { + tooltip: "Цвета", + }, + link: { + tooltip: "Создать ссылку", + secondary_tooltip: "Mod+K", + }, + file_caption: { + tooltip: "Изменить подпись", + input_placeholder: "Изменить подпись", + }, + file_replace: { + tooltip: { + image: "Заменить изображение", + video: "Заменить видео", + audio: "Заменить аудио", + file: "Заменить файл", }, - edit: { - text: "Изменить ссылку", - tooltip: "Редактировать", + }, + file_rename: { + tooltip: { + image: "Переименовать изображение", + video: "Переименовать видео", + audio: "Переименовать аудио", + file: "Переименовать файл", + }, + input_placeholder: { + image: "Переименовать изображение", + video: "Переименовать видео", + audio: "Переименовать аудио", + file: "Переименовать файл", }, - open: { - tooltip: "Открыть в новой вкладке", + }, + file_download: { + tooltip: { + image: "Скачать картинку", + video: "Скачать видео", + audio: "Скачать аудио", + file: "Скачать файл", }, - form: { - title_placeholder: "Изменить заголовок", - url_placeholder: "Изменить URL", + }, + file_delete: { + tooltip: { + image: "Удалить картинку", + video: "Удалить видео", + audio: "Удалить аудио", + file: "Скачать файл", }, }, - generic: { - ctrl_shortcut: "Ctrl", + file_preview_toggle: { + tooltip: "Переключить предварительный просмотр", + }, + nest: { + tooltip: "Сдвинуть вправо", + secondary_tooltip: "Tab", + }, + unnest: { + tooltip: "Сдвинуть влево", + secondary_tooltip: "Shift+Tab", + }, + align_left: { + tooltip: "Текст по левому краю", + }, + align_center: { + tooltip: "Текст по середине", + }, + align_right: { + tooltip: "Текст по правому краю", + }, + align_justify: { + tooltip: "По середине текст", + }, + }, + file_panel: { + upload: { + title: "Загрузить", + file_placeholder: { + image: "Загрузить картинки", + video: "Загрузить видео", + audio: "Загрузить аудио", + file: "Загрузить файл", + }, + upload_error: "Ошибка: не удалось загрузить", + }, + embed: { + title: "Вставить", + embed_button: { + image: "Вставить картинку", + video: "Вставить видео", + audio: "Вставить аудио", + file: "Вставить файл", + }, + url_placeholder: "Введите URL", + }, + }, + link_toolbar: { + delete: { + tooltip: "Удалить ссылку", + }, + edit: { + text: "Изменить ссылку", + tooltip: "Редактировать", + }, + open: { + tooltip: "Открыть в новой вкладке", + }, + form: { + title_placeholder: "Изменить заголовок", + url_placeholder: "Изменить URL", }, - }; - \ No newline at end of file + }, + generic: { + ctrl_shortcut: "Ctrl", + }, +}; diff --git a/packages/core/src/i18n/locales/vi.ts b/packages/core/src/i18n/locales/vi.ts index b00751875b..f188196ddf 100644 --- a/packages/core/src/i18n/locales/vi.ts +++ b/packages/core/src/i18n/locales/vi.ts @@ -100,10 +100,16 @@ export const vi: Dictionary = { emoji: { title: "Biểu tượng cảm xúc", subtext: "Dùng để chèn biểu tượng cảm xúc", - aliases: ["biểu tượng cảm xúc", "emoji", "emoticon", "cảm xúc expression", "khuôn mặt", "face"], + aliases: [ + "biểu tượng cảm xúc", + "emoji", + "emoticon", + "cảm xúc expression", + "khuôn mặt", + "face", + ], group: "Khác", }, - }, placeholders: { default: "Nhập văn bản hoặc gõ '/' để thêm định dạng", diff --git a/packages/core/src/i18n/locales/zh.ts b/packages/core/src/i18n/locales/zh.ts index da9f33d75f..6c28356597 100644 --- a/packages/core/src/i18n/locales/zh.ts +++ b/packages/core/src/i18n/locales/zh.ts @@ -133,10 +133,17 @@ export const zh: Dictionary = { emoji: { title: "表情符号", subtext: "用于插入表情符号", - aliases: ["表情符号", "emoji", "face", "emote", "表情", "表情表达", "表情"], + aliases: [ + "表情符号", + "emoji", + "face", + "emote", + "表情", + "表情表达", + "表情", + ], group: "其他", }, - }, placeholders: { default: "输入 '/' 以使用命令", diff --git a/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx b/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx index 28ab9cf98b..f89cc3b963 100644 --- a/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx +++ b/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx @@ -1,8 +1,6 @@ import { createInlineContentSpec } from "../../schema"; - export const Emoji = createInlineContentSpec( - //STEP 4: this component recieves an emoji, and insets it in the line { type: "emoji", propSchema: { @@ -23,4 +21,3 @@ export const Emoji = createInlineContentSpec( }, } ); - \ No newline at end of file diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 0921279c25..5c0d6e1f78 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -362,7 +362,7 @@ background-color: var(--bn-colors-side-menu); } -.bn-grid-suggestion-menu { +.bn-mantine .bn-grid-suggestion-menu { background: var(--bn-colors-menu-background); border-radius: var(--bn-border-radius-large); box-shadow: var(--bn-shadow-medium); @@ -376,7 +376,7 @@ padding: 20px; } -.bn-grid-suggestion-menu-item { +.bn-mantine .bn-grid-suggestion-menu-item { align-items: center; border-radius: var(--bn-border-radius-large); cursor: pointer; @@ -389,8 +389,8 @@ width: 32px; } -.bn-grid-suggestion-menu-item[aria-selected="true"], -.bn-grid-suggestion-menu-item:hover { +.bn-mantine .bn-grid-suggestion-menu-item[aria-selected="true"], +.bn-mantine .bn-grid-suggestion-menu-item:hover { background-color: var(--bn-colors-hovered-background); } diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx index 721c9e57e7..e17a247591 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getDefaultReactSlashMenuItems.tsx @@ -6,6 +6,7 @@ import { StyleSchema, } from "@blocknote/core"; import { + RiEmotionFill, RiH1, RiH2, RiH3, @@ -20,7 +21,6 @@ import { RiVolumeUpFill, } from "react-icons/ri"; import { DefaultReactSuggestionItem } from "./types"; -import { MdFace } from "react-icons/md"; const icons = { heading: RiH1, @@ -35,7 +35,7 @@ const icons = { video: RiFilmLine, audio: RiVolumeUpFill, file: RiFile2Line, - emoji: MdFace + emoji: RiEmotionFill, }; export function getDefaultReactSlashMenuItems< diff --git a/packages/shadcn/src/style.css b/packages/shadcn/src/style.css index 31aaf91eee..09a6dc052c 100644 --- a/packages/shadcn/src/style.css +++ b/packages/shadcn/src/style.css @@ -110,7 +110,7 @@ background-color: hsl(var(--accent)); } -.bn-grid-suggestion-menu { +.bn-shadcn .bn-grid-suggestion-menu { background: var(--bn-colors-menu-background); border-radius: var(--bn-border-radius-large); box-shadow: var(--bn-shadow-medium); @@ -124,7 +124,7 @@ padding: 20px; } -.bn-grid-suggestion-menu-item { +.bn-shadcn .bn-grid-suggestion-menu-item { align-items: center; border-radius: var(--bn-border-radius-large); cursor: pointer; @@ -137,7 +137,7 @@ width: 32px; } -.bn-grid-suggestion-menu-item[aria-selected="true"], -.bn-grid-suggestion-menu-item:hover { +.bn-shadcn .bn-grid-suggestion-menu-item[aria-selected="true"], +.bn-shadcn .bn-grid-suggestion-menu-item:hover { background-color: var(--bn-colors-hovered-background); } From 5986300d93eaa751701a4d9bb71f7fc70e19e88d Mon Sep 17 00:00:00 2001 From: hunxjunedo Date: Wed, 3 Jul 2024 14:56:11 +0500 Subject: [PATCH 23/41] unlink columns length from css, and a new prop gridCols --- packages/ariakit/src/style.css | 1 - packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx | 4 ++-- packages/mantine/src/style.css | 1 - packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx | 4 ++-- .../src/components/SuggestionMenu/GridSuggestionMenu.tsx | 2 ++ .../components/SuggestionMenu/SuggestionMenuController.tsx | 3 +++ .../src/components/SuggestionMenu/SuggestionMenuWrapper.tsx | 3 +++ packages/react/src/components/SuggestionMenu/types.tsx | 1 + packages/react/src/editor/BlockNoteDefaultUI.tsx | 1 + packages/react/src/editor/ComponentsContext.tsx | 1 + packages/shadcn/src/index.tsx | 2 +- packages/shadcn/src/style.css | 1 - packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx | 4 ++-- 13 files changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index df514f9c29..f5a48f7849 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -89,7 +89,6 @@ box-shadow: var(--bn-shadow-medium); display: grid; gap: 7px; - grid-template-columns: repeat(10, 1fr); justify-items: center; max-height: 30vh; min-width: 40vw; diff --git a/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx index 3f492b19f6..1dde527344 100644 --- a/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx +++ b/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx @@ -6,12 +6,12 @@ export const GridSuggestionMenu = forwardRef< HTMLDivElement, ComponentProps["GridSuggestionMenu"]["Root"] >((props, ref) => { - const { className, children, id, ...rest } = props; + const { className, children, id, style, ...rest } = props; assertEmpty(rest); return ( -
+
{children}
); diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 5c0d6e1f78..447fd860bc 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -368,7 +368,6 @@ box-shadow: var(--bn-shadow-medium); display: grid; gap: 7px; - grid-template-columns: repeat(10, 1fr); justify-items: center; max-height: 30vh; min-width: 40vw; diff --git a/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx index 3f492b19f6..1dde527344 100644 --- a/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx +++ b/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx @@ -6,12 +6,12 @@ export const GridSuggestionMenu = forwardRef< HTMLDivElement, ComponentProps["GridSuggestionMenu"]["Root"] >((props, ref) => { - const { className, children, id, ...rest } = props; + const { className, children, id, style, ...rest } = props; assertEmpty(rest); return ( -
+
{children}
); diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx index 68c8cb3747..e2e1abc6aa 100644 --- a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx @@ -15,6 +15,7 @@ export default function GridSuggestionMenu< // loadingState, selectedIndex, onItemClick, + gridCols } = props; // const loader = @@ -59,6 +60,7 @@ export default function GridSuggestionMenu< return ( {renderedItems} {/*{renderedItems.length === 0 &&*/} diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 3f566ae468..b6c1a1f365 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -32,6 +32,7 @@ export function SuggestionMenuController< triggerCharacter: string; getItems?: GetItemsType; grid?: boolean; + gridCols?: number } & (ItemType extends DefaultReactSuggestionItem ? { // can be undefined @@ -60,6 +61,7 @@ export function SuggestionMenuController< grid, onItemClick, getItems, + gridCols } = props; const onItemClickOrDefault = useMemo(() => { @@ -135,6 +137,7 @@ export function SuggestionMenuController< clearQuery={callbacks.clearQuery} getItems={getItemsOrDefault} grid={grid} + gridCols={gridCols} suggestionMenuComponent={suggestionMenuComponent || SuggestionMenu} onItemClick={onItemClickOrDefault} /> diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index e2a9425499..a550c6ba36 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -14,6 +14,7 @@ export function SuggestionMenuWrapper(props: { clearQuery: () => void; getItems: (query: string) => Promise; grid?: boolean; + gridCols?: number onItemClick?: (item: Item) => void; suggestionMenuComponent: FC>; }) { @@ -33,6 +34,7 @@ export function SuggestionMenuWrapper(props: { closeMenu, onItemClick, grid, + gridCols } = props; const onItemClickCloseMenu = useCallback( @@ -99,6 +101,7 @@ export function SuggestionMenuWrapper(props: { onItemClick={onItemClickCloseMenu} loadingState={loadingState} selectedIndex={selectedIndex} + gridCols={gridCols} /> ); } diff --git a/packages/react/src/components/SuggestionMenu/types.tsx b/packages/react/src/components/SuggestionMenu/types.tsx index e9aa3e6ad5..2c5a53bec7 100644 --- a/packages/react/src/components/SuggestionMenu/types.tsx +++ b/packages/react/src/components/SuggestionMenu/types.tsx @@ -25,4 +25,5 @@ export type SuggestionMenuProps = { selectedIndex: number | undefined; onItemClick?: (item: T) => void; emojiInsert?: (emoji: never) => void; + gridCols?: number }; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index 764f7a8326..bba9bd195e 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -59,6 +59,7 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { ((props, ref) => { - const { className, children, id, ...rest } = props; + const { className, children, id, style, ...rest } = props; assertEmpty(rest); return ( -
+
{children}
); From 449825da8a1367198aef0f9545bf98bffbacd724 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Fri, 5 Jul 2024 13:17:54 +0200 Subject: [PATCH 24/41] More cleanup & implemented PR feedback --- package-lock.json | 10 --- .../src/suggestionMenu/GridSuggestionMenu.tsx | 9 +- .../suggestionMenu/GridSuggestionMenuItem.tsx | 6 +- packages/core/package.json | 1 - packages/core/src/editor/BlockNoteEditor.ts | 12 +++ .../src/extensions/SideMenu/SideMenuPlugin.ts | 11 +-- .../DefaultGridSuggestionItem.ts | 1 + .../SuggestionMenu/SuggestionPlugin.ts | 2 +- .../getDefaultEmojiPickerItems.ts | 36 +++++++- .../getDefaultSlashMenuItems.ts | 13 +-- .../src/suggestionMenu/GridSuggestionMenu.tsx | 9 +- .../suggestionMenu/GridSuggestionMenuItem.tsx | 6 +- .../SuggestionMenu/GridSuggestionMenu.tsx | 10 +-- .../SuggestionMenuController.tsx | 85 ++++++++++++++----- .../SuggestionMenu/SuggestionMenuWrapper.tsx | 10 +-- .../getDefaultReactEmojiPickerItems.tsx | 8 +- .../useSuggestionMenuKeyboardNavigation.ts | 23 +++-- .../src/components/SuggestionMenu/types.tsx | 3 +- .../react/src/editor/BlockNoteDefaultUI.tsx | 32 +------ .../react/src/editor/ComponentsContext.tsx | 2 +- .../src/suggestionMenu/GridSuggestionMenu.tsx | 9 +- .../suggestionMenu/GridSuggestionMenuItem.tsx | 6 +- 22 files changed, 171 insertions(+), 133 deletions(-) diff --git a/package-lock.json b/package-lock.json index 777cf78a19..efd7b34c63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2600,15 +2600,6 @@ "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==" }, - "node_modules/@emoji-mart/react": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz", - "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==", - "peerDependencies": { - "emoji-mart": "^5.2", - "react": "^16.8 || ^17 || ^18" - } - }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -27124,7 +27115,6 @@ "license": "MPL-2.0", "dependencies": { "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", "@tiptap/core": "^2.4.0", "@tiptap/extension-bold": "^2.4.0", "@tiptap/extension-code": "^2.4.0", diff --git a/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx index 1dde527344..a5a52aece4 100644 --- a/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx +++ b/packages/ariakit/src/suggestionMenu/GridSuggestionMenu.tsx @@ -6,12 +6,17 @@ export const GridSuggestionMenu = forwardRef< HTMLDivElement, ComponentProps["GridSuggestionMenu"]["Root"] >((props, ref) => { - const { className, children, id, style, ...rest } = props; + const { className, children, id, columns, ...rest } = props; assertEmpty(rest); return ( -
+
{children}
); diff --git a/packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx index 0c4b98f80a..6db77fda62 100644 --- a/packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx +++ b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuItem.tsx @@ -27,16 +27,14 @@ export const GridSuggestionMenuItem = forwardRef< }, [isSelected]); return ( -

onItemClick?.(item)} aria-selected={isSelected || undefined}> {item.icon} -

+
); }); diff --git a/packages/core/package.json b/packages/core/package.json index 843f3e4d55..5de88201d2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -55,7 +55,6 @@ }, "dependencies": { "@emoji-mart/data": "^1.2.1", - "@emoji-mart/react": "^1.1.1", "@tiptap/core": "^2.4.0", "@tiptap/extension-bold": "^2.4.0", "@tiptap/extension-code": "^2.4.0", diff --git a/packages/core/src/editor/BlockNoteEditor.ts b/packages/core/src/editor/BlockNoteEditor.ts index 8232db27d3..57e0a0be89 100644 --- a/packages/core/src/editor/BlockNoteEditor.ts +++ b/packages/core/src/editor/BlockNoteEditor.ts @@ -1038,4 +1038,16 @@ export class BlockNoteEditor< this._tiptapEditor.off("selectionUpdate", cb); }; } + + public openSelectionMenu(triggerCharacter: string) { + this.prosemirrorView.focus(); + this.prosemirrorView.dispatch( + this.prosemirrorView.state.tr + .scrollIntoView() + .setMeta(this.suggestionMenus.plugin, { + triggerCharacter: triggerCharacter, + fromUserInput: false, + }) + ); + } } diff --git a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts index 3f5e02075c..6ab23dd15f 100644 --- a/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts +++ b/packages/core/src/extensions/SideMenu/SideMenuPlugin.ts @@ -12,7 +12,6 @@ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { UiElementPosition } from "../../extensions-shared/UiElementPosition"; import { BlockSchema, InlineContentSchema, StyleSchema } from "../../schema"; import { EventEmitter } from "../../util/EventEmitter"; -import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin"; import { MultipleNodeSelection } from "./MultipleNodeSelection"; let dragImageElement: Element | undefined; @@ -649,14 +648,8 @@ export class SideMenuView< this.editor._tiptapEditor.commands.setTextSelection(startPos + 1); } - // Focuses and activates the suggestion menu. - this.pmView.focus(); - this.pmView.dispatch( - this.pmView.state.tr.scrollIntoView().setMeta(suggestionMenuPluginKey, { - triggerCharacter: "/", - fromUserInput: false, - }) - ); + // Focuses and activates the slash menu. + this.editor.openSelectionMenu("/"); } } diff --git a/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts b/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts index 7f90bada89..01d5aea707 100644 --- a/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts +++ b/packages/core/src/extensions/SuggestionMenu/DefaultGridSuggestionItem.ts @@ -1,3 +1,4 @@ export type DefaultGridSuggestionItem = { id: string; + onItemClick: () => void; }; diff --git a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts index d64bd83984..7729108732 100644 --- a/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts +++ b/packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts @@ -139,7 +139,7 @@ type SuggestionPluginState = } | undefined; -export const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin"); +const suggestionMenuPluginKey = new PluginKey("SuggestionMenuPlugin"); /** * A ProseMirror plugin for suggestions, designed to make '/'-commands possible as well as mentions. diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts index 9a95549f33..062a87b1cd 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts @@ -16,8 +16,11 @@ export async function getDefaultEmojiPickerItems< >( editor: BlockNoteEditor, query: string -): Promise<(DefaultGridSuggestionItem & { emoji: string })[]> { - if (!checkDefaultInlineContentTypeInSchema("emoji", editor)) { +): Promise { + if ( + !checkDefaultInlineContentTypeInSchema("emoji", editor) || + !checkDefaultInlineContentTypeInSchema("text", editor) + ) { return []; } @@ -27,7 +30,32 @@ export async function getDefaultEmojiPickerItems< : ((await SearchIndex.search(query)) as Emoji[]); return emojisToShow.map((emoji: Emoji) => ({ - id: emoji.id, - emoji: emoji.skins[0].native, + id: emoji.skins[0].native, + onItemClick: () => { + // This is a bit hacky since we're doing 2 insertions, but it seems like + // writing a type guard to check if multiple default inline content types + // are in the schema is quite a pain as opposed to checking just one. And + // so this seems like a more reasonable option for now. + if (checkDefaultInlineContentTypeInSchema("emoji", editor)) { + editor.insertInlineContent([ + { + type: "emoji", + props: { + emoji: emoji.skins[0].native, + }, + }, + ]); + } + + if (checkDefaultInlineContentTypeInSchema("text", editor)) { + editor.insertInlineContent([ + { + type: "text", + text: " ", + styles: {}, + }, + ]); + } + }, })); } diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts index bb560adb45..72994f5d04 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultSlashMenuItems.ts @@ -1,6 +1,5 @@ import type { BlockNoteEditor } from "../../editor/BlockNoteEditor"; import { Block, PartialBlock } from "../../blocks/defaultBlocks"; -import { suggestionMenuPluginKey } from "../SuggestionMenu/SuggestionPlugin"; import { checkDefaultBlockTypeInSchema } from "../../blocks/defaultBlockTypeGuards"; import { @@ -273,17 +272,7 @@ export function getDefaultSlashMenuItems< } items.push({ - onItemClick: () => { - editor.prosemirrorView.focus(); - editor.prosemirrorView.dispatch( - editor.prosemirrorView.state.tr - .scrollIntoView() - .setMeta(suggestionMenuPluginKey, { - triggerCharacter: ":", - fromUserInput: false, - }) - ); - }, + onItemClick: () => editor.openSelectionMenu(":"), key: "emoji", ...editor.dictionary.slash_menu.emoji, }); diff --git a/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx index 1dde527344..a5a52aece4 100644 --- a/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx +++ b/packages/mantine/src/suggestionMenu/GridSuggestionMenu.tsx @@ -6,12 +6,17 @@ export const GridSuggestionMenu = forwardRef< HTMLDivElement, ComponentProps["GridSuggestionMenu"]["Root"] >((props, ref) => { - const { className, children, id, style, ...rest } = props; + const { className, children, id, columns, ...rest } = props; assertEmpty(rest); return ( -
+
{children}
); diff --git a/packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx b/packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx index dcb08e0eea..ded689a9cb 100644 --- a/packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx +++ b/packages/mantine/src/suggestionMenu/GridSuggestionMenuItem.tsx @@ -29,16 +29,14 @@ export const GridSuggestionMenuItem = forwardRef< }, [isSelected]); return ( -

onItemClick?.(item)} aria-selected={isSelected || undefined}> {item.icon} -

+
); }); diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx index e2e1abc6aa..90d459f00e 100644 --- a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx @@ -4,9 +4,9 @@ import { useComponentsContext } from "../../editor/ComponentsContext"; // import { useDictionary } from "../../i18n/dictionary"; import { DefaultReactGridSuggestionItem, SuggestionMenuProps } from "./types"; -export default function GridSuggestionMenu< - T extends DefaultReactGridSuggestionItem ->(props: SuggestionMenuProps): JSX.Element { +export function GridSuggestionMenu( + props: SuggestionMenuProps +) { const Components = useComponentsContext()!; // const dict = useDictionary(); @@ -15,7 +15,7 @@ export default function GridSuggestionMenu< // loadingState, selectedIndex, onItemClick, - gridCols + columns, } = props; // const loader = @@ -60,7 +60,7 @@ export default function GridSuggestionMenu< return ( {renderedItems} {/*{renderedItems.length === 0 &&*/} diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index b6c1a1f365..1f6162260b 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -14,35 +14,70 @@ import { useUIPluginState } from "../../hooks/useUIPluginState"; import { SuggestionMenu } from "./SuggestionMenu"; import { SuggestionMenuWrapper } from "./SuggestionMenuWrapper"; import { getDefaultReactSlashMenuItems } from "./getDefaultReactSlashMenuItems"; -import { DefaultReactSuggestionItem, SuggestionMenuProps } from "./types"; +import { GridSuggestionMenu } from "./GridSuggestionMenu"; +import { getDefaultReactEmojiPickerItems } from "./getDefaultReactEmojiPickerItems"; +import { + DefaultReactGridSuggestionItem, + DefaultReactSuggestionItem, + SuggestionMenuProps, +} from "./types"; type ArrayElement
= A extends readonly (infer T)[] ? T : never; type ItemType Promise> = ArrayElement>>; +// There's a lot going on with the typing here but tl;dr: +// - If Columns is undefined and the item type is DefaultReactSuggestionItem, +// `suggestionMenuComponent` and `onItemClick` are optional. +// - If Columns is a number and the item type is DefaultGridReactSuggestionItem, +// `suggestionMenuComponent` and `onItemClick` are also optional. +// - Otherwise, `suggestionMenuComponent` and `onItemClick` are required. export function SuggestionMenuController< + Columns extends number | undefined = undefined, // This is a bit hacky, but only way I found to make types work so the optionality // of suggestionMenuComponent depends on the return type of getItems GetItemsType extends (query: string) => Promise = ( query: string - ) => Promise + ) => Promise< + Columns extends number + ? DefaultReactGridSuggestionItem[] + : DefaultReactSuggestionItem[] + > >( props: { triggerCharacter: string; getItems?: GetItemsType; - grid?: boolean; - gridCols?: number + columns?: Columns; } & (ItemType extends DefaultReactSuggestionItem - ? { - // can be undefined - suggestionMenuComponent?: FC< - SuggestionMenuProps> - >; - onItemClick?: (item: ItemType) => void; - } + ? Columns extends undefined + ? { + suggestionMenuComponent?: FC< + SuggestionMenuProps> + >; + onItemClick?: (item: ItemType) => void; + } + : { + suggestionMenuComponent: FC< + SuggestionMenuProps> + >; + onItemClick: (item: ItemType) => void; + } + : ItemType extends DefaultReactGridSuggestionItem + ? Columns extends number + ? { + suggestionMenuComponent?: FC< + SuggestionMenuProps> + >; + onItemClick?: (item: ItemType) => void; + } + : { + suggestionMenuComponent: FC< + SuggestionMenuProps> + >; + onItemClick: (item: ItemType) => void; + } : { - // getItems doesn't return DefaultSuggestionItem, so suggestionMenuComponent is required suggestionMenuComponent: FC< SuggestionMenuProps> >; @@ -58,12 +93,13 @@ export function SuggestionMenuController< const { triggerCharacter, suggestionMenuComponent, - grid, + columns, onItemClick, getItems, - gridCols } = props; + const isGrid = columns !== undefined && columns > 1; + const onItemClickOrDefault = useMemo(() => { return ( onItemClick || @@ -77,12 +113,14 @@ export function SuggestionMenuController< return ( getItems || ((async (query: string) => - filterSuggestionItems( - getDefaultReactSlashMenuItems(editor), - query - )) as any as typeof getItems) + isGrid + ? await getDefaultReactEmojiPickerItems(editor, query) + : filterSuggestionItems( + getDefaultReactSlashMenuItems(editor), + query + )) as any as typeof getItems) ); - }, [editor, getItems])!; + }, [editor, getItems, isGrid])!; const callbacks = { closeMenu: editor.suggestionMenus.closeMenu, @@ -136,9 +174,12 @@ export function SuggestionMenuController< closeMenu={callbacks.closeMenu} clearQuery={callbacks.clearQuery} getItems={getItemsOrDefault} - grid={grid} - gridCols={gridCols} - suggestionMenuComponent={suggestionMenuComponent || SuggestionMenu} + columns={columns} + suggestionMenuComponent={ + suggestionMenuComponent || (columns !== undefined && columns > 1) + ? GridSuggestionMenu + : SuggestionMenu + } onItemClick={onItemClickOrDefault} />
diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx index a550c6ba36..255dba936b 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuWrapper.tsx @@ -13,8 +13,7 @@ export function SuggestionMenuWrapper(props: { closeMenu: () => void; clearQuery: () => void; getItems: (query: string) => Promise; - grid?: boolean; - gridCols?: number + columns?: number; onItemClick?: (item: Item) => void; suggestionMenuComponent: FC>; }) { @@ -33,8 +32,7 @@ export function SuggestionMenuWrapper(props: { clearQuery, closeMenu, onItemClick, - grid, - gridCols + columns, } = props; const onItemClickCloseMenu = useCallback( @@ -57,7 +55,7 @@ export function SuggestionMenuWrapper(props: { editor, query, items, - grid, + columns, onItemClickCloseMenu ); @@ -101,7 +99,7 @@ export function SuggestionMenuWrapper(props: { onItemClick={onItemClickCloseMenu} loadingState={loadingState} selectedIndex={selectedIndex} - gridCols={gridCols} + columns={columns} /> ); } diff --git a/packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx b/packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx index 62a7e563b4..85ec2e53a2 100644 --- a/packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx +++ b/packages/react/src/components/SuggestionMenu/getDefaultReactEmojiPickerItems.tsx @@ -14,8 +14,12 @@ export async function getDefaultReactEmojiPickerItems< >( editor: BlockNoteEditor, query: string -): Promise<(DefaultReactGridSuggestionItem & { emoji: string })[]> { +): Promise { return (await getDefaultEmojiPickerItems(editor, query)).map( - ({ id, emoji }) => ({ id, emoji, icon: emoji as any }) + ({ id, onItemClick }) => ({ + id, + onItemClick, + icon: id as any, + }) ); } diff --git a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts index 47ccb97c34..782a1d6f23 100644 --- a/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts +++ b/packages/react/src/components/SuggestionMenu/hooks/useSuggestionMenuKeyboardNavigation.ts @@ -8,28 +8,30 @@ export function useSuggestionMenuKeyboardNavigation( editor: BlockNoteEditor, query: string, items: Item[], - grid = false, + columns?: number, onItemClick?: (item: Item) => void ) { const [selectedIndex, setSelectedIndex] = useState(0); + const isGrid = columns !== undefined && columns > 1; + useEffect(() => { const handleMenuNavigationKeys = (event: KeyboardEvent) => { - if (grid) { - if (event.key === "ArrowRight") { + if (isGrid) { + if (event.key === "ArrowLeft") { event.preventDefault(); if (items.length) { setSelectedIndex( - (selectedIndex + 1 + items!.length) % items!.length + (selectedIndex - 1 + items!.length) % items!.length ); } } - if (event.key === "ArrowLeft") { + if (event.key === "ArrowRight") { event.preventDefault(); if (items.length) { setSelectedIndex( - (selectedIndex - 1 + items!.length) % items!.length + (selectedIndex + 1 + items!.length) % items!.length ); } } @@ -40,7 +42,8 @@ export function useSuggestionMenuKeyboardNavigation( if (items.length) { setSelectedIndex( - (selectedIndex - (grid ? 10 : 1) + items!.length) % items!.length + (selectedIndex - (isGrid ? columns : 1) + items!.length) % + items!.length ); } @@ -51,7 +54,9 @@ export function useSuggestionMenuKeyboardNavigation( event.preventDefault(); if (items.length) { - setSelectedIndex((selectedIndex + (grid ? 10 : 1)) % items!.length); + setSelectedIndex( + (selectedIndex + (isGrid ? columns : 1)) % items!.length + ); } return true; @@ -83,7 +88,7 @@ export function useSuggestionMenuKeyboardNavigation( true ); }; - }, [editor.domElement, items, selectedIndex, onItemClick, grid]); + }, [editor.domElement, items, selectedIndex, onItemClick, columns, isGrid]); // Resets index when items change useEffect(() => { diff --git a/packages/react/src/components/SuggestionMenu/types.tsx b/packages/react/src/components/SuggestionMenu/types.tsx index 2c5a53bec7..d092887f62 100644 --- a/packages/react/src/components/SuggestionMenu/types.tsx +++ b/packages/react/src/components/SuggestionMenu/types.tsx @@ -24,6 +24,5 @@ export type SuggestionMenuProps = { loadingState: "loading-initial" | "loading" | "loaded"; selectedIndex: number | undefined; onItemClick?: (item: T) => void; - emojiInsert?: (emoji: never) => void; - gridCols?: number + columns?: number; }; diff --git a/packages/react/src/editor/BlockNoteDefaultUI.tsx b/packages/react/src/editor/BlockNoteDefaultUI.tsx index bba9bd195e..5739fd251a 100644 --- a/packages/react/src/editor/BlockNoteDefaultUI.tsx +++ b/packages/react/src/editor/BlockNoteDefaultUI.tsx @@ -1,12 +1,8 @@ -import { useCallback } from "react"; - import { FormattingToolbarController } from "../components/FormattingToolbar/FormattingToolbarController"; import { LinkToolbarController } from "../components/LinkToolbar/LinkToolbarController"; import { FilePanelController } from "../components/FilePanel/FilePanelController"; import { SideMenuController } from "../components/SideMenu/SideMenuController"; import { SuggestionMenuController } from "../components/SuggestionMenu/SuggestionMenuController"; -import GridSuggestionMenu from "../components/SuggestionMenu/GridSuggestionMenu"; -import { getDefaultReactEmojiPickerItems } from "../components/SuggestionMenu/getDefaultReactEmojiPickerItems"; import { TableHandlesController } from "../components/TableHandles/TableHandlesController"; import { useBlockNoteEditor } from "../hooks/useBlockNoteEditor"; @@ -29,25 +25,6 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { ); } - const getEmojiPickerItems = useCallback( - (query: string) => getDefaultReactEmojiPickerItems(editor, query), - [editor] - ); - const emojiPickerOnItemClick = useCallback( - (item: { id: string; emoji: string }) => { - editor.insertInlineContent([ - { - type: "emoji", - props: { - emoji: item.emoji, - }, - }, - " ", - ]); - }, - [editor] - ); - return ( <> {props.formattingToolbar !== false && } @@ -56,14 +33,7 @@ export function BlockNoteDefaultUI(props: BlockNoteDefaultUIProps) { )} {props.emojiPicker !== false && ( - + )} {props.sideMenu !== false && } {editor.filePanel && props.filePanel !== false && } diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index 5e1781835e..05266f1476 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -148,9 +148,9 @@ export type ComponentProps = { GridSuggestionMenu: { Root: { id: string; + columns?: number; className?: string; children?: ReactNode; - style?:any; }; // EmptyItem: { // className?: string; diff --git a/packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx b/packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx index 1dde527344..a5a52aece4 100644 --- a/packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx +++ b/packages/shadcn/src/suggestionMenu/GridSuggestionMenu.tsx @@ -6,12 +6,17 @@ export const GridSuggestionMenu = forwardRef< HTMLDivElement, ComponentProps["GridSuggestionMenu"]["Root"] >((props, ref) => { - const { className, children, id, style, ...rest } = props; + const { className, children, id, columns, ...rest } = props; assertEmpty(rest); return ( -
+
{children}
); diff --git a/packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx index 0c4b98f80a..6db77fda62 100644 --- a/packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx +++ b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuItem.tsx @@ -27,16 +27,14 @@ export const GridSuggestionMenuItem = forwardRef< }, [isSelected]); return ( -

onItemClick?.(item)} aria-selected={isSelected || undefined}> {item.icon} -

+
); }); From d82fb818ed88f18a879d2d6dd2711c3ef40603c3 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Fri, 5 Jul 2024 13:26:50 +0200 Subject: [PATCH 25/41] Change emojis to use text instead of custom IC & fixed styles --- packages/ariakit/src/style.css | 1 - packages/core/src/blocks/defaultBlocks.ts | 3 -- .../getDefaultEmojiPickerItems.ts | 28 ++----------------- .../emojiInlineContent/Emoji.tsx | 23 --------------- packages/mantine/src/style.css | 1 - packages/shadcn/src/style.css | 1 - 6 files changed, 2 insertions(+), 55 deletions(-) delete mode 100644 packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index f5a48f7849..dfdce2fd08 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -91,7 +91,6 @@ gap: 7px; justify-items: center; max-height: 30vh; - min-width: 40vw; overflow-y: scroll; padding: 20px; } diff --git a/packages/core/src/blocks/defaultBlocks.ts b/packages/core/src/blocks/defaultBlocks.ts index e91a0845e0..e2b9b9e8dc 100644 --- a/packages/core/src/blocks/defaultBlocks.ts +++ b/packages/core/src/blocks/defaultBlocks.ts @@ -20,8 +20,6 @@ import { getStyleSchemaFromSpecs, } from "../schema"; -import { Emoji } from "../inlineContent/emojiInlineContent/Emoji"; - import { Heading } from "./HeadingBlockContent/HeadingBlockContent"; import { BulletListItem } from "./ListItemBlockContent/BulletListItemBlockContent/BulletListItemBlockContent"; import { NumberedListItem } from "./ListItemBlockContent/NumberedListItemBlockContent/NumberedListItemBlockContent"; @@ -73,7 +71,6 @@ export type DefaultStyleSchema = _DefaultStyleSchema; export const defaultInlineContentSpecs = { text: { config: "text", implementation: {} as any }, link: { config: "link", implementation: {} as any }, - emoji: Emoji, } satisfies InlineContentSpecs; export const defaultInlineContentSchema = getInlineContentSchemaFromSpecs( diff --git a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts index 062a87b1cd..2aec90435e 100644 --- a/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts +++ b/packages/core/src/extensions/SuggestionMenu/getDefaultEmojiPickerItems.ts @@ -17,10 +17,7 @@ export async function getDefaultEmojiPickerItems< editor: BlockNoteEditor, query: string ): Promise { - if ( - !checkDefaultInlineContentTypeInSchema("emoji", editor) || - !checkDefaultInlineContentTypeInSchema("text", editor) - ) { + if (!checkDefaultInlineContentTypeInSchema("text", editor)) { return []; } @@ -32,29 +29,8 @@ export async function getDefaultEmojiPickerItems< return emojisToShow.map((emoji: Emoji) => ({ id: emoji.skins[0].native, onItemClick: () => { - // This is a bit hacky since we're doing 2 insertions, but it seems like - // writing a type guard to check if multiple default inline content types - // are in the schema is quite a pain as opposed to checking just one. And - // so this seems like a more reasonable option for now. - if (checkDefaultInlineContentTypeInSchema("emoji", editor)) { - editor.insertInlineContent([ - { - type: "emoji", - props: { - emoji: emoji.skins[0].native, - }, - }, - ]); - } - if (checkDefaultInlineContentTypeInSchema("text", editor)) { - editor.insertInlineContent([ - { - type: "text", - text: " ", - styles: {}, - }, - ]); + editor.insertInlineContent(emoji.skins[0].native + " "); } }, })); diff --git a/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx b/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx deleted file mode 100644 index f89cc3b963..0000000000 --- a/packages/core/src/inlineContent/emojiInlineContent/Emoji.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { createInlineContentSpec } from "../../schema"; - -export const Emoji = createInlineContentSpec( - { - type: "emoji", - propSchema: { - emoji: { - default: "", - }, - }, - content: "none", - }, - { - render: (props: any) => { - const dom = document.createElement("span"); - dom.appendChild(document.createTextNode(props.props.emoji)); - - return { - dom, - }; - }, - } -); diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 447fd860bc..0c7e30bcfa 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -370,7 +370,6 @@ gap: 7px; justify-items: center; max-height: 30vh; - min-width: 40vw; overflow-y: scroll; padding: 20px; } diff --git a/packages/shadcn/src/style.css b/packages/shadcn/src/style.css index d3e077c75a..af968e613e 100644 --- a/packages/shadcn/src/style.css +++ b/packages/shadcn/src/style.css @@ -118,7 +118,6 @@ gap: 7px; justify-items: center; max-height: 30vh; - min-width: 40vw; overflow-y: scroll; padding: 20px; } From e72af9d2b6ea64eb25d3aaad33a42f603142fd6a Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Mon, 8 Jul 2024 00:52:39 +0200 Subject: [PATCH 26/41] Added empty grid items and loaders --- packages/ariakit/src/index.tsx | 4 + packages/ariakit/src/style.css | 131 ++++++++++-------- .../GridSuggestionMenuEmptyItem.tsx | 21 +++ .../GridSuggestionMenuLoader.tsx | 26 ++++ packages/mantine/src/index.tsx | 7 +- packages/mantine/src/style.css | 21 ++- .../GridSuggestionMenuEmptyItem.tsx | 25 ++++ .../GridSuggestionMenuLoader.tsx | 28 ++++ .../SuggestionMenu/GridSuggestionMenu.tsx | 43 +++--- .../SuggestionMenuController.tsx | 2 +- .../react/src/editor/ComponentsContext.tsx | 18 +-- packages/shadcn/src/index.tsx | 8 +- packages/shadcn/src/style.css | 25 +++- .../GridSuggestionMenuEmptyItem.tsx | 21 +++ .../GridSuggestionMenuLoader.tsx | 26 ++++ .../SuggestionMenuEmptyItem.tsx | 2 +- 16 files changed, 311 insertions(+), 97 deletions(-) create mode 100644 packages/ariakit/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx create mode 100644 packages/ariakit/src/suggestionMenu/GridSuggestionMenuLoader.tsx create mode 100644 packages/mantine/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx create mode 100644 packages/mantine/src/suggestionMenu/GridSuggestionMenuLoader.tsx create mode 100644 packages/shadcn/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx create mode 100644 packages/shadcn/src/suggestionMenu/GridSuggestionMenuLoader.tsx diff --git a/packages/ariakit/src/index.tsx b/packages/ariakit/src/index.tsx index e604632fda..a02cb855c5 100644 --- a/packages/ariakit/src/index.tsx +++ b/packages/ariakit/src/index.tsx @@ -13,7 +13,9 @@ import { ComponentProps } from "react"; import { Form } from "./input/Form"; import { GridSuggestionMenu } from "./suggestionMenu/GridSuggestionMenu"; +import { GridSuggestionMenuEmptyItem } from "./suggestionMenu/GridSuggestionMenuEmptyItem"; import { GridSuggestionMenuItem } from "./suggestionMenu/GridSuggestionMenuItem"; +import { GridSuggestionMenuLoader } from "./suggestionMenu/GridSuggestionMenuLoader"; import { TextInput } from "./input/TextInput"; import { Menu, @@ -59,6 +61,8 @@ export const components: Components = { GridSuggestionMenu: { Root: GridSuggestionMenu, Item: GridSuggestionMenuItem, + EmptyItem: GridSuggestionMenuEmptyItem, + Loader: GridSuggestionMenuLoader, }, LinkToolbar: { Root: Toolbar, diff --git a/packages/ariakit/src/style.css b/packages/ariakit/src/style.css index dfdce2fd08..213f4d5cd9 100644 --- a/packages/ariakit/src/style.css +++ b/packages/ariakit/src/style.css @@ -6,81 +6,87 @@ @import "./ariakitStyles.css"; .bn-ak-input-wrapper { - align-items: center; - display: flex; - gap: 0.5rem; + align-items: center; + display: flex; + gap: 0.5rem; } .bn-toolbar .bn-ak-button { - width: unset; + width: unset; } .bn-toolbar .bn-ak-button[data-selected] { - padding-top: 0.125rem; - box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--border); + padding-top: 0.125rem; + box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--border); } .bn-toolbar .bn-ak-button[data-selected]:where(.dark, .dark *) { - box-shadow: inset 0 0 0 1px var(--border), inset 0 1px 1px 1px var(--shadow); + box-shadow: inset 0 0 0 1px var(--border), inset 0 1px 1px 1px var(--shadow); } .bn-toolbar .bn-ak-popover { - gap: 0.5rem; + gap: 0.5rem; } .bn-ariakit .bn-tab-panel { - align-items: center; - display: flex; - flex-direction: column; - gap: 0.5rem; + align-items: center; + display: flex; + flex-direction: column; + gap: 0.5rem; } .bn-ariakit .bn-file-input { - max-width: 100%; + max-width: 100%; } .bn-ak-button { - outline-style: none; + outline-style: none; } -.bn-ak-menu-item[aria-selected="true"], .bn-ak-menu-item:hover { - background-color: hsl(204 100% 40%); - color: hsl(204 20% 100%); +.bn-ak-menu-item[aria-selected="true"], +.bn-ak-menu-item:hover { + background-color: hsl(204 100% 40%); + color: hsl(204 20% 100%); } .bn-ak-menu-item { - display: flex; + display: flex; } .bn-ariakit .bn-dropdown { - overflow: visible; + overflow: visible; } -.bn-ariakit .bn-suggestion-menu, .bn-ariakit .bn-color-picker-dropdown { - overflow: scroll; +.bn-ariakit .bn-suggestion-menu { + height: fit-content; + max-height: 100%; +} + +.bn-ariakit .bn-color-picker-dropdown { + overflow: scroll; } .bn-ak-suggestion-menu-item-body { - flex: 1; + flex: 1; } .bn-ak-suggestion-menu-item-subtitle { - font-size: 0.7rem; + font-size: 0.7rem; } .bn-ak-suggestion-menu-item-section[data-position="left"] { - padding: 8px; + padding: 8px; } .bn-ak-suggestion-menu-item-section[data-position="right"] { - --border: rgb(0 0 0/13%); - --highlight: rgb(255 255 255/20%); - --shadow: rgb(0 0 0/10%); - box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--highlight), + --border: rgb(0 0 0/13%); + --highlight: rgb(255 255 255/20%); + --shadow: rgb(0 0 0/10%); + box-shadow: inset 0 0 0 1px var(--border), inset 0 2px 0 var(--highlight), inset 0 -1px 0 var(--shadow), 0 1px 1px var(--shadow); - font-size: 0.7rem; - border-radius: 4px; - padding-inline: 4px; + font-size: 0.7rem; + border-radius: 4px; + padding-inline: 4px; } .bn-ariakit .bn-grid-suggestion-menu { @@ -89,9 +95,10 @@ box-shadow: var(--bn-shadow-medium); display: grid; gap: 7px; + height: fit-content; justify-items: center; - max-height: 30vh; - overflow-y: scroll; + max-height: min(500px, 100%); + overflow-y: auto; padding: 20px; } @@ -113,51 +120,67 @@ background-color: var(--bn-colors-hovered-background); } +.bn-ariakit .bn-grid-suggestion-menu-empty-item, +.bn-ariakit .bn-grid-suggestion-menu-loader { + align-items: center; + color: var(--bn-colors-menu-text); + display: flex; + font-size: 14px; + font-weight: 500; + height: 32px; + justify-content: center; +} + +.bn-ariakit .bn-grid-suggestion-menu-loader span { + background-color: var(--bn-colors-side-menu); +} + .bn-ariakit .bn-side-menu { - align-items: center; - display: flex; - justify-content: center; + align-items: center; + display: flex; + justify-content: center; } .bn-side-menu .bn-ak-button { - height: fit-content; - padding: 0; - width: fit-content; + height: fit-content; + padding: 0; + width: fit-content; } .bn-ariakit .bn-panel-popover { - background-color: transparent; - border: none; - box-shadow: none; + background-color: transparent; + border: none; + box-shadow: none; } .bn-ariakit .bn-table-handle { - height: fit-content; - padding: 0; - width: fit-content; + height: fit-content; + padding: 0; + width: fit-content; } .bn-ariakit .bn-side-menu, .bn-ariakit .bn-table-handle { - color: gray; + color: gray; } .bn-ak-button:where(.dark, .dark *) { - color: hsl(204 20% 100%); + color: hsl(204 20% 100%); } -.bn-ak-tab, .bn-ariakit .bn-file-input { - background-color: transparent; - color: black; +.bn-ak-tab, +.bn-ariakit .bn-file-input { + background-color: transparent; + color: black; } .bn-ak-tab:where(.dark, .dark *), .bn-ariakit .bn-file-input:where(.dark, .dark *) { - color: white; + color: white; } .bn-ak-tooltip { - align-items: center; - display: flex; - flex-direction: column; + align-items: center; + display: flex; + flex-direction: column; } diff --git a/packages/ariakit/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx new file mode 100644 index 0000000000..62dbf5caa0 --- /dev/null +++ b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx @@ -0,0 +1,21 @@ +import { assertEmpty, mergeCSSClasses } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenuEmptyItem = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["EmptyItem"] +>((props, ref) => { + const { className, children, columns, ...rest } = props; + + assertEmpty(rest); + + return ( +
+
{children}
+
+ ); +}); diff --git a/packages/ariakit/src/suggestionMenu/GridSuggestionMenuLoader.tsx b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuLoader.tsx new file mode 100644 index 0000000000..ba10d6c03b --- /dev/null +++ b/packages/ariakit/src/suggestionMenu/GridSuggestionMenuLoader.tsx @@ -0,0 +1,26 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenuLoader = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Loader"] +>((props, ref) => { + const { + className, + children, // unused, using "dots" instead + columns, + ...rest + } = props; + + assertEmpty(rest); + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index 34ff30e7e7..54cd54390c 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -20,7 +20,9 @@ import { removeBlockNoteCSSVariables, } from "./BlockNoteTheme"; import { GridSuggestionMenu } from "./suggestionMenu/GridSuggestionMenu"; +import { GridSuggestionMenuEmptyItem } from "./suggestionMenu/GridSuggestionMenuEmptyItem"; import { GridSuggestionMenuItem } from "./suggestionMenu/GridSuggestionMenuItem"; +import { GridSuggestionMenuLoader } from "./suggestionMenu/GridSuggestionMenuLoader"; import { Menu, MenuDivider, @@ -50,9 +52,6 @@ import { ToolbarSelect } from "./toolbar/ToolbarSelect"; import "./style.css"; -export * from "./BlockNoteTheme"; -export * from "./defaultThemes"; - export const components: Components = { FormattingToolbar: { Root: Toolbar, @@ -69,6 +68,8 @@ export const components: Components = { GridSuggestionMenu: { Root: GridSuggestionMenu, Item: GridSuggestionMenuItem, + EmptyItem: GridSuggestionMenuEmptyItem, + Loader: GridSuggestionMenuLoader, }, LinkToolbar: { Root: Toolbar, diff --git a/packages/mantine/src/style.css b/packages/mantine/src/style.css index 0c7e30bcfa..a436a81ba4 100644 --- a/packages/mantine/src/style.css +++ b/packages/mantine/src/style.css @@ -299,6 +299,7 @@ box-shadow: var(--bn-shadow-medium); box-sizing: border-box; color: var(--bn-colors-menu-text); + height: fit-content; overflow-y: auto; padding: 2px; } @@ -368,9 +369,10 @@ box-shadow: var(--bn-shadow-medium); display: grid; gap: 7px; + height: fit-content; justify-items: center; - max-height: 30vh; - overflow-y: scroll; + max-height: min(500px, 100%); + overflow-y: auto; padding: 20px; } @@ -392,6 +394,21 @@ background-color: var(--bn-colors-hovered-background); } +.bn-mantine .bn-grid-suggestion-menu-empty-item, +.bn-mantine .bn-grid-suggestion-menu-loader{ + align-items: center; + color: var(--bn-colors-menu-text); + display: flex; + font-size: 14px; + font-weight: 500; + height: 32px; + justify-content: center; +} + +.bn-mantine .bn-grid-suggestion-menu-loader span { + background-color: var(--bn-colors-side-menu); +} + /* Side Menu styling */ .bn-mantine .bn-side-menu { background-color: transparent; diff --git a/packages/mantine/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx b/packages/mantine/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx new file mode 100644 index 0000000000..5b13c7d118 --- /dev/null +++ b/packages/mantine/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx @@ -0,0 +1,25 @@ +import { Group as MantineGroup } from "@mantine/core"; + +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenuEmptyItem = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["EmptyItem"] +>((props, ref) => { + const { className, children, columns, ...rest } = props; + + assertEmpty(rest); + + return ( + + + {children} + + + ); +}); diff --git a/packages/mantine/src/suggestionMenu/GridSuggestionMenuLoader.tsx b/packages/mantine/src/suggestionMenu/GridSuggestionMenuLoader.tsx new file mode 100644 index 0000000000..8ad468a75d --- /dev/null +++ b/packages/mantine/src/suggestionMenu/GridSuggestionMenuLoader.tsx @@ -0,0 +1,28 @@ +import { Loader as MantineLoader } from "@mantine/core"; + +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenuLoader = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Loader"] +>((props, ref) => { + const { + className, + children, // unused, using "dots" instead + columns, + ...rest + } = props; + + assertEmpty(rest); + + return ( + + ); +}); diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx index 90d459f00e..4ab6ce5015 100644 --- a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu.tsx @@ -1,29 +1,25 @@ import { useMemo } from "react"; import { useComponentsContext } from "../../editor/ComponentsContext"; -// import { useDictionary } from "../../i18n/dictionary"; +import { useDictionary } from "../../i18n/dictionary"; import { DefaultReactGridSuggestionItem, SuggestionMenuProps } from "./types"; export function GridSuggestionMenu( props: SuggestionMenuProps ) { const Components = useComponentsContext()!; - // const dict = useDictionary(); + const dict = useDictionary(); - const { - items, - // loadingState, - selectedIndex, - onItemClick, - columns, - } = props; + const { items, loadingState, selectedIndex, onItemClick, columns } = props; - // const loader = - // loadingState === "loading-initial" || loadingState === "loading" ? ( - // - // {dict.suggestion_menu.loading} - // - // ) : null; + const loader = + loadingState === "loading-initial" || loadingState === "loading" ? ( + + {dict.suggestion_menu.loading} + + ) : null; const renderedItems = useMemo(() => { // let currentGroup: string | undefined = undefined; @@ -62,16 +58,15 @@ export function GridSuggestionMenu( id="bn-grid-suggestion-menu" columns={columns} className="bn-grid-suggestion-menu"> + {loader} {renderedItems} - {/*{renderedItems.length === 0 &&*/} - {/* (props.loadingState === "loading" ||*/} - {/* props.loadingState === "loaded") && (*/} - {/* */} - {/* {dict.suggestion_menu.no_items_title}*/} - {/* */} - {/* )}*/} - {/*{loader}*/} + {renderedItems.length === 0 && props.loadingState === "loaded" && ( + + {dict.suggestion_menu.no_items_title} + + )} ); } diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 1f6162260b..070e89dc90 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -150,7 +150,7 @@ export function SuggestionMenuController< size({ apply({ availableHeight, elements }) { Object.assign(elements.floating.style, { - maxHeight: `${availableHeight - 10}px`, + height: `${availableHeight - 10}px`, }); }, }), diff --git a/packages/react/src/editor/ComponentsContext.tsx b/packages/react/src/editor/ComponentsContext.tsx index 05266f1476..8b043f39f1 100644 --- a/packages/react/src/editor/ComponentsContext.tsx +++ b/packages/react/src/editor/ComponentsContext.tsx @@ -152,10 +152,11 @@ export type ComponentProps = { className?: string; children?: ReactNode; }; - // EmptyItem: { - // className?: string; - // children?: ReactNode; - // }; + EmptyItem: { + columns?: number; + className?: string; + children?: ReactNode; + }; Item: { className?: string; id: string; @@ -167,10 +168,11 @@ export type ComponentProps = { // className?: string; // children?: ReactNode; // }; - // Loader: { - // className?: string; - // children?: ReactNode; - // }; + Loader: { + columns?: number; + className?: string; + children?: ReactNode; + }; }; TableHandle: { Root: { diff --git a/packages/shadcn/src/index.tsx b/packages/shadcn/src/index.tsx index c4a26afd74..4c0d1bf52e 100644 --- a/packages/shadcn/src/index.tsx +++ b/packages/shadcn/src/index.tsx @@ -17,8 +17,10 @@ import { ShadCNDefaultComponents, } from "./ShadCNComponentsContext"; import { Form } from "./form/Form"; -import {GridSuggestionMenu} from "@/suggestionMenu/GridSuggestionMenu"; -import { GridSuggestionMenuItem } from "@/suggestionMenu/GridSuggestionMenuItem"; +import { GridSuggestionMenu } from "./suggestionMenu/GridSuggestionMenu"; +import { GridSuggestionMenuEmptyItem } from "./suggestionMenu/GridSuggestionMenuEmptyItem"; +import { GridSuggestionMenuItem } from "./suggestionMenu/GridSuggestionMenuItem"; +import { GridSuggestionMenuLoader } from "./suggestionMenu/GridSuggestionMenuLoader"; import { Menu, MenuDivider, @@ -77,6 +79,8 @@ export const components: Components = { GridSuggestionMenu: { Root: GridSuggestionMenu, Item: GridSuggestionMenuItem, + EmptyItem: GridSuggestionMenuEmptyItem, + Loader: GridSuggestionMenuLoader, }, TableHandle: { Root: TableHandle, diff --git a/packages/shadcn/src/style.css b/packages/shadcn/src/style.css index af968e613e..bd507e628f 100644 --- a/packages/shadcn/src/style.css +++ b/packages/shadcn/src/style.css @@ -105,6 +105,11 @@ overflow: auto; } +.bn-shadcn .bn-suggestion-menu { + height: fit-content; + max-height: 100%; +} + .bn-shadcn .bn-suggestion-menu-item[aria-selected="true"], .bn-shadcn .bn-suggestion-menu-item:hover { background-color: hsl(var(--accent)); @@ -116,9 +121,10 @@ box-shadow: var(--bn-shadow-medium); display: grid; gap: 7px; + height: fit-content; justify-items: center; - max-height: 30vh; - overflow-y: scroll; + max-height: min(500px, 100%); + overflow-y: auto; padding: 20px; } @@ -139,3 +145,18 @@ .bn-shadcn .bn-grid-suggestion-menu-item:hover { background-color: var(--bn-colors-hovered-background); } + +.bn-shadcn .bn-grid-suggestion-menu-empty-item, +.bn-shadcn .bn-grid-suggestion-menu-loader{ + align-items: center; + color: var(--bn-colors-menu-text); + display: flex; + font-size: 14px; + font-weight: 500; + height: 32px; + justify-content: center; +} + +.bn-shadcn .bn-grid-suggestion-menu-loader span { + background-color: hsl(var(--accent)); +} diff --git a/packages/shadcn/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx new file mode 100644 index 0000000000..a3bd3929cf --- /dev/null +++ b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuEmptyItem.tsx @@ -0,0 +1,21 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenuEmptyItem = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["EmptyItem"] +>((props, ref) => { + const { className, children, columns, ...rest } = props; + + assertEmpty(rest); + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/shadcn/src/suggestionMenu/GridSuggestionMenuLoader.tsx b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuLoader.tsx new file mode 100644 index 0000000000..ba10d6c03b --- /dev/null +++ b/packages/shadcn/src/suggestionMenu/GridSuggestionMenuLoader.tsx @@ -0,0 +1,26 @@ +import { assertEmpty } from "@blocknote/core"; +import { ComponentProps } from "@blocknote/react"; +import { forwardRef } from "react"; + +export const GridSuggestionMenuLoader = forwardRef< + HTMLDivElement, + ComponentProps["GridSuggestionMenu"]["Loader"] +>((props, ref) => { + const { + className, + children, // unused, using "dots" instead + columns, + ...rest + } = props; + + assertEmpty(rest); + + return ( +
+ {children} +
+ ); +}); diff --git a/packages/shadcn/src/suggestionMenu/SuggestionMenuEmptyItem.tsx b/packages/shadcn/src/suggestionMenu/SuggestionMenuEmptyItem.tsx index 8ec2dcd780..09f51e1868 100644 --- a/packages/shadcn/src/suggestionMenu/SuggestionMenuEmptyItem.tsx +++ b/packages/shadcn/src/suggestionMenu/SuggestionMenuEmptyItem.tsx @@ -1,8 +1,8 @@ +import { assertEmpty } from "@blocknote/core"; import { ComponentProps } from "@blocknote/react"; import { forwardRef } from "react"; import { cn } from "../lib/utils"; -import { assertEmpty } from "@blocknote/core"; export const SuggestionMenuEmptyItem = forwardRef< HTMLDivElement, From 6446f225aef20736038fe8b4bcb55cca73eedc06 Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Mon, 8 Jul 2024 01:10:17 +0200 Subject: [PATCH 27/41] Small fix --- packages/mantine/src/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/mantine/src/index.tsx b/packages/mantine/src/index.tsx index 54cd54390c..17577e6af6 100644 --- a/packages/mantine/src/index.tsx +++ b/packages/mantine/src/index.tsx @@ -49,9 +49,11 @@ import { TextInput } from "./form/TextInput"; import { Toolbar } from "./toolbar/Toolbar"; import { ToolbarButton } from "./toolbar/ToolbarButton"; import { ToolbarSelect } from "./toolbar/ToolbarSelect"; - import "./style.css"; +export * from "./BlockNoteTheme"; +export * from "./defaultThemes"; + export const components: Components = { FormattingToolbar: { Root: Toolbar, From f0e776f955136715b2242d04025ed28b61b8479f Mon Sep 17 00:00:00 2001 From: matthewlipski Date: Tue, 9 Jul 2024 03:00:58 +0200 Subject: [PATCH 28/41] Added docs & small fixes --- docs/pages/docs/editor-basics/setup.mdx | 2 + .../docs/ui-components/suggestion-menus.mdx | 40 ++++++- docs/public/img/screenshots/emoji_picker.png | Bin 0 -> 285241 bytes .../img/screenshots/emoji_picker_dark.png | Bin 0 -> 279227 bytes .../styles.css | 4 + .../.bnexample.json | 12 ++ .../App.tsx | 38 ++++++ .../README.md | 10 ++ .../index.html | 14 +++ .../main.tsx | 0 .../package.json | 37 ++++++ .../tsconfig.json | 0 .../vite.config.ts | 0 .../.bnexample.json | 12 ++ .../App.tsx | 70 ++++++++++++ .../README.md | 10 ++ .../index.html | 14 +++ .../main.tsx | 11 ++ .../package.json | 37 ++++++ .../styles.css | 41 +++++++ .../tsconfig.json | 36 ++++++ .../vite.config.ts | 32 ++++++ .../.bnexample.json | 11 ++ .../10-suggestion-menus-grid-mentions/App.tsx | 108 ++++++++++++++++++ .../Mention.tsx | 21 ++++ .../README.md | 11 ++ .../index.html | 14 +++ .../main.tsx | 11 ++ .../package.json | 37 ++++++ .../tsconfig.json | 36 ++++++ .../vite.config.ts | 32 ++++++ .../.bnexample.json | 0 .../{08-custom-ui => 11-custom-ui}/App.tsx | 0 .../ColorMenu.tsx | 0 .../CustomFormattingToolbar.tsx | 0 .../CustomSideMenu.tsx | 0 .../CustomSlashMenu.tsx | 0 .../LinkMenu.tsx | 0 .../{08-custom-ui => 11-custom-ui}/README.md | 0 .../{08-custom-ui => 11-custom-ui}/index.html | 0 .../02-ui-components/11-custom-ui/main.tsx | 11 ++ .../package.json | 0 .../{08-custom-ui => 11-custom-ui}/styles.css | 0 .../11-custom-ui/tsconfig.json | 36 ++++++ .../11-custom-ui/vite.config.ts | 32 ++++++ .../SuggestionMenuController.tsx | 5 +- .../react/src/editor/BlockNoteDefaultUI.tsx | 2 +- packages/react/src/editor/BlockNoteView.tsx | 2 + playground/src/examples.gen.tsx | 67 ++++++++++- 49 files changed, 849 insertions(+), 7 deletions(-) create mode 100644 docs/public/img/screenshots/emoji_picker.png create mode 100644 docs/public/img/screenshots/emoji_picker_dark.png create mode 100644 examples/02-ui-components/08-suggestion-menus-emoji-picker-columns/.bnexample.json create mode 100644 examples/02-ui-components/08-suggestion-menus-emoji-picker-columns/App.tsx create mode 100644 examples/02-ui-components/08-suggestion-menus-emoji-picker-columns/README.md create mode 100644 examples/02-ui-components/08-suggestion-menus-emoji-picker-columns/index.html rename examples/02-ui-components/{08-custom-ui => 08-suggestion-menus-emoji-picker-columns}/main.tsx (100%) create mode 100644 examples/02-ui-components/08-suggestion-menus-emoji-picker-columns/package.json rename examples/02-ui-components/{08-custom-ui => 08-suggestion-menus-emoji-picker-columns}/tsconfig.json (100%) rename examples/02-ui-components/{08-custom-ui => 08-suggestion-menus-emoji-picker-columns}/vite.config.ts (100%) create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/.bnexample.json create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/App.tsx create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/README.md create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/index.html create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/main.tsx create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/package.json create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/styles.css create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/tsconfig.json create mode 100644 examples/02-ui-components/09-suggestion-menus-emoji-picker-component/vite.config.ts create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/.bnexample.json create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/App.tsx create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/Mention.tsx create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/README.md create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/index.html create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/main.tsx create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/package.json create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/tsconfig.json create mode 100644 examples/02-ui-components/10-suggestion-menus-grid-mentions/vite.config.ts rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/.bnexample.json (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/App.tsx (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/ColorMenu.tsx (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/CustomFormattingToolbar.tsx (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/CustomSideMenu.tsx (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/CustomSlashMenu.tsx (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/LinkMenu.tsx (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/README.md (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/index.html (100%) create mode 100644 examples/02-ui-components/11-custom-ui/main.tsx rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/package.json (100%) rename examples/02-ui-components/{08-custom-ui => 11-custom-ui}/styles.css (100%) create mode 100644 examples/02-ui-components/11-custom-ui/tsconfig.json create mode 100644 examples/02-ui-components/11-custom-ui/vite.config.ts diff --git a/docs/pages/docs/editor-basics/setup.mdx b/docs/pages/docs/editor-basics/setup.mdx index e16960f469..0df6f8067c 100644 --- a/docs/pages/docs/editor-basics/setup.mdx +++ b/docs/pages/docs/editor-basics/setup.mdx @@ -120,6 +120,8 @@ export type BlockNoteViewProps = { `slashMenu`: Whether the [Slash Menu](/docs/ui-components/suggestion-menus#slash-menu) should be enabled. +`emojiPicker`: Whether the [Emoji Picker](/docs/ui-components/suggestion-menus#emoji-picker) should be enabled. + `filePanel`: Whether the File Toolbar should be enabled. `tableHandles`: Whether the Table Handles should be enabled. diff --git a/docs/pages/docs/ui-components/suggestion-menus.mdx b/docs/pages/docs/ui-components/suggestion-menus.mdx index e6ed27d4d8..e4926cd3f8 100644 --- a/docs/pages/docs/ui-components/suggestion-menus.mdx +++ b/docs/pages/docs/ui-components/suggestion-menus.mdx @@ -31,9 +31,9 @@ Passing `slashMenu={false}` to `BlockNoteView` tells BlockNote not to show the d `getItems` should return the items that need to be shown in the Slash Menu, based on a `query` entered by the user (anything the user types after the `triggerCharacter`). -### Replacing the Suggestion Menu Component +### Replacing the Slash Menu Component -You can replace the React component used for the Suggestion Menu with your own, as you can see in the demo below. +You can replace the React component used for the Slash Menu with your own, as you can see in the demo below. @@ -41,6 +41,34 @@ Again, we add a `SuggestionMenuController` component with `triggerCharacter={"/" Now, we also pass a component to its `suggestionMenuComponent` prop. The `suggestionMenuComponent` we pass is responsible for rendering the filtered items. The `SuggestionMenuController` controls its position and visibility (below the trigger character), and it also determines which items should be shown (using the optional `getItems` prop we've seen above). +## Emoji Picker + +The Emoji Picker is a Suggestion Menu that opens with the `:` character (or when selecting emoji item in the Slash Menu). + +image + +### Changing Emoji Picker Columns + +By default, the Emoji Picker is rendered with 10 columns, but you can change this to any amount. In the demo below, the Emoji Picker is changed to only display 5 columns. + + + +Passing `emojiPicker={false}` to `BlockNoteView` tells BlockNote not to show the default Emoji Picker. Adding the `SuggestionMenuController` with `triggerCharacter={":"}` and `columns={5}` tells BlockNote to show one with 5 columns instead. + +### Replacing the Emoji Picker Component + +You can replace the React component used for the Emoji Picker with your own, as you can see in the demo below. + + + +Again, we add a `SuggestionMenuController` component with `triggerCharacter={":"}` and set `slashMenu={false}` to replace the default Slash Menu. We also set the `columns` prop so the `SuggestionMenuController` knows to render a grid. + +Now, we also pass a component to its `suggestionMenuComponent` prop. The `suggestionMenuComponent` we pass is responsible for rendering the filtered items. The `SuggestionMenuController` controls its position and visibility (below the trigger character), and it also determines which items should be shown. Since we don't specify which items to show (the `getItems` prop isn't defined), it will use the default items for a grid, which are the emojis. + ## Creating additional Suggestion Menus You can add additional Suggestion Menus to the editor, which can use any trigger character. The demo below adds an example Suggestion Menu for mentions, which opens with the `@` character. @@ -48,3 +76,11 @@ You can add additional Suggestion Menus to the editor, which can use any trigger Changing the items in the new Suggestion Menu, or the component used to render it, is done the same way as for the [Slash Menu](/docs/ui-components/suggestion-menus#slash-menu). For more information about how the mentions elements work, see [Custom Inline Content](/docs/custom-schemas/custom-inline-content). + +### Grid Suggestion Menus + +You may have noticed that the Emoji Picker is rendered using a grid instead of a stack. You can make your custom Suggestion Menus also render as a grid by passing a number to the `columns` prop in `SuggestionMenuController`. The demo below also adds a Suggestion Menu for mentions, but as a grid with only the first letters of names. + + + +Keep in mind that while we used the `DefaultReactSuggestionMenuItem` type in previous demos where we were setting the `getItems` prop, we're now using `DefaultReactGridSuggestionMenuItem`. diff --git a/docs/public/img/screenshots/emoji_picker.png b/docs/public/img/screenshots/emoji_picker.png new file mode 100644 index 0000000000000000000000000000000000000000..d912a299deeaefc36ccd3a5c9a5a96ddcc49bf0d GIT binary patch literal 285241 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
}W|S^^3ss2{$z)ZGDbQ$xI+Tb( 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