chore: upgrade lexical & make linting/formatting consistent with web codebase (#2144)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/eslint-plugin-react-npm-7.32.0-60a40d5ae1-b81ce2623b.zip
vendored
Normal file
BIN
.yarn/cache/eslint-plugin-react-npm-7.32.0-60a40d5ae1-b81ce2623b.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/lexical-npm-0.7.6-a76c7e85e6-594423da85.zip
vendored
Normal file
BIN
.yarn/cache/lexical-npm-0.7.6-a76c7e85e6-594423da85.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,254 +1,18 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
const restrictedGlobals = require('confusing-browser-globals');
|
|
||||||
|
|
||||||
const OFF = 0;
|
|
||||||
const ERROR = 2;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
// Prettier must be last so it can override other configs (https://github.com/prettier/eslint-config-prettier#installation)
|
extends: ['../../common.eslintrc.js', 'plugin:react-hooks/recommended'],
|
||||||
extends: [
|
parserOptions: {
|
||||||
'fbjs',
|
project: './tsconfig.json',
|
||||||
'plugin:react-hooks/recommended',
|
tsconfigRootDir: __dirname,
|
||||||
'plugin:lexical/all',
|
},
|
||||||
'prettier',
|
ignorePatterns: ['**/*.spec.ts', '__mocks__'],
|
||||||
],
|
plugins: ['@typescript-eslint', 'react', 'react-hooks', 'prettier'],
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
globals: {
|
globals: {
|
||||||
|
__WEB_VERSION__: true,
|
||||||
JSX: true,
|
JSX: true,
|
||||||
__DEV__: true,
|
__DEV__: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
// We apply these settings to the source files that get compiled.
|
|
||||||
// They can use all features including JSX (but shouldn't use `var`).
|
|
||||||
files: [
|
|
||||||
'packages/*/src/**/*.js',
|
|
||||||
'packages/*/__tests__/**/*.?(m)js',
|
|
||||||
'packages/*/src/**/*.jsx',
|
|
||||||
],
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
parserOptions: {
|
|
||||||
allowImportExportEverywhere: true,
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
'no-var': ERROR,
|
|
||||||
'prefer-const': ERROR,
|
|
||||||
strict: OFF,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// node scripts should be console logging so don't lint against that
|
|
||||||
files: ['scripts/**/*.js'],
|
|
||||||
rules: {
|
|
||||||
'no-console': OFF,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/eslint-recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
],
|
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: ['react', '@typescript-eslint', 'header'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/ban-ts-comment': OFF,
|
|
||||||
'@typescript-eslint/no-this-alias': OFF,
|
|
||||||
'@typescript-eslint/no-unused-vars': [ERROR, {args: 'none'}],
|
|
||||||
'header/header': [2, 'scripts/www/headerTemplate.js'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// don't lint headers in entrypoint files so we can add TypeDoc module comments
|
|
||||||
files: ['packages/**/src/index.ts'],
|
|
||||||
rules: {
|
|
||||||
'header/header': OFF,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: [
|
|
||||||
'packages/**/src/__tests__/**',
|
|
||||||
'packages/lexical-playground/**',
|
|
||||||
'packages/lexical-devtools/**',
|
|
||||||
],
|
|
||||||
rules: {
|
|
||||||
'lexical/no-optional-chaining': OFF,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
parser: 'babel-eslint',
|
|
||||||
|
|
||||||
parserOptions: {
|
|
||||||
ecmaFeatures: {
|
|
||||||
experimentalObjectRestSpread: true,
|
|
||||||
},
|
|
||||||
ecmaVersion: 8,
|
|
||||||
sourceType: 'script',
|
|
||||||
},
|
|
||||||
|
|
||||||
plugins: [
|
|
||||||
'sort-keys-fix',
|
|
||||||
'simple-import-sort',
|
|
||||||
'header',
|
|
||||||
|
|
||||||
// import helps to configure simple-import-sort
|
|
||||||
'import',
|
|
||||||
'jest',
|
|
||||||
'no-function-declare-after-return',
|
|
||||||
'react',
|
|
||||||
'no-only-tests',
|
|
||||||
'lexical',
|
|
||||||
],
|
|
||||||
|
|
||||||
// Stop ESLint from looking for a configuration file in parent folders
|
|
||||||
root: true,
|
|
||||||
// We're stricter than the default config, mostly. We'll override a few rules
|
|
||||||
// and then enable some React specific ones.
|
|
||||||
rules: {
|
|
||||||
'accessor-pairs': OFF,
|
|
||||||
|
|
||||||
'brace-style': [ERROR, '1tbs'],
|
|
||||||
'consistent-return': OFF,
|
|
||||||
'dot-location': [ERROR, 'property'],
|
|
||||||
// We use console['error']() as a signal to not transform it:
|
|
||||||
'dot-notation': [ERROR, {allowPattern: '^(error|warn)$'}],
|
|
||||||
|
|
||||||
'eol-last': ERROR,
|
|
||||||
eqeqeq: [ERROR, 'allow-null'],
|
|
||||||
// Prettier forces semicolons in a few places
|
|
||||||
'flowtype/object-type-delimiter': OFF,
|
|
||||||
|
|
||||||
'flowtype/sort-keys': ERROR,
|
|
||||||
|
|
||||||
'header/header': [2, 'scripts/www/headerTemplate.js'],
|
|
||||||
|
|
||||||
// (This helps configure simple-import-sort) Make sure all imports are at the top of the file
|
|
||||||
'import/first': ERROR,
|
|
||||||
|
|
||||||
// (This helps configure simple-import-sort) Make sure there's a newline after the imports
|
|
||||||
'import/newline-after-import': ERROR,
|
|
||||||
|
|
||||||
// (This helps configure simple-import-sort) Merge imports of the same file
|
|
||||||
'import/no-duplicates': ERROR,
|
|
||||||
|
|
||||||
indent: OFF,
|
|
||||||
|
|
||||||
'jsx-quotes': [ERROR, 'prefer-double'],
|
|
||||||
|
|
||||||
'keyword-spacing': [ERROR, {after: true, before: true}],
|
|
||||||
|
|
||||||
// Enforced by Prettier
|
|
||||||
// TODO: Prettier doesn't handle long strings or long comments. Not a big
|
|
||||||
// deal. But I turned it off because loading the plugin causes some obscure
|
|
||||||
// syntax error and it didn't seem worth investigating.
|
|
||||||
'max-len': OFF,
|
|
||||||
|
|
||||||
'no-bitwise': OFF,
|
|
||||||
|
|
||||||
'no-console': ERROR,
|
|
||||||
|
|
||||||
'no-debugger': ERROR,
|
|
||||||
|
|
||||||
// Prevent function declarations after return statements
|
|
||||||
'no-function-declare-after-return/no-function-declare-after-return': ERROR,
|
|
||||||
|
|
||||||
'no-inner-declarations': [ERROR, 'functions'],
|
|
||||||
|
|
||||||
'no-multi-spaces': ERROR,
|
|
||||||
|
|
||||||
'no-only-tests/no-only-tests': ERROR,
|
|
||||||
|
|
||||||
'no-restricted-globals': [ERROR].concat(restrictedGlobals),
|
|
||||||
|
|
||||||
'no-restricted-syntax': [ERROR, 'WithStatement'],
|
|
||||||
|
|
||||||
'no-shadow': ERROR,
|
|
||||||
|
|
||||||
'no-unused-expressions': ERROR,
|
|
||||||
|
|
||||||
'no-unused-vars': [ERROR, {args: 'none'}],
|
|
||||||
|
|
||||||
'no-use-before-define': OFF,
|
|
||||||
|
|
||||||
// Flow fails with with non-string literal keys
|
|
||||||
'no-useless-computed-key': OFF,
|
|
||||||
|
|
||||||
'no-useless-concat': OFF,
|
|
||||||
|
|
||||||
// We apply these settings to files that should run on Node.
|
|
||||||
// They can't use JSX or ES6 modules, and must be in strict mode.
|
|
||||||
// They can, however, use other ES6 features.
|
|
||||||
// (Note these rules are overridden later for source files.)
|
|
||||||
'no-var': ERROR,
|
|
||||||
|
|
||||||
quotes: [ERROR, 'single', {allowTemplateLiterals: true, avoidEscape: true}],
|
|
||||||
|
|
||||||
// React & JSX
|
|
||||||
// Our transforms set this automatically
|
|
||||||
'react/jsx-boolean-value': [ERROR, 'always'],
|
|
||||||
|
|
||||||
'react/jsx-no-undef': ERROR,
|
|
||||||
|
|
||||||
// We don't care to do this
|
|
||||||
'react/jsx-sort-prop-types': OFF,
|
|
||||||
|
|
||||||
'react/jsx-tag-spacing': ERROR,
|
|
||||||
|
|
||||||
'react/jsx-uses-react': ERROR,
|
|
||||||
|
|
||||||
// We don't care to do this
|
|
||||||
'react/jsx-wrap-multilines': [
|
|
||||||
ERROR,
|
|
||||||
{assignment: false, declaration: false},
|
|
||||||
],
|
|
||||||
|
|
||||||
'react/no-is-mounted': OFF,
|
|
||||||
|
|
||||||
// This isn't useful in our test code
|
|
||||||
'react/react-in-jsx-scope': ERROR,
|
|
||||||
|
|
||||||
'react/self-closing-comp': ERROR,
|
|
||||||
|
|
||||||
// This sorts re-exports (`export * from 'foo';`), but not other types of exports.
|
|
||||||
'simple-import-sort/exports': ERROR,
|
|
||||||
|
|
||||||
'simple-import-sort/imports': [
|
|
||||||
ERROR,
|
|
||||||
{
|
|
||||||
// The default grouping, but with type imports first as a separate group.
|
|
||||||
// See: https://github.com/lydell/eslint-plugin-simple-import-sort/blob/d9a116f71302c5dcfc1581fc7ded8d77392f1924/examples/.eslintrc.js#L122-L133
|
|
||||||
groups: [['^.*\\u0000$'], ['^\\u0000'], ['^@?\\w'], ['^'], ['^\\.']],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
'sort-keys-fix/sort-keys-fix': ERROR,
|
|
||||||
|
|
||||||
'space-before-blocks': ERROR,
|
|
||||||
|
|
||||||
'space-before-function-paren': OFF,
|
|
||||||
|
|
||||||
strict: ERROR,
|
|
||||||
|
|
||||||
'valid-typeof': [ERROR, {requireStringLiterals: true}],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
bracketSpacing: false,
|
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
bracketSameLine: true,
|
|
||||||
printWidth: 80,
|
|
||||||
trailingComma: 'all',
|
trailingComma: 'all',
|
||||||
htmlWhitespaceSensitivity: 'ignore',
|
printWidth: 120,
|
||||||
attributeGroups: ['$DEFAULT', '^data-'],
|
semi: false,
|
||||||
|
plugins: [require('prettier-plugin-tailwindcss')],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,20 +4,25 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc -p tsconfig.json"
|
"tsc": "tsc -p tsconfig.json",
|
||||||
|
"format": "prettier --write src/",
|
||||||
|
"lint:fix": "eslint src/ --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lexical/react": "0.7.5",
|
"@lexical/react": "0.7.6",
|
||||||
"@standardnotes/icons": "workspace:*",
|
"@standardnotes/icons": "workspace:*",
|
||||||
"@types/react": "^18.0.26",
|
"@types/react": "^18.0.26",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-dom": "^18.0.9",
|
||||||
"lexical": "0.7.5",
|
"lexical": "0.7.6",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "*",
|
"eslint": "*",
|
||||||
|
"eslint-plugin-react": "*",
|
||||||
|
"eslint-plugin-react-hooks": "*",
|
||||||
"prettier": "*",
|
"prettier": "*",
|
||||||
|
"prettier-plugin-tailwindcss": "*",
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import {FunctionComponent, useCallback, useState} from 'react';
|
import { FunctionComponent, useCallback, useState } from 'react'
|
||||||
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
|
||||||
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable'
|
||||||
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
||||||
import {CheckListPlugin} from '@lexical/react/LexicalCheckListPlugin';
|
import { CheckListPlugin } from '@lexical/react/LexicalCheckListPlugin'
|
||||||
import {ClearEditorPlugin} from '@lexical/react/LexicalClearEditorPlugin';
|
import { ClearEditorPlugin } from '@lexical/react/LexicalClearEditorPlugin'
|
||||||
import {MarkdownShortcutPlugin} from '@lexical/react/LexicalMarkdownShortcutPlugin';
|
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'
|
||||||
import {TablePlugin} from '@lexical/react/LexicalTablePlugin';
|
import { TablePlugin } from '@lexical/react/LexicalTablePlugin'
|
||||||
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
|
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
|
||||||
import {HashtagPlugin} from '@lexical/react/LexicalHashtagPlugin';
|
import { HashtagPlugin } from '@lexical/react/LexicalHashtagPlugin'
|
||||||
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
||||||
import {LinkPlugin} from '@lexical/react/LexicalLinkPlugin';
|
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin'
|
||||||
import {ListPlugin} from '@lexical/react/LexicalListPlugin';
|
import { ListPlugin } from '@lexical/react/LexicalListPlugin'
|
||||||
import {$getRoot, EditorState, LexicalEditor} from 'lexical';
|
import { $getRoot, EditorState, LexicalEditor } from 'lexical'
|
||||||
import HorizontalRulePlugin from '../Lexical/Plugins/HorizontalRulePlugin';
|
import HorizontalRulePlugin from '../Lexical/Plugins/HorizontalRulePlugin'
|
||||||
import TwitterPlugin from '../Lexical/Plugins/TwitterPlugin';
|
import TwitterPlugin from '../Lexical/Plugins/TwitterPlugin'
|
||||||
import YouTubePlugin from '../Lexical/Plugins/YouTubePlugin';
|
import YouTubePlugin from '../Lexical/Plugins/YouTubePlugin'
|
||||||
import AutoEmbedPlugin from '../Lexical/Plugins/AutoEmbedPlugin';
|
import AutoEmbedPlugin from '../Lexical/Plugins/AutoEmbedPlugin'
|
||||||
import CollapsiblePlugin from '../Lexical/Plugins/CollapsiblePlugin';
|
import CollapsiblePlugin from '../Lexical/Plugins/CollapsiblePlugin'
|
||||||
import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin';
|
import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin'
|
||||||
import CodeHighlightPlugin from '../Lexical/Plugins/CodeHighlightPlugin';
|
import CodeHighlightPlugin from '../Lexical/Plugins/CodeHighlightPlugin'
|
||||||
import FloatingTextFormatToolbarPlugin from '../Lexical/Plugins/FloatingTextFormatToolbarPlugin';
|
import FloatingTextFormatToolbarPlugin from '../Lexical/Plugins/FloatingTextFormatToolbarPlugin'
|
||||||
import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugin';
|
import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugin'
|
||||||
import {TabIndentationPlugin} from '../Lexical/Plugins/TabIndentationPlugin';
|
import { TabIndentationPlugin } from '../Lexical/Plugins/TabIndentationPlugin'
|
||||||
import {truncateString} from './Utils';
|
import { truncateString } from './Utils'
|
||||||
import {SuperEditorContentId} from './Constants';
|
import { SuperEditorContentId } from './Constants'
|
||||||
import {classNames} from '@standardnotes/utils';
|
import { classNames } from '@standardnotes/utils'
|
||||||
import {MarkdownTransformers} from './MarkdownTransformers';
|
import { MarkdownTransformers } from './MarkdownTransformers'
|
||||||
|
|
||||||
type BlocksEditorProps = {
|
type BlocksEditorProps = {
|
||||||
onChange?: (value: string, preview: string) => void;
|
onChange?: (value: string, preview: string) => void
|
||||||
className?: string;
|
className?: string
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode
|
||||||
previewLength?: number;
|
previewLength?: number
|
||||||
spellcheck?: boolean;
|
spellcheck?: boolean
|
||||||
ignoreFirstChange?: boolean;
|
ignoreFirstChange?: boolean
|
||||||
readonly?: boolean;
|
readonly?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
@@ -46,49 +46,50 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
ignoreFirstChange = false,
|
ignoreFirstChange = false,
|
||||||
readonly,
|
readonly,
|
||||||
}) => {
|
}) => {
|
||||||
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false);
|
const [didIgnoreFirstChange, setDidIgnoreFirstChange] = useState(false)
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(editorState: EditorState, _editor: LexicalEditor) => {
|
(editorState: EditorState, _editor: LexicalEditor) => {
|
||||||
if (ignoreFirstChange && !didIgnoreFirstChange) {
|
if (ignoreFirstChange && !didIgnoreFirstChange) {
|
||||||
setDidIgnoreFirstChange(true);
|
setDidIgnoreFirstChange(true)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const childrenNodes = $getRoot().getAllTextNodes().slice(0, 2);
|
const childrenNodes = $getRoot().getAllTextNodes().slice(0, 2)
|
||||||
let previewText = '';
|
let previewText = ''
|
||||||
childrenNodes.forEach((node, index) => {
|
childrenNodes.forEach((node, index) => {
|
||||||
previewText += node.getTextContent();
|
previewText += node.getTextContent()
|
||||||
if (index !== childrenNodes.length - 1) {
|
if (index !== childrenNodes.length - 1) {
|
||||||
previewText += '\n';
|
previewText += '\n'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
if (previewLength) {
|
if (previewLength) {
|
||||||
previewText = truncateString(previewText, previewLength);
|
previewText = truncateString(previewText, previewLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stringifiedEditorState = JSON.stringify(editorState.toJSON());
|
const stringifiedEditorState = JSON.stringify(editorState.toJSON())
|
||||||
onChange?.(stringifiedEditorState, previewText);
|
onChange?.(stringifiedEditorState, previewText)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
window.alert(
|
window.alert(
|
||||||
`An invalid change was made inside the Super editor. Your change was not saved. Please report this error to the team: ${error}`,
|
`An invalid change was made inside the Super editor. Your change was not saved. Please report this error to the team: ${error}`,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
|
// Ignoring 'ignoreFirstChange' and 'previewLength'
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[onChange, didIgnoreFirstChange],
|
[onChange, didIgnoreFirstChange],
|
||||||
);
|
)
|
||||||
|
|
||||||
const [floatingAnchorElem, setFloatingAnchorElem] =
|
const [floatingAnchorElem, setFloatingAnchorElem] = useState<HTMLDivElement | null>(null)
|
||||||
useState<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
|
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
|
||||||
if (_floatingAnchorElem !== null) {
|
if (_floatingAnchorElem !== null) {
|
||||||
setFloatingAnchorElem(_floatingAnchorElem);
|
setFloatingAnchorElem(_floatingAnchorElem)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,10 +100,7 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
<div className="editor" ref={onRef}>
|
<div className="editor" ref={onRef}>
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
id={SuperEditorContentId}
|
id={SuperEditorContentId}
|
||||||
className={classNames(
|
className={classNames('ContentEditable__root overflow-y-auto', className)}
|
||||||
'ContentEditable__root overflow-y-auto',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
spellCheck={spellcheck}
|
spellCheck={spellcheck}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,5 +133,5 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
import {FunctionComponent} from 'react';
|
import { FunctionComponent } from 'react'
|
||||||
import {LexicalComposer} from '@lexical/react/LexicalComposer';
|
import { LexicalComposer } from '@lexical/react/LexicalComposer'
|
||||||
import BlocksEditorTheme from '../Lexical/Theme/Theme';
|
import BlocksEditorTheme from '../Lexical/Theme/Theme'
|
||||||
import {BlockEditorNodes} from '../Lexical/Nodes/AllNodes';
|
import { BlockEditorNodes } from '../Lexical/Nodes/AllNodes'
|
||||||
import {Klass, LexicalNode} from 'lexical';
|
import { Klass, LexicalNode } from 'lexical'
|
||||||
|
|
||||||
type BlocksEditorComposerProps = {
|
type BlocksEditorComposerProps = {
|
||||||
initialValue: string | undefined;
|
initialValue: string | undefined
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
nodes?: Array<Klass<LexicalNode>>;
|
nodes?: Array<Klass<LexicalNode>>
|
||||||
readonly?: boolean;
|
readonly?: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export const BlocksEditorComposer: FunctionComponent<
|
export const BlocksEditorComposer: FunctionComponent<BlocksEditorComposerProps> = ({
|
||||||
BlocksEditorComposerProps
|
initialValue,
|
||||||
> = ({initialValue, children, readonly, nodes = []}) => {
|
children,
|
||||||
|
readonly,
|
||||||
|
nodes = [],
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<LexicalComposer
|
<LexicalComposer
|
||||||
initialConfig={{
|
initialConfig={{
|
||||||
@@ -21,11 +24,11 @@ export const BlocksEditorComposer: FunctionComponent<
|
|||||||
theme: BlocksEditorTheme,
|
theme: BlocksEditorTheme,
|
||||||
editable: !readonly,
|
editable: !readonly,
|
||||||
onError: (error: Error) => console.error(error),
|
onError: (error: Error) => console.error(error),
|
||||||
editorState:
|
editorState: initialValue && initialValue.length > 0 ? initialValue : undefined,
|
||||||
initialValue && initialValue.length > 0 ? initialValue : undefined,
|
|
||||||
nodes: [...nodes, ...BlockEditorNodes],
|
nodes: [...nodes, ...BlockEditorNodes],
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export const SuperEditorContentId = 'super-editor-content';
|
export const SuperEditorContentId = 'super-editor-content'
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import {
|
import { CHECK_LIST, ELEMENT_TRANSFORMERS, TEXT_FORMAT_TRANSFORMERS, TEXT_MATCH_TRANSFORMERS } from '@lexical/markdown'
|
||||||
CHECK_LIST,
|
|
||||||
ELEMENT_TRANSFORMERS,
|
|
||||||
TEXT_FORMAT_TRANSFORMERS,
|
|
||||||
TEXT_MATCH_TRANSFORMERS,
|
|
||||||
} from '@lexical/markdown';
|
|
||||||
|
|
||||||
export const MarkdownTransformers = [
|
export const MarkdownTransformers = [
|
||||||
CHECK_LIST,
|
CHECK_LIST,
|
||||||
...ELEMENT_TRANSFORMERS,
|
...ELEMENT_TRANSFORMERS,
|
||||||
...TEXT_FORMAT_TRANSFORMERS,
|
...TEXT_FORMAT_TRANSFORMERS,
|
||||||
...TEXT_MATCH_TRANSFORMERS,
|
...TEXT_MATCH_TRANSFORMERS,
|
||||||
];
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
export function truncateString(string: string, limit: number) {
|
export function truncateString(string: string, limit: number) {
|
||||||
if (string.length <= limit) {
|
if (string.length <= limit) {
|
||||||
return string;
|
return string
|
||||||
} else {
|
} else {
|
||||||
return string.substring(0, limit) + '...';
|
return string.substring(0, limit) + '...'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,38 +6,35 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useCallback, useMemo, useState} from 'react';
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
|
||||||
import Modal from '../UI/Modal';
|
import Modal from '../UI/Modal'
|
||||||
|
|
||||||
export default function useModal(): [
|
export default function useModal(): [
|
||||||
JSX.Element | null,
|
JSX.Element | null,
|
||||||
(title: string, showModal: (onClose: () => void) => JSX.Element) => void,
|
(title: string, showModal: (onClose: () => void) => JSX.Element) => void,
|
||||||
] {
|
] {
|
||||||
const [modalContent, setModalContent] = useState<null | {
|
const [modalContent, setModalContent] = useState<null | {
|
||||||
closeOnClickOutside: boolean;
|
closeOnClickOutside: boolean
|
||||||
content: JSX.Element;
|
content: JSX.Element
|
||||||
title: string;
|
title: string
|
||||||
}>(null);
|
}>(null)
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
setModalContent(null);
|
setModalContent(null)
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
const modal = useMemo(() => {
|
const modal = useMemo(() => {
|
||||||
if (modalContent === null) {
|
if (modalContent === null) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
const {title, content, closeOnClickOutside} = modalContent;
|
const { title, content, closeOnClickOutside } = modalContent
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal onClose={onClose} title={title} closeOnClickOutside={closeOnClickOutside}>
|
||||||
onClose={onClose}
|
|
||||||
title={title}
|
|
||||||
closeOnClickOutside={closeOnClickOutside}>
|
|
||||||
{content}
|
{content}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
)
|
||||||
}, [modalContent, onClose]);
|
}, [modalContent, onClose])
|
||||||
|
|
||||||
const showModal = useCallback(
|
const showModal = useCallback(
|
||||||
(
|
(
|
||||||
@@ -50,10 +47,10 @@ export default function useModal(): [
|
|||||||
closeOnClickOutside,
|
closeOnClickOutside,
|
||||||
content: getContent(onClose),
|
content: getContent(onClose),
|
||||||
title,
|
title,
|
||||||
});
|
})
|
||||||
},
|
},
|
||||||
[onClose],
|
[onClose],
|
||||||
);
|
)
|
||||||
|
|
||||||
return [modal, showModal];
|
return [modal, showModal]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import {CodeHighlightNode, CodeNode} from '@lexical/code';
|
import { CodeHighlightNode, CodeNode } from '@lexical/code'
|
||||||
import {HashtagNode} from '@lexical/hashtag';
|
import { HashtagNode } from '@lexical/hashtag'
|
||||||
import {AutoLinkNode, LinkNode} from '@lexical/link';
|
import { AutoLinkNode, LinkNode } from '@lexical/link'
|
||||||
import {ListItemNode, ListNode} from '@lexical/list';
|
import { ListItemNode, ListNode } from '@lexical/list'
|
||||||
import {MarkNode} from '@lexical/mark';
|
import { MarkNode } from '@lexical/mark'
|
||||||
import {OverflowNode} from '@lexical/overflow';
|
import { OverflowNode } from '@lexical/overflow'
|
||||||
import {HorizontalRuleNode} from '@lexical/react/LexicalHorizontalRuleNode';
|
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode'
|
||||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text'
|
||||||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
|
import { TableCellNode, TableNode, TableRowNode } from '@lexical/table'
|
||||||
import {TweetNode} from './TweetNode';
|
import { TweetNode } from './TweetNode'
|
||||||
import {YouTubeNode} from './YouTubeNode';
|
import { YouTubeNode } from './YouTubeNode'
|
||||||
import {CollapsibleContainerNode} from '../Plugins/CollapsiblePlugin/CollapsibleContainerNode';
|
import { CollapsibleContainerNode } from '../Plugins/CollapsiblePlugin/CollapsibleContainerNode'
|
||||||
import {CollapsibleContentNode} from '../Plugins/CollapsiblePlugin/CollapsibleContentNode';
|
import { CollapsibleContentNode } from '../Plugins/CollapsiblePlugin/CollapsibleContentNode'
|
||||||
import {CollapsibleTitleNode} from '../Plugins/CollapsiblePlugin/CollapsibleTitleNode';
|
import { CollapsibleTitleNode } from '../Plugins/CollapsiblePlugin/CollapsibleTitleNode'
|
||||||
|
|
||||||
export const BlockEditorNodes = [
|
export const BlockEditorNodes = [
|
||||||
AutoLinkNode,
|
AutoLinkNode,
|
||||||
@@ -34,4 +34,4 @@ export const BlockEditorNodes = [
|
|||||||
TableRowNode,
|
TableRowNode,
|
||||||
TweetNode,
|
TweetNode,
|
||||||
YouTubeNode,
|
YouTubeNode,
|
||||||
];
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,48 +9,50 @@ import {
|
|||||||
SerializedLexicalNode,
|
SerializedLexicalNode,
|
||||||
Spread,
|
Spread,
|
||||||
DecoratorNode,
|
DecoratorNode,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import {Suspense} from 'react';
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
export type Cell = {
|
export type Cell = {
|
||||||
colSpan: number;
|
colSpan: number
|
||||||
json: string;
|
json: string
|
||||||
type: 'normal' | 'header';
|
type: 'normal' | 'header'
|
||||||
id: string;
|
id: string
|
||||||
width: number | null;
|
width: number | null
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Row = {
|
export type Row = {
|
||||||
cells: Array<Cell>;
|
cells: Array<Cell>
|
||||||
height: null | number;
|
height: null | number
|
||||||
id: string;
|
id: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Rows = Array<Row>;
|
export type Rows = Array<Row>
|
||||||
|
|
||||||
export const cellHTMLCache: Map<string, string> = new Map();
|
export const cellHTMLCache: Map<string, string> = new Map()
|
||||||
export const cellTextContentCache: Map<string, string> = new Map();
|
export const cellTextContentCache: Map<string, string> = new Map()
|
||||||
|
|
||||||
const emptyEditorJSON =
|
const emptyEditorJSON =
|
||||||
'{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
|
'{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'
|
||||||
|
|
||||||
const plainTextEditorJSON = (text: string) =>
|
const plainTextEditorJSON = (text: string) => {
|
||||||
text === ''
|
return text === ''
|
||||||
? emptyEditorJSON
|
? emptyEditorJSON
|
||||||
: `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":${text},"type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`;
|
: `{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":${text},"type":"text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}`
|
||||||
|
}
|
||||||
|
|
||||||
const TableComponent = React.lazy(
|
const TableComponent = React.lazy(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
() => import('./TableComponent'),
|
() => import('./TableComponent'),
|
||||||
);
|
)
|
||||||
|
|
||||||
export function createUID(): string {
|
export function createUID(): string {
|
||||||
return Math.random()
|
return Math.random()
|
||||||
.toString(36)
|
.toString(36)
|
||||||
.replace(/[^a-z]+/g, '')
|
.replace(/[^a-z]+/g, '')
|
||||||
.substr(0, 5);
|
.substr(0, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCell(type: 'normal' | 'header'): Cell {
|
function createCell(type: 'normal' | 'header'): Cell {
|
||||||
@@ -60,7 +62,7 @@ function createCell(type: 'normal' | 'header'): Cell {
|
|||||||
json: emptyEditorJSON,
|
json: emptyEditorJSON,
|
||||||
type,
|
type,
|
||||||
width: null,
|
width: null,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRow(): Row {
|
export function createRow(): Row {
|
||||||
@@ -68,141 +70,123 @@ export function createRow(): Row {
|
|||||||
cells: [],
|
cells: [],
|
||||||
height: null,
|
height: null,
|
||||||
id: createUID(),
|
id: createUID(),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedTableNode = Spread<
|
export type SerializedTableNode = Spread<
|
||||||
{
|
{
|
||||||
rows: Rows;
|
rows: Rows
|
||||||
type: 'tablesheet';
|
type: 'tablesheet'
|
||||||
version: 1;
|
version: 1
|
||||||
},
|
},
|
||||||
SerializedLexicalNode
|
SerializedLexicalNode
|
||||||
>;
|
>
|
||||||
|
|
||||||
export function extractRowsFromHTML(tableElem: HTMLTableElement): Rows {
|
export function extractRowsFromHTML(tableElem: HTMLTableElement): Rows {
|
||||||
const rowElems = tableElem.querySelectorAll('tr');
|
const rowElems = tableElem.querySelectorAll('tr')
|
||||||
const rows: Rows = [];
|
const rows: Rows = []
|
||||||
for (let y = 0; y < rowElems.length; y++) {
|
for (let y = 0; y < rowElems.length; y++) {
|
||||||
const rowElem = rowElems[y];
|
const rowElem = rowElems[y]
|
||||||
const cellElems = rowElem.querySelectorAll('td,th');
|
const cellElems = rowElem.querySelectorAll('td,th')
|
||||||
if (!cellElems || cellElems.length === 0) {
|
if (!cellElems || cellElems.length === 0) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
const cells: Array<Cell> = [];
|
const cells: Array<Cell> = []
|
||||||
for (let x = 0; x < cellElems.length; x++) {
|
for (let x = 0; x < cellElems.length; x++) {
|
||||||
const cellElem = cellElems[x] as HTMLElement;
|
const cellElem = cellElems[x] as HTMLElement
|
||||||
const isHeader = cellElem.nodeName === 'TH';
|
const isHeader = cellElem.nodeName === 'TH'
|
||||||
const cell = createCell(isHeader ? 'header' : 'normal');
|
const cell = createCell(isHeader ? 'header' : 'normal')
|
||||||
cell.json = plainTextEditorJSON(
|
cell.json = plainTextEditorJSON(JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')))
|
||||||
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
|
cells.push(cell)
|
||||||
);
|
|
||||||
cells.push(cell);
|
|
||||||
}
|
}
|
||||||
const row = createRow();
|
const row = createRow()
|
||||||
row.cells = cells;
|
row.cells = cells
|
||||||
rows.push(row);
|
rows.push(row)
|
||||||
}
|
}
|
||||||
return rows;
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertTableElement(domNode: HTMLElement): null | DOMConversionOutput {
|
function convertTableElement(domNode: HTMLElement): null | DOMConversionOutput {
|
||||||
const rowElems = domNode.querySelectorAll('tr');
|
const rowElems = domNode.querySelectorAll('tr')
|
||||||
if (!rowElems || rowElems.length === 0) {
|
if (!rowElems || rowElems.length === 0) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
const rows: Rows = [];
|
const rows: Rows = []
|
||||||
for (let y = 0; y < rowElems.length; y++) {
|
for (let y = 0; y < rowElems.length; y++) {
|
||||||
const rowElem = rowElems[y];
|
const rowElem = rowElems[y]
|
||||||
const cellElems = rowElem.querySelectorAll('td,th');
|
const cellElems = rowElem.querySelectorAll('td,th')
|
||||||
if (!cellElems || cellElems.length === 0) {
|
if (!cellElems || cellElems.length === 0) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
const cells: Array<Cell> = [];
|
const cells: Array<Cell> = []
|
||||||
for (let x = 0; x < cellElems.length; x++) {
|
for (let x = 0; x < cellElems.length; x++) {
|
||||||
const cellElem = cellElems[x] as HTMLElement;
|
const cellElem = cellElems[x] as HTMLElement
|
||||||
const isHeader = cellElem.nodeName === 'TH';
|
const isHeader = cellElem.nodeName === 'TH'
|
||||||
const cell = createCell(isHeader ? 'header' : 'normal');
|
const cell = createCell(isHeader ? 'header' : 'normal')
|
||||||
cell.json = plainTextEditorJSON(
|
cell.json = plainTextEditorJSON(JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')))
|
||||||
JSON.stringify(cellElem.innerText.replace(/\n/g, ' ')),
|
cells.push(cell)
|
||||||
);
|
|
||||||
cells.push(cell);
|
|
||||||
}
|
}
|
||||||
const row = createRow();
|
const row = createRow()
|
||||||
row.cells = cells;
|
row.cells = cells
|
||||||
rows.push(row);
|
rows.push(row)
|
||||||
}
|
}
|
||||||
return {node: $createTableNode(rows)};
|
return { node: $createTableNode(rows) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function exportTableCellsToHTML(
|
export function exportTableCellsToHTML(
|
||||||
rows: Rows,
|
rows: Rows,
|
||||||
rect?: {startX: number; endX: number; startY: number; endY: number},
|
rect?: { startX: number; endX: number; startY: number; endY: number },
|
||||||
): HTMLElement {
|
): HTMLElement {
|
||||||
const table = document.createElement('table');
|
const table = document.createElement('table')
|
||||||
const colGroup = document.createElement('colgroup');
|
const colGroup = document.createElement('colgroup')
|
||||||
const tBody = document.createElement('tbody');
|
const tBody = document.createElement('tbody')
|
||||||
const firstRow = rows[0];
|
const firstRow = rows[0]
|
||||||
|
|
||||||
for (
|
for (let x = rect != null ? rect.startX : 0; x < (rect != null ? rect.endX + 1 : firstRow.cells.length); x++) {
|
||||||
let x = rect != null ? rect.startX : 0;
|
const col = document.createElement('col')
|
||||||
x < (rect != null ? rect.endX + 1 : firstRow.cells.length);
|
colGroup.append(col)
|
||||||
x++
|
|
||||||
) {
|
|
||||||
const col = document.createElement('col');
|
|
||||||
colGroup.append(col);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (
|
for (let y = rect != null ? rect.startY : 0; y < (rect != null ? rect.endY + 1 : rows.length); y++) {
|
||||||
let y = rect != null ? rect.startY : 0;
|
const row = rows[y]
|
||||||
y < (rect != null ? rect.endY + 1 : rows.length);
|
const cells = row.cells
|
||||||
y++
|
const rowElem = document.createElement('tr')
|
||||||
) {
|
|
||||||
const row = rows[y];
|
|
||||||
const cells = row.cells;
|
|
||||||
const rowElem = document.createElement('tr');
|
|
||||||
|
|
||||||
for (
|
for (let x = rect != null ? rect.startX : 0; x < (rect != null ? rect.endX + 1 : cells.length); x++) {
|
||||||
let x = rect != null ? rect.startX : 0;
|
const cell = cells[x]
|
||||||
x < (rect != null ? rect.endX + 1 : cells.length);
|
const cellElem = document.createElement(cell.type === 'header' ? 'th' : 'td')
|
||||||
x++
|
cellElem.innerHTML = cellHTMLCache.get(cell.json) || ''
|
||||||
) {
|
rowElem.appendChild(cellElem)
|
||||||
const cell = cells[x];
|
|
||||||
const cellElem = document.createElement(
|
|
||||||
cell.type === 'header' ? 'th' : 'td',
|
|
||||||
);
|
|
||||||
cellElem.innerHTML = cellHTMLCache.get(cell.json) || '';
|
|
||||||
rowElem.appendChild(cellElem);
|
|
||||||
}
|
}
|
||||||
tBody.appendChild(rowElem);
|
tBody.appendChild(rowElem)
|
||||||
}
|
}
|
||||||
|
|
||||||
table.appendChild(colGroup);
|
table.appendChild(colGroup)
|
||||||
table.appendChild(tBody);
|
table.appendChild(tBody)
|
||||||
return table;
|
return table
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TableNode extends DecoratorNode<JSX.Element> {
|
export class TableNode extends DecoratorNode<JSX.Element> {
|
||||||
__rows: Rows;
|
__rows: Rows
|
||||||
|
|
||||||
static getType(): string {
|
static override getType(): string {
|
||||||
return 'tablesheet';
|
return 'tablesheet'
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: TableNode): TableNode {
|
static override clone(node: TableNode): TableNode {
|
||||||
return new TableNode(Array.from(node.__rows), node.__key);
|
return new TableNode(Array.from(node.__rows), node.__key)
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedTableNode): TableNode {
|
static override importJSON(serializedNode: SerializedTableNode): TableNode {
|
||||||
return $createTableNode(serializedNode.rows);
|
return $createTableNode(serializedNode.rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedTableNode {
|
override exportJSON(): SerializedTableNode {
|
||||||
return {
|
return {
|
||||||
rows: this.__rows,
|
rows: this.__rows,
|
||||||
type: 'tablesheet',
|
type: 'tablesheet',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
static importDOM(): DOMConversionMap | null {
|
||||||
@@ -211,188 +195,182 @@ export class TableNode extends DecoratorNode<JSX.Element> {
|
|||||||
conversion: convertTableElement,
|
conversion: convertTableElement,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
}),
|
}),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exportDOM(): DOMExportOutput {
|
override exportDOM(): DOMExportOutput {
|
||||||
return {element: exportTableCellsToHTML(this.__rows)};
|
return { element: exportTableCellsToHTML(this.__rows) }
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(rows?: Rows, key?: NodeKey) {
|
constructor(rows?: Rows, key?: NodeKey) {
|
||||||
super(key);
|
super(key)
|
||||||
this.__rows = rows || [];
|
this.__rows = rows || []
|
||||||
}
|
}
|
||||||
|
|
||||||
createDOM(): HTMLElement {
|
override createDOM(): HTMLElement {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div')
|
||||||
div.style.display = 'contents';
|
div.style.display = 'contents'
|
||||||
return div;
|
return div
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(): false {
|
override updateDOM(): false {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeRows(startX: number, startY: number, mergeRows: Rows): void {
|
mergeRows(startX: number, startY: number, mergeRows: Rows): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
const endY = Math.min(rows.length, startY + mergeRows.length);
|
const endY = Math.min(rows.length, startY + mergeRows.length)
|
||||||
for (let y = startY; y < endY; y++) {
|
for (let y = startY; y < endY; y++) {
|
||||||
const row = rows[y];
|
const row = rows[y]
|
||||||
const mergeRow = mergeRows[y - startY];
|
const mergeRow = mergeRows[y - startY]
|
||||||
const cells = row.cells;
|
const cells = row.cells
|
||||||
const cellsClone = Array.from(cells);
|
const cellsClone = Array.from(cells)
|
||||||
const rowClone = {...row, cells: cellsClone};
|
const rowClone = { ...row, cells: cellsClone }
|
||||||
const mergeCells = mergeRow.cells;
|
const mergeCells = mergeRow.cells
|
||||||
const endX = Math.min(cells.length, startX + mergeCells.length);
|
const endX = Math.min(cells.length, startX + mergeCells.length)
|
||||||
for (let x = startX; x < endX; x++) {
|
for (let x = startX; x < endX; x++) {
|
||||||
const cell = cells[x];
|
const cell = cells[x]
|
||||||
const mergeCell = mergeCells[x - startX];
|
const mergeCell = mergeCells[x - startX]
|
||||||
const cellClone = {...cell, json: mergeCell.json, type: mergeCell.type};
|
const cellClone = { ...cell, json: mergeCell.json, type: mergeCell.type }
|
||||||
cellsClone[x] = cellClone;
|
cellsClone[x] = cellClone
|
||||||
}
|
}
|
||||||
rows[y] = rowClone;
|
rows[y] = rowClone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCellJSON(x: number, y: number, json: string): void {
|
updateCellJSON(x: number, y: number, json: string): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
const row = rows[y];
|
const row = rows[y]
|
||||||
const cells = row.cells;
|
const cells = row.cells
|
||||||
const cell = cells[x];
|
const cell = cells[x]
|
||||||
const cellsClone = Array.from(cells);
|
const cellsClone = Array.from(cells)
|
||||||
const cellClone = {...cell, json};
|
const cellClone = { ...cell, json }
|
||||||
const rowClone = {...row, cells: cellsClone};
|
const rowClone = { ...row, cells: cellsClone }
|
||||||
cellsClone[x] = cellClone;
|
cellsClone[x] = cellClone
|
||||||
rows[y] = rowClone;
|
rows[y] = rowClone
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCellType(x: number, y: number, type: 'header' | 'normal'): void {
|
updateCellType(x: number, y: number, type: 'header' | 'normal'): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
const row = rows[y];
|
const row = rows[y]
|
||||||
const cells = row.cells;
|
const cells = row.cells
|
||||||
const cell = cells[x];
|
const cell = cells[x]
|
||||||
const cellsClone = Array.from(cells);
|
const cellsClone = Array.from(cells)
|
||||||
const cellClone = {...cell, type};
|
const cellClone = { ...cell, type }
|
||||||
const rowClone = {...row, cells: cellsClone};
|
const rowClone = { ...row, cells: cellsClone }
|
||||||
cellsClone[x] = cellClone;
|
cellsClone[x] = cellClone
|
||||||
rows[y] = rowClone;
|
rows[y] = rowClone
|
||||||
}
|
}
|
||||||
|
|
||||||
insertColumnAt(x: number): void {
|
insertColumnAt(x: number): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
for (let y = 0; y < rows.length; y++) {
|
for (let y = 0; y < rows.length; y++) {
|
||||||
const row = rows[y];
|
const row = rows[y]
|
||||||
const cells = row.cells;
|
const cells = row.cells
|
||||||
const cellsClone = Array.from(cells);
|
const cellsClone = Array.from(cells)
|
||||||
const rowClone = {...row, cells: cellsClone};
|
const rowClone = { ...row, cells: cellsClone }
|
||||||
const type = (cells[x] || cells[x - 1]).type;
|
const type = (cells[x] || cells[x - 1]).type
|
||||||
cellsClone.splice(x, 0, createCell(type));
|
cellsClone.splice(x, 0, createCell(type))
|
||||||
rows[y] = rowClone;
|
rows[y] = rowClone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteColumnAt(x: number): void {
|
deleteColumnAt(x: number): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
for (let y = 0; y < rows.length; y++) {
|
for (let y = 0; y < rows.length; y++) {
|
||||||
const row = rows[y];
|
const row = rows[y]
|
||||||
const cells = row.cells;
|
const cells = row.cells
|
||||||
const cellsClone = Array.from(cells);
|
const cellsClone = Array.from(cells)
|
||||||
const rowClone = {...row, cells: cellsClone};
|
const rowClone = { ...row, cells: cellsClone }
|
||||||
cellsClone.splice(x, 1);
|
cellsClone.splice(x, 1)
|
||||||
rows[y] = rowClone;
|
rows[y] = rowClone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addColumns(count: number): void {
|
addColumns(count: number): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
for (let y = 0; y < rows.length; y++) {
|
for (let y = 0; y < rows.length; y++) {
|
||||||
const row = rows[y];
|
const row = rows[y]
|
||||||
const cells = row.cells;
|
const cells = row.cells
|
||||||
const cellsClone = Array.from(cells);
|
const cellsClone = Array.from(cells)
|
||||||
const rowClone = {...row, cells: cellsClone};
|
const rowClone = { ...row, cells: cellsClone }
|
||||||
const type = cells[cells.length - 1].type;
|
const type = cells[cells.length - 1].type
|
||||||
for (let x = 0; x < count; x++) {
|
for (let x = 0; x < count; x++) {
|
||||||
cellsClone.push(createCell(type));
|
cellsClone.push(createCell(type))
|
||||||
}
|
}
|
||||||
rows[y] = rowClone;
|
rows[y] = rowClone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
insertRowAt(y: number): void {
|
insertRowAt(y: number): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
const prevRow = rows[y] || rows[y - 1];
|
const prevRow = rows[y] || rows[y - 1]
|
||||||
const cellCount = prevRow.cells.length;
|
const cellCount = prevRow.cells.length
|
||||||
const row = createRow();
|
const row = createRow()
|
||||||
for (let x = 0; x < cellCount; x++) {
|
for (let x = 0; x < cellCount; x++) {
|
||||||
const cell = createCell(prevRow.cells[x].type);
|
const cell = createCell(prevRow.cells[x].type)
|
||||||
row.cells.push(cell);
|
row.cells.push(cell)
|
||||||
}
|
}
|
||||||
rows.splice(y, 0, row);
|
rows.splice(y, 0, row)
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRowAt(y: number): void {
|
deleteRowAt(y: number): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
rows.splice(y, 1);
|
rows.splice(y, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
addRows(count: number): void {
|
addRows(count: number): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
const prevRow = rows[rows.length - 1];
|
const prevRow = rows[rows.length - 1]
|
||||||
const cellCount = prevRow.cells.length;
|
const cellCount = prevRow.cells.length
|
||||||
|
|
||||||
for (let y = 0; y < count; y++) {
|
for (let y = 0; y < count; y++) {
|
||||||
const row = createRow();
|
const row = createRow()
|
||||||
for (let x = 0; x < cellCount; x++) {
|
for (let x = 0; x < cellCount; x++) {
|
||||||
const cell = createCell(prevRow.cells[x].type);
|
const cell = createCell(prevRow.cells[x].type)
|
||||||
row.cells.push(cell);
|
row.cells.push(cell)
|
||||||
}
|
}
|
||||||
rows.push(row);
|
rows.push(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateColumnWidth(x: number, width: number): void {
|
updateColumnWidth(x: number, width: number): void {
|
||||||
const self = this.getWritable();
|
const self = this.getWritable()
|
||||||
const rows = self.__rows;
|
const rows = self.__rows
|
||||||
for (let y = 0; y < rows.length; y++) {
|
for (let y = 0; y < rows.length; y++) {
|
||||||
const row = rows[y];
|
const row = rows[y]
|
||||||
const cells = row.cells;
|
const cells = row.cells
|
||||||
const cellsClone = Array.from(cells);
|
const cellsClone = Array.from(cells)
|
||||||
const rowClone = {...row, cells: cellsClone};
|
const rowClone = { ...row, cells: cellsClone }
|
||||||
cellsClone[x].width = width;
|
cellsClone[x].width = width
|
||||||
rows[y] = rowClone;
|
rows[y] = rowClone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
override decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<TableComponent
|
<TableComponent nodeKey={this.__key} theme={config.theme} rows={this.__rows} />
|
||||||
nodeKey={this.__key}
|
|
||||||
theme={config.theme}
|
|
||||||
rows={this.__rows}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isTableNode(
|
export function $isTableNode(node: LexicalNode | null | undefined): node is TableNode {
|
||||||
node: LexicalNode | null | undefined,
|
return node instanceof TableNode
|
||||||
): node is TableNode {
|
|
||||||
return node instanceof TableNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createTableNode(rows: Rows): TableNode {
|
export function $createTableNode(rows: Rows): TableNode {
|
||||||
return new TableNode(rows);
|
return new TableNode(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createTableNodeWithDimensions(
|
export function $createTableNodeWithDimensions(
|
||||||
@@ -400,17 +378,13 @@ export function $createTableNodeWithDimensions(
|
|||||||
columnCount: number,
|
columnCount: number,
|
||||||
includeHeaders = true,
|
includeHeaders = true,
|
||||||
): TableNode {
|
): TableNode {
|
||||||
const rows: Rows = [];
|
const rows: Rows = []
|
||||||
for (let y = 0; y < columnCount; y++) {
|
for (let y = 0; y < columnCount; y++) {
|
||||||
const row: Row = createRow();
|
const row: Row = createRow()
|
||||||
rows.push(row);
|
rows.push(row)
|
||||||
for (let x = 0; x < rowCount; x++) {
|
for (let x = 0; x < rowCount; x++) {
|
||||||
row.cells.push(
|
row.cells.push(createCell(includeHeaders === true && (y === 0 || x === 0) ? 'header' : 'normal'))
|
||||||
createCell(
|
|
||||||
includeHeaders === true && (y === 0 || x === 0) ? 'header' : 'normal',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new TableNode(rows);
|
return new TableNode(rows)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,42 +16,37 @@ import type {
|
|||||||
LexicalNode,
|
LexicalNode,
|
||||||
NodeKey,
|
NodeKey,
|
||||||
Spread,
|
Spread,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
|
|
||||||
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
|
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||||
import {
|
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
DecoratorBlockNode,
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
SerializedDecoratorBlockNode,
|
|
||||||
} from '@lexical/react/LexicalDecoratorBlockNode';
|
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
|
||||||
|
|
||||||
const WIDGET_SCRIPT_URL = 'https://platform.twitter.com/widgets.js';
|
const WIDGET_SCRIPT_URL = 'https://platform.twitter.com/widgets.js'
|
||||||
|
|
||||||
type TweetComponentProps = Readonly<{
|
type TweetComponentProps = Readonly<{
|
||||||
className: Readonly<{
|
className: Readonly<{
|
||||||
base: string;
|
base: string
|
||||||
focus: string;
|
focus: string
|
||||||
}>;
|
}>
|
||||||
format: ElementFormatType | null;
|
format: ElementFormatType | null
|
||||||
loadingComponent?: JSX.Element | string;
|
loadingComponent?: JSX.Element | string
|
||||||
nodeKey: NodeKey;
|
nodeKey: NodeKey
|
||||||
onError?: (error: string) => void;
|
onError?: (error: string) => void
|
||||||
onLoad?: () => void;
|
onLoad?: () => void
|
||||||
tweetID: string;
|
tweetID: string
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
function convertTweetElement(
|
function convertTweetElement(domNode: HTMLDivElement): DOMConversionOutput | null {
|
||||||
domNode: HTMLDivElement,
|
const id = domNode.getAttribute('data-lexical-tweet-id')
|
||||||
): DOMConversionOutput | null {
|
|
||||||
const id = domNode.getAttribute('data-lexical-tweet-id');
|
|
||||||
if (id) {
|
if (id) {
|
||||||
const node = $createTweetNode(id);
|
const node = $createTweetNode(id)
|
||||||
return {node};
|
return { node }
|
||||||
}
|
}
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
let isTwitterScriptLoading = true;
|
let isTwitterScriptLoading = true
|
||||||
|
|
||||||
function TweetComponent({
|
function TweetComponent({
|
||||||
className,
|
className,
|
||||||
@@ -62,145 +57,136 @@ function TweetComponent({
|
|||||||
onLoad,
|
onLoad,
|
||||||
tweetID,
|
tweetID,
|
||||||
}: TweetComponentProps) {
|
}: TweetComponentProps) {
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const previousTweetIDRef = useRef<string>('');
|
const previousTweetIDRef = useRef<string>('')
|
||||||
const [isTweetLoading, setIsTweetLoading] = useState(false);
|
const [isTweetLoading, setIsTweetLoading] = useState(false)
|
||||||
|
|
||||||
const createTweet = useCallback(async () => {
|
const createTweet = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error Twitter is attached to the window.
|
// @ts-expect-error Twitter is attached to the window.
|
||||||
await window.twttr.widgets.createTweet(tweetID, containerRef.current);
|
await window.twttr.widgets.createTweet(tweetID, containerRef.current)
|
||||||
|
|
||||||
setIsTweetLoading(false);
|
setIsTweetLoading(false)
|
||||||
isTwitterScriptLoading = false;
|
isTwitterScriptLoading = false
|
||||||
|
|
||||||
if (onLoad) {
|
if (onLoad) {
|
||||||
onLoad();
|
onLoad()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (onError) {
|
if (onError) {
|
||||||
onError(String(error));
|
onError(String(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [onError, onLoad, tweetID]);
|
}, [onError, onLoad, tweetID])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tweetID !== previousTweetIDRef.current) {
|
if (tweetID !== previousTweetIDRef.current) {
|
||||||
setIsTweetLoading(true);
|
setIsTweetLoading(true)
|
||||||
|
|
||||||
if (isTwitterScriptLoading) {
|
if (isTwitterScriptLoading) {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script')
|
||||||
script.src = WIDGET_SCRIPT_URL;
|
script.src = WIDGET_SCRIPT_URL
|
||||||
script.async = true;
|
script.async = true
|
||||||
document.body?.appendChild(script);
|
document.body?.appendChild(script)
|
||||||
script.onload = createTweet;
|
script.onload = createTweet
|
||||||
if (onError) {
|
if (onError) {
|
||||||
script.onerror = onError as OnErrorEventHandler;
|
script.onerror = onError as OnErrorEventHandler
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
createTweet();
|
createTweet().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousTweetIDRef) {
|
if (previousTweetIDRef) {
|
||||||
previousTweetIDRef.current = tweetID;
|
previousTweetIDRef.current = tweetID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [createTweet, onError, tweetID]);
|
}, [createTweet, onError, tweetID])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockWithAlignableContents
|
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||||
className={className}
|
|
||||||
format={format}
|
|
||||||
nodeKey={nodeKey}>
|
|
||||||
{isTweetLoading ? loadingComponent : null}
|
{isTweetLoading ? loadingComponent : null}
|
||||||
<div
|
<div style={{ display: 'inline-block', width: '550px' }} ref={containerRef} />
|
||||||
style={{display: 'inline-block', width: '550px'}}
|
|
||||||
ref={containerRef}
|
|
||||||
/>
|
|
||||||
</BlockWithAlignableContents>
|
</BlockWithAlignableContents>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedTweetNode = Spread<
|
export type SerializedTweetNode = Spread<
|
||||||
{
|
{
|
||||||
id: string;
|
id: string
|
||||||
type: 'tweet';
|
type: 'tweet'
|
||||||
version: 1;
|
version: 1
|
||||||
},
|
},
|
||||||
SerializedDecoratorBlockNode
|
SerializedDecoratorBlockNode
|
||||||
>;
|
>
|
||||||
|
|
||||||
export class TweetNode extends DecoratorBlockNode {
|
export class TweetNode extends DecoratorBlockNode {
|
||||||
__id: string;
|
__id: string
|
||||||
|
|
||||||
static getType(): string {
|
static override getType(): string {
|
||||||
return 'tweet';
|
return 'tweet'
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: TweetNode): TweetNode {
|
static override clone(node: TweetNode): TweetNode {
|
||||||
return new TweetNode(node.__id, node.__format, node.__key);
|
return new TweetNode(node.__id, node.__format, node.__key)
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedTweetNode): TweetNode {
|
static override importJSON(serializedNode: SerializedTweetNode): TweetNode {
|
||||||
const node = $createTweetNode(serializedNode.id);
|
const node = $createTweetNode(serializedNode.id)
|
||||||
node.setFormat(serializedNode.format);
|
node.setFormat(serializedNode.format)
|
||||||
return node;
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedTweetNode {
|
override exportJSON(): SerializedTweetNode {
|
||||||
return {
|
return {
|
||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
id: this.getId(),
|
id: this.getId(),
|
||||||
type: 'tweet',
|
type: 'tweet',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
|
static importDOM(): DOMConversionMap<HTMLDivElement> | null {
|
||||||
return {
|
return {
|
||||||
div: (domNode: HTMLDivElement) => {
|
div: (domNode: HTMLDivElement) => {
|
||||||
if (!domNode.hasAttribute('data-lexical-tweet-id')) {
|
if (!domNode.hasAttribute('data-lexical-tweet-id')) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
conversion: convertTweetElement,
|
conversion: convertTweetElement,
|
||||||
priority: 2,
|
priority: 2,
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exportDOM(): DOMExportOutput {
|
override exportDOM(): DOMExportOutput {
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div')
|
||||||
element.setAttribute('data-lexical-tweet-id', this.__id);
|
element.setAttribute('data-lexical-tweet-id', this.__id)
|
||||||
const text = document.createTextNode(this.getTextContent());
|
const text = document.createTextNode(this.getTextContent())
|
||||||
element.append(text);
|
element.append(text)
|
||||||
return {element};
|
return { element }
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||||
super(format, key);
|
super(format, key)
|
||||||
this.__id = id;
|
this.__id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): string {
|
getId(): string {
|
||||||
return this.__id;
|
return this.__id
|
||||||
}
|
}
|
||||||
|
|
||||||
getTextContent(
|
override getTextContent(_includeInert?: boolean | undefined, _includeDirectionless?: false | undefined): string {
|
||||||
_includeInert?: boolean | undefined,
|
return `https://twitter.com/i/web/status/${this.__id}`
|
||||||
_includeDirectionless?: false | undefined,
|
|
||||||
): string {
|
|
||||||
return `https://twitter.com/i/web/status/${this.__id}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decorate(editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
override decorate(_: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||||
const embedBlockTheme = config.theme.embedBlock || {};
|
const embedBlockTheme = config.theme.embedBlock || {}
|
||||||
const className = {
|
const className = {
|
||||||
base: embedBlockTheme.base || '',
|
base: embedBlockTheme.base || '',
|
||||||
focus: embedBlockTheme.focus || '',
|
focus: embedBlockTheme.focus || '',
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<TweetComponent
|
<TweetComponent
|
||||||
className={className}
|
className={className}
|
||||||
@@ -209,20 +195,18 @@ export class TweetNode extends DecoratorBlockNode {
|
|||||||
nodeKey={this.getKey()}
|
nodeKey={this.getKey()}
|
||||||
tweetID={this.__id}
|
tweetID={this.__id}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
isInline(): false {
|
override isInline(): false {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createTweetNode(tweetID: string): TweetNode {
|
export function $createTweetNode(tweetID: string): TweetNode {
|
||||||
return new TweetNode(tweetID);
|
return new TweetNode(tweetID)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isTweetNode(
|
export function $isTweetNode(node: TweetNode | LexicalNode | null | undefined): node is TweetNode {
|
||||||
node: TweetNode | LexicalNode | null | undefined,
|
return node instanceof TweetNode
|
||||||
): node is TweetNode {
|
|
||||||
return node instanceof TweetNode;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,42 +6,24 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type { EditorConfig, ElementFormatType, LexicalEditor, LexicalNode, NodeKey, Spread } from 'lexical'
|
||||||
EditorConfig,
|
|
||||||
ElementFormatType,
|
|
||||||
LexicalEditor,
|
|
||||||
LexicalNode,
|
|
||||||
NodeKey,
|
|
||||||
Spread,
|
|
||||||
} from 'lexical';
|
|
||||||
|
|
||||||
import {BlockWithAlignableContents} from '@lexical/react/LexicalBlockWithAlignableContents';
|
import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents'
|
||||||
import {
|
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||||
DecoratorBlockNode,
|
|
||||||
SerializedDecoratorBlockNode,
|
|
||||||
} from '@lexical/react/LexicalDecoratorBlockNode';
|
|
||||||
|
|
||||||
type YouTubeComponentProps = Readonly<{
|
type YouTubeComponentProps = Readonly<{
|
||||||
className: Readonly<{
|
className: Readonly<{
|
||||||
base: string;
|
base: string
|
||||||
focus: string;
|
focus: string
|
||||||
}>;
|
}>
|
||||||
format: ElementFormatType | null;
|
format: ElementFormatType | null
|
||||||
nodeKey: NodeKey;
|
nodeKey: NodeKey
|
||||||
videoID: string;
|
videoID: string
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
function YouTubeComponent({
|
function YouTubeComponent({ className, format, nodeKey, videoID }: YouTubeComponentProps) {
|
||||||
className,
|
|
||||||
format,
|
|
||||||
nodeKey,
|
|
||||||
videoID,
|
|
||||||
}: YouTubeComponentProps) {
|
|
||||||
return (
|
return (
|
||||||
<BlockWithAlignableContents
|
<BlockWithAlignableContents className={className} format={format} nodeKey={nodeKey}>
|
||||||
className={className}
|
|
||||||
format={format}
|
|
||||||
nodeKey={nodeKey}>
|
|
||||||
<iframe
|
<iframe
|
||||||
width="560"
|
width="560"
|
||||||
height="315"
|
height="315"
|
||||||
@@ -52,33 +34,33 @@ function YouTubeComponent({
|
|||||||
title="YouTube video"
|
title="YouTube video"
|
||||||
/>
|
/>
|
||||||
</BlockWithAlignableContents>
|
</BlockWithAlignableContents>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SerializedYouTubeNode = Spread<
|
export type SerializedYouTubeNode = Spread<
|
||||||
{
|
{
|
||||||
videoID: string;
|
videoID: string
|
||||||
type: 'youtube';
|
type: 'youtube'
|
||||||
version: 1;
|
version: 1
|
||||||
},
|
},
|
||||||
SerializedDecoratorBlockNode
|
SerializedDecoratorBlockNode
|
||||||
>;
|
>
|
||||||
|
|
||||||
export class YouTubeNode extends DecoratorBlockNode {
|
export class YouTubeNode extends DecoratorBlockNode {
|
||||||
__id: string;
|
__id: string
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'youtube';
|
return 'youtube'
|
||||||
}
|
}
|
||||||
|
|
||||||
static clone(node: YouTubeNode): YouTubeNode {
|
static clone(node: YouTubeNode): YouTubeNode {
|
||||||
return new YouTubeNode(node.__id, node.__format, node.__key);
|
return new YouTubeNode(node.__id, node.__format, node.__key)
|
||||||
}
|
}
|
||||||
|
|
||||||
static importJSON(serializedNode: SerializedYouTubeNode): YouTubeNode {
|
static importJSON(serializedNode: SerializedYouTubeNode): YouTubeNode {
|
||||||
const node = $createYouTubeNode(serializedNode.videoID);
|
const node = $createYouTubeNode(serializedNode.videoID)
|
||||||
node.setFormat(serializedNode.format);
|
node.setFormat(serializedNode.format)
|
||||||
return node;
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
exportJSON(): SerializedYouTubeNode {
|
exportJSON(): SerializedYouTubeNode {
|
||||||
@@ -87,56 +69,44 @@ export class YouTubeNode extends DecoratorBlockNode {
|
|||||||
type: 'youtube',
|
type: 'youtube',
|
||||||
version: 1,
|
version: 1,
|
||||||
videoID: this.__id,
|
videoID: this.__id,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||||
super(format, key);
|
super(format, key)
|
||||||
this.__id = id;
|
this.__id = id
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDOM(): false {
|
updateDOM(): false {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
getId(): string {
|
getId(): string {
|
||||||
return this.__id;
|
return this.__id
|
||||||
}
|
}
|
||||||
|
|
||||||
getTextContent(
|
getTextContent(_includeInert?: boolean | undefined, _includeDirectionless?: false | undefined): string {
|
||||||
_includeInert?: boolean | undefined,
|
return `https://www.youtube.com/watch?v=${this.__id}`
|
||||||
_includeDirectionless?: false | undefined,
|
|
||||||
): string {
|
|
||||||
return `https://www.youtube.com/watch?v=${this.__id}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
decorate(_editor: LexicalEditor, config: EditorConfig): JSX.Element {
|
||||||
const embedBlockTheme = config.theme.embedBlock || {};
|
const embedBlockTheme = config.theme.embedBlock || {}
|
||||||
const className = {
|
const className = {
|
||||||
base: embedBlockTheme.base || '',
|
base: embedBlockTheme.base || '',
|
||||||
focus: embedBlockTheme.focus || '',
|
focus: embedBlockTheme.focus || '',
|
||||||
};
|
}
|
||||||
return (
|
return <YouTubeComponent className={className} format={this.__format} nodeKey={this.getKey()} videoID={this.__id} />
|
||||||
<YouTubeComponent
|
|
||||||
className={className}
|
|
||||||
format={this.__format}
|
|
||||||
nodeKey={this.getKey()}
|
|
||||||
videoID={this.__id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isInline(): false {
|
isInline(): false {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createYouTubeNode(videoID: string): YouTubeNode {
|
export function $createYouTubeNode(videoID: string): YouTubeNode {
|
||||||
return new YouTubeNode(videoID);
|
return new YouTubeNode(videoID)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isYouTubeNode(
|
export function $isYouTubeNode(node: YouTubeNode | LexicalNode | null | undefined): node is YouTubeNode {
|
||||||
node: YouTubeNode | LexicalNode | null | undefined,
|
return node instanceof YouTubeNode
|
||||||
): node is YouTubeNode {
|
|
||||||
return node instanceof YouTubeNode;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {LexicalEditor} from 'lexical';
|
import type { LexicalEditor } from 'lexical'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AutoEmbedOption,
|
AutoEmbedOption,
|
||||||
@@ -14,33 +14,33 @@ import {
|
|||||||
EmbedMatchResult,
|
EmbedMatchResult,
|
||||||
LexicalAutoEmbedPlugin,
|
LexicalAutoEmbedPlugin,
|
||||||
URL_MATCHER,
|
URL_MATCHER,
|
||||||
} from '@lexical/react/LexicalAutoEmbedPlugin';
|
} from '@lexical/react/LexicalAutoEmbedPlugin'
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {useState} from 'react';
|
import { useState } from 'react'
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom'
|
||||||
|
|
||||||
import useModal from '../../Hooks/useModal';
|
import useModal from '../../Hooks/useModal'
|
||||||
import Button from '../../UI/Button';
|
import Button from '../../UI/Button'
|
||||||
import {DialogActions} from '../../UI/Dialog';
|
import { DialogActions } from '../../UI/Dialog'
|
||||||
import {INSERT_TWEET_COMMAND} from '../TwitterPlugin';
|
import { INSERT_TWEET_COMMAND } from '../TwitterPlugin'
|
||||||
import {INSERT_YOUTUBE_COMMAND} from '../YouTubePlugin';
|
import { INSERT_YOUTUBE_COMMAND } from '../YouTubePlugin'
|
||||||
|
|
||||||
interface PlaygroundEmbedConfig extends EmbedConfig {
|
interface PlaygroundEmbedConfig extends EmbedConfig {
|
||||||
// Human readable name of the embeded content e.g. Tweet or Google Map.
|
// Human readable name of the embeded content e.g. Tweet or Google Map.
|
||||||
contentName: string;
|
contentName: string
|
||||||
|
|
||||||
// Icon for display.
|
// Icon for display.
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element
|
||||||
iconName: string;
|
iconName: string
|
||||||
|
|
||||||
// An example of a matching url https://twitter.com/jack/status/20
|
// An example of a matching url https://twitter.com/jack/status/20
|
||||||
exampleUrl: string;
|
exampleUrl: string
|
||||||
|
|
||||||
// For extra searching.
|
// For extra searching.
|
||||||
keywords: Array<string>;
|
keywords: Array<string>
|
||||||
|
|
||||||
// Embed a Figma Project.
|
// Embed a Figma Project.
|
||||||
description?: string;
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
|
export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
|
||||||
@@ -53,30 +53,29 @@ export const YoutubeEmbedConfig: PlaygroundEmbedConfig = {
|
|||||||
iconName: 'youtube',
|
iconName: 'youtube',
|
||||||
|
|
||||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
||||||
editor.dispatchCommand(INSERT_YOUTUBE_COMMAND, result.id);
|
editor.dispatchCommand(INSERT_YOUTUBE_COMMAND, result.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
keywords: ['youtube', 'video'],
|
keywords: ['youtube', 'video'],
|
||||||
|
|
||||||
// Determine if a given URL is a match and return url data.
|
// Determine if a given URL is a match and return url data.
|
||||||
parseUrl: (url: string) => {
|
parseUrl: (url: string) => {
|
||||||
const match =
|
const match = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/.exec(url)
|
||||||
/^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/.exec(url);
|
|
||||||
|
|
||||||
const id = match ? (match?.[2].length === 11 ? match[2] : null) : null;
|
const id = match ? (match?.[2].length === 11 ? match[2] : null) : null
|
||||||
|
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
url,
|
url,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
type: 'youtube-video',
|
type: 'youtube-video',
|
||||||
};
|
}
|
||||||
|
|
||||||
export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
|
export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
|
||||||
// e.g. Tweet or Google Map.
|
// e.g. Tweet or Google Map.
|
||||||
@@ -90,7 +89,7 @@ export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
|
|||||||
|
|
||||||
// Create the Lexical embed node from the url data.
|
// Create the Lexical embed node from the url data.
|
||||||
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
insertNode: (editor: LexicalEditor, result: EmbedMatchResult) => {
|
||||||
editor.dispatchCommand(INSERT_TWEET_COMMAND, result.id);
|
editor.dispatchCommand(INSERT_TWEET_COMMAND, result.id)
|
||||||
},
|
},
|
||||||
|
|
||||||
// For extra searching.
|
// For extra searching.
|
||||||
@@ -98,23 +97,22 @@ export const TwitterEmbedConfig: PlaygroundEmbedConfig = {
|
|||||||
|
|
||||||
// Determine if a given URL is a match and return url data.
|
// Determine if a given URL is a match and return url data.
|
||||||
parseUrl: (text: string) => {
|
parseUrl: (text: string) => {
|
||||||
const match =
|
const match = /^https:\/\/twitter\.com\/(#!\/)?(\w+)\/status(es)*\/(\d+)$/.exec(text)
|
||||||
/^https:\/\/twitter\.com\/(#!\/)?(\w+)\/status(es)*\/(\d+)$/.exec(text);
|
|
||||||
|
|
||||||
if (match != null) {
|
if (match != null) {
|
||||||
return {
|
return {
|
||||||
id: match[4],
|
id: match[4],
|
||||||
url: match[0],
|
url: match[0],
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
},
|
},
|
||||||
|
|
||||||
type: 'tweet',
|
type: 'tweet',
|
||||||
};
|
}
|
||||||
|
|
||||||
export const EmbedConfigs = [TwitterEmbedConfig, YoutubeEmbedConfig];
|
export const EmbedConfigs = [TwitterEmbedConfig, YoutubeEmbedConfig]
|
||||||
|
|
||||||
function AutoEmbedMenuItem({
|
function AutoEmbedMenuItem({
|
||||||
index,
|
index,
|
||||||
@@ -123,15 +121,15 @@ function AutoEmbedMenuItem({
|
|||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
option,
|
option,
|
||||||
}: {
|
}: {
|
||||||
index: number;
|
index: number
|
||||||
isSelected: boolean;
|
isSelected: boolean
|
||||||
onClick: () => void;
|
onClick: () => void
|
||||||
onMouseEnter: () => void;
|
onMouseEnter: () => void
|
||||||
option: AutoEmbedOption;
|
option: AutoEmbedOption
|
||||||
}) {
|
}) {
|
||||||
let className = 'item';
|
let className = 'item'
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
className += ' selected';
|
className += ' selected'
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
@@ -143,10 +141,11 @@ function AutoEmbedMenuItem({
|
|||||||
aria-selected={isSelected}
|
aria-selected={isSelected}
|
||||||
id={'typeahead-item-' + index}
|
id={'typeahead-item-' + index}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onClick={onClick}>
|
onClick={onClick}
|
||||||
|
>
|
||||||
<span className="text">{option.title}</span>
|
<span className="text">{option.title}</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AutoEmbedMenu({
|
function AutoEmbedMenu({
|
||||||
@@ -155,10 +154,10 @@ function AutoEmbedMenu({
|
|||||||
onOptionClick,
|
onOptionClick,
|
||||||
onOptionMouseEnter,
|
onOptionMouseEnter,
|
||||||
}: {
|
}: {
|
||||||
selectedItemIndex: number | null;
|
selectedItemIndex: number | null
|
||||||
onOptionClick: (option: AutoEmbedOption, index: number) => void;
|
onOptionClick: (option: AutoEmbedOption, index: number) => void
|
||||||
onOptionMouseEnter: (index: number) => void;
|
onOptionMouseEnter: (index: number) => void
|
||||||
options: Array<AutoEmbedOption>;
|
options: Array<AutoEmbedOption>
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="typeahead-popover">
|
<div className="typeahead-popover">
|
||||||
@@ -175,33 +174,32 @@ function AutoEmbedMenu({
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AutoEmbedDialog({
|
export function AutoEmbedDialog({
|
||||||
embedConfig,
|
embedConfig,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
embedConfig: PlaygroundEmbedConfig;
|
embedConfig: PlaygroundEmbedConfig
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('')
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
const urlMatch = URL_MATCHER.exec(text);
|
const urlMatch = URL_MATCHER.exec(text)
|
||||||
const embedResult =
|
const embedResult = text != null && urlMatch != null ? embedConfig.parseUrl(text) : null
|
||||||
text != null && urlMatch != null ? embedConfig.parseUrl(text) : null;
|
|
||||||
|
|
||||||
const onClick = async () => {
|
const onClick = async () => {
|
||||||
const result = await embedResult;
|
const result = await embedResult
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
embedConfig.insertNode(editor, result);
|
embedConfig.insertNode(editor, result)
|
||||||
onClose();
|
onClose()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{width: '600px'}}>
|
<div style={{ width: '600px' }}>
|
||||||
<div className="Input__wrapper">
|
<div className="Input__wrapper">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -210,36 +208,29 @@ export function AutoEmbedDialog({
|
|||||||
value={text}
|
value={text}
|
||||||
data-test-id={`${embedConfig.type}-embed-modal-url`}
|
data-test-id={`${embedConfig.type}-embed-modal-url`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setText(e.target.value);
|
setText(e.target.value)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button disabled={!embedResult} onClick={onClick} data-test-id={`${embedConfig.type}-embed-modal-submit-btn`}>
|
||||||
disabled={!embedResult}
|
|
||||||
onClick={onClick}
|
|
||||||
data-test-id={`${embedConfig.type}-embed-modal-submit-btn`}>
|
|
||||||
Embed
|
Embed
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AutoEmbedPlugin(): JSX.Element {
|
export default function AutoEmbedPlugin(): JSX.Element {
|
||||||
const [modal, showModal] = useModal();
|
const [modal, showModal] = useModal()
|
||||||
|
|
||||||
const openEmbedModal = (embedConfig: PlaygroundEmbedConfig) => {
|
const openEmbedModal = (embedConfig: PlaygroundEmbedConfig) => {
|
||||||
showModal(`Embed ${embedConfig.contentName}`, (onClose) => (
|
showModal(`Embed ${embedConfig.contentName}`, (onClose) => (
|
||||||
<AutoEmbedDialog embedConfig={embedConfig} onClose={onClose} />
|
<AutoEmbedDialog embedConfig={embedConfig} onClose={onClose} />
|
||||||
));
|
))
|
||||||
};
|
}
|
||||||
|
|
||||||
const getMenuOptions = (
|
const getMenuOptions = (activeEmbedConfig: PlaygroundEmbedConfig, embedFn: () => void, dismissFn: () => void) => {
|
||||||
activeEmbedConfig: PlaygroundEmbedConfig,
|
|
||||||
embedFn: () => void,
|
|
||||||
dismissFn: () => void,
|
|
||||||
) => {
|
|
||||||
return [
|
return [
|
||||||
new AutoEmbedOption('Dismiss', {
|
new AutoEmbedOption('Dismiss', {
|
||||||
onSelect: dismissFn,
|
onSelect: dismissFn,
|
||||||
@@ -247,8 +238,8 @@ export default function AutoEmbedPlugin(): JSX.Element {
|
|||||||
new AutoEmbedOption(`Embed ${activeEmbedConfig.contentName}`, {
|
new AutoEmbedOption(`Embed ${activeEmbedConfig.contentName}`, {
|
||||||
onSelect: embedFn,
|
onSelect: embedFn,
|
||||||
}),
|
}),
|
||||||
];
|
]
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -257,34 +248,32 @@ export default function AutoEmbedPlugin(): JSX.Element {
|
|||||||
embedConfigs={EmbedConfigs}
|
embedConfigs={EmbedConfigs}
|
||||||
onOpenEmbedModalForConfig={openEmbedModal}
|
onOpenEmbedModalForConfig={openEmbedModal}
|
||||||
getMenuOptions={getMenuOptions}
|
getMenuOptions={getMenuOptions}
|
||||||
menuRenderFn={(
|
menuRenderFn={(anchorElementRef, { selectedIndex, options, selectOptionAndCleanUp, setHighlightedIndex }) => {
|
||||||
anchorElementRef,
|
return anchorElementRef.current
|
||||||
{selectedIndex, options, selectOptionAndCleanUp, setHighlightedIndex},
|
|
||||||
) =>
|
|
||||||
anchorElementRef.current
|
|
||||||
? ReactDOM.createPortal(
|
? ReactDOM.createPortal(
|
||||||
<div
|
<div
|
||||||
className="typeahead-popover auto-embed-menu"
|
className="typeahead-popover auto-embed-menu"
|
||||||
style={{
|
style={{
|
||||||
marginLeft: anchorElementRef.current.style.width,
|
marginLeft: anchorElementRef.current.style.width,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<AutoEmbedMenu
|
<AutoEmbedMenu
|
||||||
options={options}
|
options={options}
|
||||||
selectedItemIndex={selectedIndex}
|
selectedItemIndex={selectedIndex}
|
||||||
onOptionClick={(option: AutoEmbedOption, index: number) => {
|
onOptionClick={(option: AutoEmbedOption, index: number) => {
|
||||||
setHighlightedIndex(index);
|
setHighlightedIndex(index)
|
||||||
selectOptionAndCleanUp(option);
|
selectOptionAndCleanUp(option)
|
||||||
}}
|
}}
|
||||||
onOptionMouseEnter={(index: number) => {
|
onOptionMouseEnter={(index: number) => {
|
||||||
setHighlightedIndex(index);
|
setHighlightedIndex(index)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
anchorElementRef.current,
|
anchorElementRef.current,
|
||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {registerCodeHighlighting} from '@lexical/code';
|
import { registerCodeHighlighting } from '@lexical/code'
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {useEffect} from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function CodeHighlightPlugin(): JSX.Element | null {
|
export default function CodeHighlightPlugin(): JSX.Element | null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return registerCodeHighlighting(editor);
|
return registerCodeHighlighting(editor)
|
||||||
}, [editor]);
|
}, [editor])
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,62 +14,55 @@ import {
|
|||||||
NodeKey,
|
NodeKey,
|
||||||
SerializedElementNode,
|
SerializedElementNode,
|
||||||
Spread,
|
Spread,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
|
|
||||||
type SerializedCollapsibleContainerNode = Spread<
|
type SerializedCollapsibleContainerNode = Spread<
|
||||||
{
|
{
|
||||||
type: 'collapsible-container';
|
type: 'collapsible-container'
|
||||||
version: 1;
|
version: 1
|
||||||
open: boolean;
|
open: boolean
|
||||||
},
|
},
|
||||||
SerializedElementNode
|
SerializedElementNode
|
||||||
>;
|
>
|
||||||
|
|
||||||
export class CollapsibleContainerNode extends ElementNode {
|
export class CollapsibleContainerNode extends ElementNode {
|
||||||
__open: boolean;
|
__open: boolean
|
||||||
|
|
||||||
constructor(open: boolean, key?: NodeKey) {
|
constructor(open: boolean, key?: NodeKey) {
|
||||||
super(key);
|
super(key)
|
||||||
this.__open = open ?? false;
|
this.__open = open ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
static override getType(): string {
|
static override getType(): string {
|
||||||
return 'collapsible-container';
|
return 'collapsible-container'
|
||||||
}
|
}
|
||||||
|
|
||||||
static override clone(
|
static override clone(node: CollapsibleContainerNode): CollapsibleContainerNode {
|
||||||
node: CollapsibleContainerNode,
|
return new CollapsibleContainerNode(node.__open, node.__key)
|
||||||
): CollapsibleContainerNode {
|
|
||||||
return new CollapsibleContainerNode(node.__open, node.__key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override createDOM(config: EditorConfig): HTMLElement {
|
override createDOM(_: EditorConfig): HTMLElement {
|
||||||
const dom = document.createElement('details');
|
const dom = document.createElement('details')
|
||||||
dom.classList.add('Collapsible__container');
|
dom.classList.add('Collapsible__container')
|
||||||
dom.open = this.__open;
|
dom.open = this.__open
|
||||||
return dom;
|
return dom
|
||||||
}
|
}
|
||||||
|
|
||||||
override updateDOM(
|
override updateDOM(prevNode: CollapsibleContainerNode, dom: HTMLDetailsElement): boolean {
|
||||||
prevNode: CollapsibleContainerNode,
|
|
||||||
dom: HTMLDetailsElement,
|
|
||||||
): boolean {
|
|
||||||
if (prevNode.__open !== this.__open) {
|
if (prevNode.__open !== this.__open) {
|
||||||
dom.open = this.__open;
|
dom.open = this.__open
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
static importDOM(): DOMConversionMap | null {
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
static override importJSON(
|
static override importJSON(serializedNode: SerializedCollapsibleContainerNode): CollapsibleContainerNode {
|
||||||
serializedNode: SerializedCollapsibleContainerNode,
|
const node = $createCollapsibleContainerNode(serializedNode.open)
|
||||||
): CollapsibleContainerNode {
|
return node
|
||||||
const node = $createCollapsibleContainerNode(serializedNode.open);
|
|
||||||
return node;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override exportJSON(): SerializedCollapsibleContainerNode {
|
override exportJSON(): SerializedCollapsibleContainerNode {
|
||||||
@@ -78,31 +71,27 @@ export class CollapsibleContainerNode extends ElementNode {
|
|||||||
type: 'collapsible-container',
|
type: 'collapsible-container',
|
||||||
version: 1,
|
version: 1,
|
||||||
open: this.__open,
|
open: this.__open,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setOpen(open: boolean): void {
|
setOpen(open: boolean): void {
|
||||||
const writable = this.getWritable();
|
const writable = this.getWritable()
|
||||||
writable.__open = open;
|
writable.__open = open
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpen(): boolean {
|
getOpen(): boolean {
|
||||||
return this.__open;
|
return this.__open
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOpen(): void {
|
toggleOpen(): void {
|
||||||
this.setOpen(!this.getOpen());
|
this.setOpen(!this.getOpen())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createCollapsibleContainerNode(
|
export function $createCollapsibleContainerNode(open: boolean): CollapsibleContainerNode {
|
||||||
open: boolean,
|
return new CollapsibleContainerNode(open)
|
||||||
): CollapsibleContainerNode {
|
|
||||||
return new CollapsibleContainerNode(open);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isCollapsibleContainerNode(
|
export function $isCollapsibleContainerNode(node: LexicalNode | null | undefined): node is CollapsibleContainerNode {
|
||||||
node: LexicalNode | null | undefined,
|
return node instanceof CollapsibleContainerNode
|
||||||
): node is CollapsibleContainerNode {
|
|
||||||
return node instanceof CollapsibleContainerNode;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,57 +6,45 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { DOMConversionMap, EditorConfig, ElementNode, LexicalNode, SerializedElementNode, Spread } from 'lexical'
|
||||||
DOMConversionMap,
|
|
||||||
EditorConfig,
|
|
||||||
ElementNode,
|
|
||||||
LexicalNode,
|
|
||||||
SerializedElementNode,
|
|
||||||
Spread,
|
|
||||||
} from 'lexical';
|
|
||||||
|
|
||||||
type SerializedCollapsibleContentNode = Spread<
|
type SerializedCollapsibleContentNode = Spread<
|
||||||
{
|
{
|
||||||
type: 'collapsible-content';
|
type: 'collapsible-content'
|
||||||
version: 1;
|
version: 1
|
||||||
},
|
},
|
||||||
SerializedElementNode
|
SerializedElementNode
|
||||||
>;
|
>
|
||||||
|
|
||||||
export class CollapsibleContentNode extends ElementNode {
|
export class CollapsibleContentNode extends ElementNode {
|
||||||
static override getType(): string {
|
static override getType(): string {
|
||||||
return 'collapsible-content';
|
return 'collapsible-content'
|
||||||
}
|
}
|
||||||
|
|
||||||
static override clone(node: CollapsibleContentNode): CollapsibleContentNode {
|
static override clone(node: CollapsibleContentNode): CollapsibleContentNode {
|
||||||
return new CollapsibleContentNode(node.__key);
|
return new CollapsibleContentNode(node.__key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override createDOM(config: EditorConfig): HTMLElement {
|
override createDOM(_config: EditorConfig): HTMLElement {
|
||||||
const dom = document.createElement('div');
|
const dom = document.createElement('div')
|
||||||
dom.classList.add('Collapsible__content');
|
dom.classList.add('Collapsible__content')
|
||||||
return dom;
|
return dom
|
||||||
}
|
}
|
||||||
|
|
||||||
override updateDOM(
|
override updateDOM(_prevNode: CollapsibleContentNode, _dom: HTMLElement): boolean {
|
||||||
prevNode: CollapsibleContentNode,
|
return false
|
||||||
dom: HTMLElement,
|
|
||||||
): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
static importDOM(): DOMConversionMap | null {
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
static override importJSON(
|
static override importJSON(_serializedNode: SerializedCollapsibleContentNode): CollapsibleContentNode {
|
||||||
serializedNode: SerializedCollapsibleContentNode,
|
return $createCollapsibleContentNode()
|
||||||
): CollapsibleContentNode {
|
|
||||||
return $createCollapsibleContentNode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override isShadowRoot(): boolean {
|
override isShadowRoot(): boolean {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override exportJSON(): SerializedCollapsibleContentNode {
|
override exportJSON(): SerializedCollapsibleContentNode {
|
||||||
@@ -64,16 +52,14 @@ export class CollapsibleContentNode extends ElementNode {
|
|||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
type: 'collapsible-content',
|
type: 'collapsible-content',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createCollapsibleContentNode(): CollapsibleContentNode {
|
export function $createCollapsibleContentNode(): CollapsibleContentNode {
|
||||||
return new CollapsibleContentNode();
|
return new CollapsibleContentNode()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isCollapsibleContentNode(
|
export function $isCollapsibleContentNode(node: LexicalNode | null | undefined): node is CollapsibleContentNode {
|
||||||
node: LexicalNode | null | undefined,
|
return node instanceof CollapsibleContentNode
|
||||||
): node is CollapsibleContentNode {
|
|
||||||
return node instanceof CollapsibleContentNode;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,59 +17,54 @@ import {
|
|||||||
RangeSelection,
|
RangeSelection,
|
||||||
SerializedElementNode,
|
SerializedElementNode,
|
||||||
Spread,
|
Spread,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
|
|
||||||
import {$isCollapsibleContainerNode} from './CollapsibleContainerNode';
|
import { $isCollapsibleContainerNode } from './CollapsibleContainerNode'
|
||||||
import {$isCollapsibleContentNode} from './CollapsibleContentNode';
|
import { $isCollapsibleContentNode } from './CollapsibleContentNode'
|
||||||
|
|
||||||
type SerializedCollapsibleTitleNode = Spread<
|
type SerializedCollapsibleTitleNode = Spread<
|
||||||
{
|
{
|
||||||
type: 'collapsible-title';
|
type: 'collapsible-title'
|
||||||
version: 1;
|
version: 1
|
||||||
},
|
},
|
||||||
SerializedElementNode
|
SerializedElementNode
|
||||||
>;
|
>
|
||||||
|
|
||||||
export class CollapsibleTitleNode extends ElementNode {
|
export class CollapsibleTitleNode extends ElementNode {
|
||||||
static override getType(): string {
|
static override getType(): string {
|
||||||
return 'collapsible-title';
|
return 'collapsible-title'
|
||||||
}
|
}
|
||||||
|
|
||||||
static override clone(node: CollapsibleTitleNode): CollapsibleTitleNode {
|
static override clone(node: CollapsibleTitleNode): CollapsibleTitleNode {
|
||||||
return new CollapsibleTitleNode(node.__key);
|
return new CollapsibleTitleNode(node.__key)
|
||||||
}
|
}
|
||||||
|
|
||||||
override createDOM(config: EditorConfig, editor: LexicalEditor): HTMLElement {
|
override createDOM(_config: EditorConfig, editor: LexicalEditor): HTMLElement {
|
||||||
const dom = document.createElement('summary');
|
const dom = document.createElement('summary')
|
||||||
dom.classList.add('Collapsible__title');
|
dom.classList.add('Collapsible__title')
|
||||||
dom.onclick = (event) => {
|
dom.onclick = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
event.stopPropagation();
|
event.stopPropagation()
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const containerNode = this.getParentOrThrow();
|
const containerNode = this.getParentOrThrow()
|
||||||
if ($isCollapsibleContainerNode(containerNode)) {
|
if ($isCollapsibleContainerNode(containerNode)) {
|
||||||
containerNode.toggleOpen();
|
containerNode.toggleOpen()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
return dom;
|
return dom
|
||||||
}
|
}
|
||||||
|
|
||||||
override updateDOM(
|
override updateDOM(_prevNode: CollapsibleTitleNode, _dom: HTMLElement): boolean {
|
||||||
prevNode: CollapsibleTitleNode,
|
return false
|
||||||
dom: HTMLElement,
|
|
||||||
): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static importDOM(): DOMConversionMap | null {
|
static importDOM(): DOMConversionMap | null {
|
||||||
return {};
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
static override importJSON(
|
static override importJSON(_serializedNode: SerializedCollapsibleTitleNode): CollapsibleTitleNode {
|
||||||
serializedNode: SerializedCollapsibleTitleNode,
|
return $createCollapsibleTitleNode()
|
||||||
): CollapsibleTitleNode {
|
|
||||||
return $createCollapsibleTitleNode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override exportJSON(): SerializedCollapsibleTitleNode {
|
override exportJSON(): SerializedCollapsibleTitleNode {
|
||||||
@@ -77,53 +72,47 @@ export class CollapsibleTitleNode extends ElementNode {
|
|||||||
...super.exportJSON(),
|
...super.exportJSON(),
|
||||||
type: 'collapsible-title',
|
type: 'collapsible-title',
|
||||||
version: 1,
|
version: 1,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override collapseAtStart(_selection: RangeSelection): boolean {
|
override collapseAtStart(_selection: RangeSelection): boolean {
|
||||||
this.getParentOrThrow().insertBefore(this);
|
this.getParentOrThrow().insertBefore(this)
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override insertNewAfter(): ElementNode {
|
override insertNewAfter(): ElementNode {
|
||||||
const containerNode = this.getParentOrThrow();
|
const containerNode = this.getParentOrThrow()
|
||||||
|
|
||||||
if (!$isCollapsibleContainerNode(containerNode)) {
|
if (!$isCollapsibleContainerNode(containerNode)) {
|
||||||
throw new Error(
|
throw new Error('CollapsibleTitleNode expects to be child of CollapsibleContainerNode')
|
||||||
'CollapsibleTitleNode expects to be child of CollapsibleContainerNode',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (containerNode.getOpen()) {
|
if (containerNode.getOpen()) {
|
||||||
const contentNode = this.getNextSibling();
|
const contentNode = this.getNextSibling()
|
||||||
if (!$isCollapsibleContentNode(contentNode)) {
|
if (!$isCollapsibleContentNode(contentNode)) {
|
||||||
throw new Error(
|
throw new Error('CollapsibleTitleNode expects to have CollapsibleContentNode sibling')
|
||||||
'CollapsibleTitleNode expects to have CollapsibleContentNode sibling',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstChild = contentNode.getFirstChild();
|
const firstChild = contentNode.getFirstChild()
|
||||||
if ($isElementNode(firstChild)) {
|
if ($isElementNode(firstChild)) {
|
||||||
return firstChild;
|
return firstChild
|
||||||
} else {
|
} else {
|
||||||
const paragraph = $createParagraphNode();
|
const paragraph = $createParagraphNode()
|
||||||
contentNode.append(paragraph);
|
contentNode.append(paragraph)
|
||||||
return paragraph;
|
return paragraph
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const paragraph = $createParagraphNode();
|
const paragraph = $createParagraphNode()
|
||||||
containerNode.insertAfter(paragraph);
|
containerNode.insertAfter(paragraph)
|
||||||
return paragraph;
|
return paragraph
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $createCollapsibleTitleNode(): CollapsibleTitleNode {
|
export function $createCollapsibleTitleNode(): CollapsibleTitleNode {
|
||||||
return new CollapsibleTitleNode();
|
return new CollapsibleTitleNode()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function $isCollapsibleTitleNode(
|
export function $isCollapsibleTitleNode(node: LexicalNode | null | undefined): node is CollapsibleTitleNode {
|
||||||
node: LexicalNode | null | undefined,
|
return node instanceof CollapsibleTitleNode
|
||||||
): node is CollapsibleTitleNode {
|
|
||||||
return node instanceof CollapsibleTitleNode;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './Collapsible.css';
|
import './Collapsible.css'
|
||||||
|
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
|
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
||||||
import {
|
import {
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
$getNodeByKey,
|
$getNodeByKey,
|
||||||
@@ -25,41 +25,31 @@ import {
|
|||||||
INSERT_PARAGRAPH_COMMAND,
|
INSERT_PARAGRAPH_COMMAND,
|
||||||
KEY_ARROW_DOWN_COMMAND,
|
KEY_ARROW_DOWN_COMMAND,
|
||||||
NodeKey,
|
NodeKey,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
import {useEffect} from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
$createCollapsibleContainerNode,
|
$createCollapsibleContainerNode,
|
||||||
$isCollapsibleContainerNode,
|
$isCollapsibleContainerNode,
|
||||||
CollapsibleContainerNode,
|
CollapsibleContainerNode,
|
||||||
} from './CollapsibleContainerNode';
|
} from './CollapsibleContainerNode'
|
||||||
import {
|
import {
|
||||||
$createCollapsibleContentNode,
|
$createCollapsibleContentNode,
|
||||||
$isCollapsibleContentNode,
|
$isCollapsibleContentNode,
|
||||||
CollapsibleContentNode,
|
CollapsibleContentNode,
|
||||||
} from './CollapsibleContentNode';
|
} from './CollapsibleContentNode'
|
||||||
import {
|
import { $createCollapsibleTitleNode, $isCollapsibleTitleNode, CollapsibleTitleNode } from './CollapsibleTitleNode'
|
||||||
$createCollapsibleTitleNode,
|
|
||||||
$isCollapsibleTitleNode,
|
|
||||||
CollapsibleTitleNode,
|
|
||||||
} from './CollapsibleTitleNode';
|
|
||||||
|
|
||||||
export const INSERT_COLLAPSIBLE_COMMAND = createCommand<void>();
|
export const INSERT_COLLAPSIBLE_COMMAND = createCommand<void>()
|
||||||
export const TOGGLE_COLLAPSIBLE_COMMAND = createCommand<NodeKey>();
|
export const TOGGLE_COLLAPSIBLE_COMMAND = createCommand<NodeKey>()
|
||||||
|
|
||||||
export default function CollapsiblePlugin(): JSX.Element | null {
|
export default function CollapsiblePlugin(): JSX.Element | null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!editor.hasNodes([CollapsibleContainerNode, CollapsibleTitleNode, CollapsibleContentNode])) {
|
||||||
!editor.hasNodes([
|
|
||||||
CollapsibleContainerNode,
|
|
||||||
CollapsibleTitleNode,
|
|
||||||
CollapsibleContentNode,
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'CollapsiblePlugin: CollapsibleContainerNode, CollapsibleTitleNode, or CollapsibleContentNode not registered on editor',
|
'CollapsiblePlugin: CollapsibleContainerNode, CollapsibleTitleNode, or CollapsibleContentNode not registered on editor',
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeRegister(
|
return mergeRegister(
|
||||||
@@ -67,32 +57,28 @@ export default function CollapsiblePlugin(): JSX.Element | null {
|
|||||||
// "Container > Title + Content" it'll unwrap nodes and convert it back
|
// "Container > Title + Content" it'll unwrap nodes and convert it back
|
||||||
// to regular content.
|
// to regular content.
|
||||||
editor.registerNodeTransform(CollapsibleContentNode, (node) => {
|
editor.registerNodeTransform(CollapsibleContentNode, (node) => {
|
||||||
const parent = node.getParent();
|
const parent = node.getParent()
|
||||||
if (!$isCollapsibleContainerNode(parent)) {
|
if (!$isCollapsibleContainerNode(parent)) {
|
||||||
const children = node.getChildren();
|
const children = node.getChildren()
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
node.insertAfter(child);
|
node.insertAfter(child)
|
||||||
}
|
}
|
||||||
node.remove();
|
node.remove()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
editor.registerNodeTransform(CollapsibleTitleNode, (node) => {
|
editor.registerNodeTransform(CollapsibleTitleNode, (node) => {
|
||||||
const parent = node.getParent();
|
const parent = node.getParent()
|
||||||
if (!$isCollapsibleContainerNode(parent)) {
|
if (!$isCollapsibleContainerNode(parent)) {
|
||||||
node.replace($createParagraphNode().append(...node.getChildren()));
|
node.replace($createParagraphNode().append(...node.getChildren()))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
editor.registerNodeTransform(CollapsibleContainerNode, (node) => {
|
editor.registerNodeTransform(CollapsibleContainerNode, (node) => {
|
||||||
const children = node.getChildren();
|
const children = node.getChildren()
|
||||||
if (
|
if (children.length !== 2 || !$isCollapsibleTitleNode(children[0]) || !$isCollapsibleContentNode(children[1])) {
|
||||||
children.length !== 2 ||
|
|
||||||
!$isCollapsibleTitleNode(children[0]) ||
|
|
||||||
!$isCollapsibleContentNode(children[1])
|
|
||||||
) {
|
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
node.insertAfter(child);
|
node.insertAfter(child)
|
||||||
}
|
}
|
||||||
node.remove();
|
node.remove()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
// This handles the case when container is collapsed and we delete its previous sibling
|
// This handles the case when container is collapsed and we delete its previous sibling
|
||||||
@@ -102,28 +88,24 @@ export default function CollapsiblePlugin(): JSX.Element | null {
|
|||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
DELETE_CHARACTER_COMMAND,
|
DELETE_CHARACTER_COMMAND,
|
||||||
() => {
|
() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
if (
|
if (!$isRangeSelection(selection) || !selection.isCollapsed() || selection.anchor.offset !== 0) {
|
||||||
!$isRangeSelection(selection) ||
|
return false
|
||||||
!selection.isCollapsed() ||
|
|
||||||
selection.anchor.offset !== 0
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchorNode = selection.anchor.getNode();
|
const anchorNode = selection.anchor.getNode()
|
||||||
const topLevelElement = anchorNode.getTopLevelElement();
|
const topLevelElement = anchorNode.getTopLevelElement()
|
||||||
if (topLevelElement === null) {
|
if (topLevelElement === null) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = topLevelElement.getPreviousSibling();
|
const container = topLevelElement.getPreviousSibling()
|
||||||
if (!$isCollapsibleContainerNode(container) || container.getOpen()) {
|
if (!$isCollapsibleContainerNode(container) || container.getOpen()) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
container.setOpen(true);
|
container.setOpen(true)
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
),
|
),
|
||||||
@@ -134,25 +116,22 @@ export default function CollapsiblePlugin(): JSX.Element | null {
|
|||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
KEY_ARROW_DOWN_COMMAND,
|
KEY_ARROW_DOWN_COMMAND,
|
||||||
() => {
|
() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
|
if (!$isRangeSelection(selection) || !selection.isCollapsed()) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = $findMatchingParent(
|
const container = $findMatchingParent(selection.anchor.getNode(), $isCollapsibleContainerNode)
|
||||||
selection.anchor.getNode(),
|
|
||||||
$isCollapsibleContainerNode,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (container === null) {
|
if (container === null) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = container.getParent();
|
const parent = container.getParent()
|
||||||
if (parent !== null && parent.getLastChild() === container) {
|
if (parent !== null && parent.getLastChild() === container) {
|
||||||
parent.append($createParagraphNode());
|
parent.append($createParagraphNode())
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
),
|
),
|
||||||
@@ -160,33 +139,30 @@ export default function CollapsiblePlugin(): JSX.Element | null {
|
|||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
INSERT_PARAGRAPH_COMMAND,
|
INSERT_PARAGRAPH_COMMAND,
|
||||||
() => {
|
() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const windowEvent: KeyboardEvent | undefined = editor._window?.event;
|
const windowEvent: KeyboardEvent | undefined = editor._window?.event
|
||||||
|
|
||||||
if (
|
if (windowEvent && (windowEvent.ctrlKey || windowEvent.metaKey) && windowEvent.key === 'Enter') {
|
||||||
windowEvent &&
|
const selection = $getPreviousSelection()
|
||||||
(windowEvent.ctrlKey || windowEvent.metaKey) &&
|
|
||||||
windowEvent.key === 'Enter'
|
|
||||||
) {
|
|
||||||
const selection = $getPreviousSelection();
|
|
||||||
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
||||||
const parent = $findMatchingParent(
|
const parent = $findMatchingParent(
|
||||||
selection.anchor.getNode(),
|
selection.anchor.getNode(),
|
||||||
(node) => $isElementNode(node) && !node.isInline(),
|
(node) => $isElementNode(node) && !node.isInline(),
|
||||||
);
|
)
|
||||||
|
|
||||||
if ($isCollapsibleTitleNode(parent)) {
|
if ($isCollapsibleTitleNode(parent)) {
|
||||||
const container = parent.getParent();
|
const container = parent.getParent()
|
||||||
if ($isCollapsibleContainerNode(container)) {
|
if ($isCollapsibleContainerNode(container)) {
|
||||||
container.toggleOpen();
|
container.toggleOpen()
|
||||||
$setSelection(selection.clone());
|
$setSelection(selection.clone())
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
),
|
),
|
||||||
@@ -194,25 +170,20 @@ export default function CollapsiblePlugin(): JSX.Element | null {
|
|||||||
INSERT_COLLAPSIBLE_COMMAND,
|
INSERT_COLLAPSIBLE_COMMAND,
|
||||||
() => {
|
() => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
|
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = $createCollapsibleTitleNode();
|
const title = $createCollapsibleTitleNode()
|
||||||
const content = $createCollapsibleContentNode().append(
|
const content = $createCollapsibleContentNode().append($createParagraphNode())
|
||||||
$createParagraphNode(),
|
const container = $createCollapsibleContainerNode(true).append(title, content)
|
||||||
);
|
selection.insertNodes([container])
|
||||||
const container = $createCollapsibleContainerNode(true).append(
|
title.selectStart()
|
||||||
title,
|
})
|
||||||
content,
|
|
||||||
);
|
|
||||||
selection.insertNodes([container]);
|
|
||||||
title.selectStart();
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
),
|
),
|
||||||
@@ -220,17 +191,17 @@ export default function CollapsiblePlugin(): JSX.Element | null {
|
|||||||
TOGGLE_COLLAPSIBLE_COMMAND,
|
TOGGLE_COLLAPSIBLE_COMMAND,
|
||||||
(key: NodeKey) => {
|
(key: NodeKey) => {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const containerNode = $getNodeByKey(key);
|
const containerNode = $getNodeByKey(key)
|
||||||
if ($isCollapsibleContainerNode(containerNode)) {
|
if ($isCollapsibleContainerNode(containerNode)) {
|
||||||
containerNode.toggleOpen();
|
containerNode.toggleOpen()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}, [editor]);
|
}, [editor])
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import {$createListNode, $isListNode} from '@lexical/list';
|
import { $createListNode, $isListNode } from '@lexical/list'
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {eventFiles} from '@lexical/rich-text';
|
import { eventFiles } from '@lexical/rich-text'
|
||||||
import {mergeRegister} from '@lexical/utils';
|
import { mergeRegister } from '@lexical/utils'
|
||||||
import {
|
import {
|
||||||
$getNearestNodeFromDOMNode,
|
$getNearestNodeFromDOMNode,
|
||||||
$getNodeByKey,
|
$getNodeByKey,
|
||||||
@@ -19,185 +19,155 @@ import {
|
|||||||
DROP_COMMAND,
|
DROP_COMMAND,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
import {
|
import { DragEvent as ReactDragEvent, TouchEvent, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
DragEvent as ReactDragEvent,
|
import { createPortal } from 'react-dom'
|
||||||
TouchEvent,
|
import { BlockIcon } from '@standardnotes/icons'
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import {createPortal} from 'react-dom';
|
|
||||||
import {BlockIcon} from '@standardnotes/icons';
|
|
||||||
|
|
||||||
import {isHTMLElement} from '../../Utils/guard';
|
import { isHTMLElement } from '../../Utils/guard'
|
||||||
import {Point} from '../../Utils/point';
|
import { Point } from '../../Utils/point'
|
||||||
import {ContainsPointReturn, Rect} from '../../Utils/rect';
|
import { ContainsPointReturn, Rect } from '../../Utils/rect'
|
||||||
|
|
||||||
const DRAGGABLE_BLOCK_MENU_LEFT_SPACE = -2;
|
const DRAGGABLE_BLOCK_MENU_LEFT_SPACE = -2
|
||||||
const TARGET_LINE_HALF_HEIGHT = 2;
|
const TARGET_LINE_HALF_HEIGHT = 2
|
||||||
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu';
|
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu'
|
||||||
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block';
|
const DRAG_DATA_FORMAT = 'application/x-lexical-drag-block'
|
||||||
const TEXT_BOX_HORIZONTAL_PADDING = 24;
|
const TEXT_BOX_HORIZONTAL_PADDING = 24
|
||||||
|
|
||||||
const Downward = 1;
|
const Downward = 1
|
||||||
const Upward = -1;
|
const Upward = -1
|
||||||
const Indeterminate = 0;
|
const Indeterminate = 0
|
||||||
|
|
||||||
let prevIndex = Infinity;
|
let prevIndex = Infinity
|
||||||
|
|
||||||
function getCurrentIndex(keysLength: number): number {
|
function getCurrentIndex(keysLength: number): number {
|
||||||
if (keysLength === 0) {
|
if (keysLength === 0) {
|
||||||
return Infinity;
|
return Infinity
|
||||||
}
|
}
|
||||||
if (prevIndex >= 0 && prevIndex < keysLength) {
|
if (prevIndex >= 0 && prevIndex < keysLength) {
|
||||||
return prevIndex;
|
return prevIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.floor(keysLength / 2);
|
return Math.floor(keysLength / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
|
function getTopLevelNodeKeys(editor: LexicalEditor): string[] {
|
||||||
return editor.getEditorState().read(() => $getRoot().getChildrenKeys());
|
return editor.getEditorState().read(() => $getRoot().getChildrenKeys())
|
||||||
}
|
}
|
||||||
|
|
||||||
function elementContainingEventLocation(
|
function elementContainingEventLocation(
|
||||||
anchorElem: HTMLElement,
|
anchorElem: HTMLElement,
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
eventLocation: Point,
|
eventLocation: Point,
|
||||||
): {contains: ContainsPointReturn; element: HTMLElement} {
|
): { contains: ContainsPointReturn; element: HTMLElement } {
|
||||||
const anchorElementRect = anchorElem.getBoundingClientRect();
|
const anchorElementRect = anchorElem.getBoundingClientRect()
|
||||||
|
|
||||||
const elementDomRect = Rect.fromDOM(element);
|
const elementDomRect = Rect.fromDOM(element)
|
||||||
const {marginTop, marginBottom} = window.getComputedStyle(element);
|
const { marginTop, marginBottom } = window.getComputedStyle(element)
|
||||||
|
|
||||||
const rect = elementDomRect.generateNewRect({
|
const rect = elementDomRect.generateNewRect({
|
||||||
bottom: elementDomRect.bottom + parseFloat(marginBottom),
|
bottom: elementDomRect.bottom + parseFloat(marginBottom),
|
||||||
left: anchorElementRect.left,
|
left: anchorElementRect.left,
|
||||||
right: anchorElementRect.right,
|
right: anchorElementRect.right,
|
||||||
top: elementDomRect.top - parseFloat(marginTop),
|
top: elementDomRect.top - parseFloat(marginTop),
|
||||||
});
|
})
|
||||||
|
|
||||||
const children = Array.from(element.children);
|
const children = Array.from(element.children)
|
||||||
|
|
||||||
const shouldRecurseIntoChildren = ['UL', 'OL', 'LI'].includes(
|
const shouldRecurseIntoChildren = ['UL', 'OL', 'LI'].includes(element.tagName)
|
||||||
element.tagName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldRecurseIntoChildren) {
|
if (shouldRecurseIntoChildren) {
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
const isLeaf = child.children.length === 0;
|
const isLeaf = child.children.length === 0
|
||||||
if (isLeaf) {
|
if (isLeaf) {
|
||||||
continue;
|
continue
|
||||||
}
|
}
|
||||||
const childResult = elementContainingEventLocation(
|
const childResult = elementContainingEventLocation(anchorElem, child as HTMLElement, eventLocation)
|
||||||
anchorElem,
|
|
||||||
child as HTMLElement,
|
|
||||||
eventLocation,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (childResult.contains.result) {
|
if (childResult.contains.result) {
|
||||||
return childResult;
|
return childResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {contains: rect.contains(eventLocation), element: element};
|
return { contains: rect.contains(eventLocation), element: element }
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBlockElement(
|
function getBlockElement(anchorElem: HTMLElement, editor: LexicalEditor, eventLocation: Point): HTMLElement | null {
|
||||||
anchorElem: HTMLElement,
|
const topLevelNodeKeys = getTopLevelNodeKeys(editor)
|
||||||
editor: LexicalEditor,
|
|
||||||
eventLocation: Point,
|
|
||||||
): HTMLElement | null {
|
|
||||||
const topLevelNodeKeys = getTopLevelNodeKeys(editor);
|
|
||||||
|
|
||||||
let blockElem: HTMLElement | null = null;
|
let blockElem: HTMLElement | null = null
|
||||||
|
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
let index = getCurrentIndex(topLevelNodeKeys.length);
|
let index = getCurrentIndex(topLevelNodeKeys.length)
|
||||||
let direction = Indeterminate;
|
let direction = Indeterminate
|
||||||
|
|
||||||
while (index >= 0 && index < topLevelNodeKeys.length) {
|
while (index >= 0 && index < topLevelNodeKeys.length) {
|
||||||
const key = topLevelNodeKeys[index];
|
const key = topLevelNodeKeys[index]
|
||||||
const elem = editor.getElementByKey(key);
|
const elem = editor.getElementByKey(key)
|
||||||
if (elem === null) {
|
if (elem === null) {
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
const {contains, element} = elementContainingEventLocation(
|
const { contains, element } = elementContainingEventLocation(anchorElem, elem, eventLocation)
|
||||||
anchorElem,
|
|
||||||
elem,
|
|
||||||
eventLocation,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contains.result) {
|
if (contains.result) {
|
||||||
blockElem = element;
|
blockElem = element
|
||||||
prevIndex = index;
|
prevIndex = index
|
||||||
break;
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if (direction === Indeterminate) {
|
if (direction === Indeterminate) {
|
||||||
if (contains.reason.isOnTopSide) {
|
if (contains.reason.isOnTopSide) {
|
||||||
direction = Upward;
|
direction = Upward
|
||||||
} else if (contains.reason.isOnBottomSide) {
|
} else if (contains.reason.isOnBottomSide) {
|
||||||
direction = Downward;
|
direction = Downward
|
||||||
} else {
|
} else {
|
||||||
// stop search block element
|
// stop search block element
|
||||||
direction = Infinity;
|
direction = Infinity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index += direction;
|
index += direction
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
return blockElem;
|
return blockElem
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOnMenu(element: HTMLElement): boolean {
|
function isOnMenu(element: HTMLElement): boolean {
|
||||||
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`);
|
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMenuPosition(
|
function setMenuPosition(targetElem: HTMLElement | null, floatingElem: HTMLElement, anchorElem: HTMLElement) {
|
||||||
targetElem: HTMLElement | null,
|
|
||||||
floatingElem: HTMLElement,
|
|
||||||
anchorElem: HTMLElement,
|
|
||||||
) {
|
|
||||||
if (!targetElem) {
|
if (!targetElem) {
|
||||||
floatingElem.style.opacity = '0';
|
floatingElem.style.opacity = '0'
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetRect = targetElem.getBoundingClientRect();
|
const targetRect = targetElem.getBoundingClientRect()
|
||||||
const targetStyle = window.getComputedStyle(targetElem);
|
const targetStyle = window.getComputedStyle(targetElem)
|
||||||
const floatingElemRect = floatingElem.getBoundingClientRect();
|
const floatingElemRect = floatingElem.getBoundingClientRect()
|
||||||
const anchorElementRect = anchorElem.getBoundingClientRect();
|
const anchorElementRect = anchorElem.getBoundingClientRect()
|
||||||
|
|
||||||
const top =
|
const top =
|
||||||
targetRect.top +
|
targetRect.top + (parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 - anchorElementRect.top
|
||||||
(parseInt(targetStyle.lineHeight, 10) - floatingElemRect.height) / 2 -
|
|
||||||
anchorElementRect.top;
|
|
||||||
|
|
||||||
const left = DRAGGABLE_BLOCK_MENU_LEFT_SPACE;
|
const left = DRAGGABLE_BLOCK_MENU_LEFT_SPACE
|
||||||
|
|
||||||
floatingElem.style.opacity = '1';
|
floatingElem.style.opacity = '1'
|
||||||
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
floatingElem.style.transform = `translate(${left}px, ${top}px)`
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDragImage(
|
function setDragImage(dataTransfer: DataTransfer, draggableBlockElem: HTMLElement) {
|
||||||
dataTransfer: DataTransfer,
|
const { transform } = draggableBlockElem.style
|
||||||
draggableBlockElem: HTMLElement,
|
|
||||||
) {
|
|
||||||
const {transform} = draggableBlockElem.style;
|
|
||||||
|
|
||||||
// Remove dragImage borders
|
// Remove dragImage borders
|
||||||
draggableBlockElem.style.transform = 'translateZ(0)';
|
draggableBlockElem.style.transform = 'translateZ(0)'
|
||||||
dataTransfer.setDragImage(draggableBlockElem, 0, 0);
|
dataTransfer.setDragImage(draggableBlockElem, 0, 0)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
draggableBlockElem.style.transform = transform;
|
draggableBlockElem.style.transform = transform
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTargetLine(
|
function setTargetLine(
|
||||||
@@ -206,293 +176,258 @@ function setTargetLine(
|
|||||||
mouseY: number,
|
mouseY: number,
|
||||||
anchorElem: HTMLElement,
|
anchorElem: HTMLElement,
|
||||||
) {
|
) {
|
||||||
const targetStyle = window.getComputedStyle(targetBlockElem);
|
const targetStyle = window.getComputedStyle(targetBlockElem)
|
||||||
const {top: targetBlockElemTop, height: targetBlockElemHeight} =
|
const { top: targetBlockElemTop, height: targetBlockElemHeight } = targetBlockElem.getBoundingClientRect()
|
||||||
targetBlockElem.getBoundingClientRect();
|
const { top: anchorTop, width: anchorWidth } = anchorElem.getBoundingClientRect()
|
||||||
const {top: anchorTop, width: anchorWidth} =
|
|
||||||
anchorElem.getBoundingClientRect();
|
|
||||||
|
|
||||||
let lineTop = targetBlockElemTop;
|
let lineTop = targetBlockElemTop
|
||||||
// At the bottom of the target
|
// At the bottom of the target
|
||||||
if (mouseY - targetBlockElemTop > targetBlockElemHeight / 2) {
|
if (mouseY - targetBlockElemTop > targetBlockElemHeight / 2) {
|
||||||
lineTop += targetBlockElemHeight + parseFloat(targetStyle.marginBottom);
|
lineTop += targetBlockElemHeight + parseFloat(targetStyle.marginBottom)
|
||||||
} else {
|
} else {
|
||||||
lineTop -= parseFloat(targetStyle.marginTop);
|
lineTop -= parseFloat(targetStyle.marginTop)
|
||||||
}
|
}
|
||||||
|
|
||||||
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT;
|
const top = lineTop - anchorTop - TARGET_LINE_HALF_HEIGHT
|
||||||
const left = TEXT_BOX_HORIZONTAL_PADDING - DRAGGABLE_BLOCK_MENU_LEFT_SPACE;
|
const left = TEXT_BOX_HORIZONTAL_PADDING - DRAGGABLE_BLOCK_MENU_LEFT_SPACE
|
||||||
|
|
||||||
targetLineElem.style.transform = `translate(${left}px, ${top}px)`;
|
targetLineElem.style.transform = `translate(${left}px, ${top}px)`
|
||||||
targetLineElem.style.width = `${
|
targetLineElem.style.width = `${anchorWidth - (TEXT_BOX_HORIZONTAL_PADDING - DRAGGABLE_BLOCK_MENU_LEFT_SPACE) * 2}px`
|
||||||
anchorWidth -
|
targetLineElem.style.opacity = '.6'
|
||||||
(TEXT_BOX_HORIZONTAL_PADDING - DRAGGABLE_BLOCK_MENU_LEFT_SPACE) * 2
|
|
||||||
}px`;
|
|
||||||
targetLineElem.style.opacity = '.6';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideTargetLine(targetLineElem: HTMLElement | null) {
|
function hideTargetLine(targetLineElem: HTMLElement | null) {
|
||||||
if (targetLineElem) {
|
if (targetLineElem) {
|
||||||
targetLineElem.style.opacity = '0';
|
targetLineElem.style.opacity = '0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function useDraggableBlockMenu(
|
function useDraggableBlockMenu(editor: LexicalEditor, anchorElem: HTMLElement, isEditable: boolean): JSX.Element {
|
||||||
editor: LexicalEditor,
|
const scrollerElem = anchorElem.parentElement
|
||||||
anchorElem: HTMLElement,
|
|
||||||
isEditable: boolean,
|
|
||||||
): JSX.Element {
|
|
||||||
const scrollerElem = anchorElem.parentElement;
|
|
||||||
|
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
const targetLineRef = useRef<HTMLDivElement>(null);
|
const targetLineRef = useRef<HTMLDivElement>(null)
|
||||||
const [draggableBlockElem, setDraggableBlockElem] =
|
const [draggableBlockElem, setDraggableBlockElem] = useState<HTMLElement | null>(null)
|
||||||
useState<HTMLElement | null>(null);
|
const dragDataRef = useRef<string | null>(null)
|
||||||
const dragDataRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onMouseMove(event: MouseEvent) {
|
function onMouseMove(event: MouseEvent) {
|
||||||
const target = event.target;
|
const target = event.target
|
||||||
if (!isHTMLElement(target)) {
|
if (!isHTMLElement(target)) {
|
||||||
setDraggableBlockElem(null);
|
setDraggableBlockElem(null)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOnMenu(target)) {
|
if (isOnMenu(target)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const _draggableBlockElem = getBlockElement(
|
const _draggableBlockElem = getBlockElement(anchorElem, editor, new Point(event.clientX, event.clientY))
|
||||||
anchorElem,
|
|
||||||
editor,
|
|
||||||
new Point(event.clientX, event.clientY),
|
|
||||||
);
|
|
||||||
|
|
||||||
setDraggableBlockElem(_draggableBlockElem);
|
setDraggableBlockElem(_draggableBlockElem)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseLeave() {
|
function onMouseLeave() {
|
||||||
setDraggableBlockElem(null);
|
setDraggableBlockElem(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollerElem?.addEventListener('mousemove', onMouseMove);
|
scrollerElem?.addEventListener('mousemove', onMouseMove)
|
||||||
scrollerElem?.addEventListener('mouseleave', onMouseLeave);
|
scrollerElem?.addEventListener('mouseleave', onMouseLeave)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
scrollerElem?.removeEventListener('mousemove', onMouseMove);
|
scrollerElem?.removeEventListener('mousemove', onMouseMove)
|
||||||
scrollerElem?.removeEventListener('mouseleave', onMouseLeave);
|
scrollerElem?.removeEventListener('mouseleave', onMouseLeave)
|
||||||
};
|
}
|
||||||
}, [scrollerElem, anchorElem, editor]);
|
}, [scrollerElem, anchorElem, editor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (menuRef.current) {
|
if (menuRef.current) {
|
||||||
setMenuPosition(draggableBlockElem, menuRef.current, anchorElem);
|
setMenuPosition(draggableBlockElem, menuRef.current, anchorElem)
|
||||||
}
|
}
|
||||||
}, [anchorElem, draggableBlockElem]);
|
}, [anchorElem, draggableBlockElem])
|
||||||
|
|
||||||
const insertDraggedNode = useCallback(
|
const insertDraggedNode = useCallback(
|
||||||
(
|
(draggedNode: LexicalNode, targetNode: LexicalNode, targetBlockElem: HTMLElement, pageY: number) => {
|
||||||
draggedNode: LexicalNode,
|
let nodeToInsert = draggedNode
|
||||||
targetNode: LexicalNode,
|
const targetParent = targetNode.getParent()
|
||||||
targetBlockElem: HTMLElement,
|
const sourceParent = draggedNode.getParent()
|
||||||
pageY: number,
|
|
||||||
) => {
|
|
||||||
let nodeToInsert = draggedNode;
|
|
||||||
const targetParent = targetNode.getParent();
|
|
||||||
const sourceParent = draggedNode.getParent();
|
|
||||||
|
|
||||||
if ($isListNode(sourceParent) && !$isListNode(targetParent)) {
|
if ($isListNode(sourceParent) && !$isListNode(targetParent)) {
|
||||||
const newList = $createListNode(sourceParent.getListType());
|
const newList = $createListNode(sourceParent.getListType())
|
||||||
newList.append(draggedNode);
|
newList.append(draggedNode)
|
||||||
nodeToInsert = newList;
|
nodeToInsert = newList
|
||||||
}
|
}
|
||||||
|
|
||||||
const {top, height} = targetBlockElem.getBoundingClientRect();
|
const { top, height } = targetBlockElem.getBoundingClientRect()
|
||||||
const shouldInsertAfter = pageY - top > height / 2;
|
const shouldInsertAfter = pageY - top > height / 2
|
||||||
if (shouldInsertAfter) {
|
if (shouldInsertAfter) {
|
||||||
targetNode.insertAfter(nodeToInsert);
|
targetNode.insertAfter(nodeToInsert)
|
||||||
} else {
|
} else {
|
||||||
targetNode.insertBefore(nodeToInsert);
|
targetNode.insertBefore(nodeToInsert)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
);
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onDragover(event: DragEvent): boolean {
|
function onDragover(event: DragEvent): boolean {
|
||||||
const [isFileTransfer] = eventFiles(event);
|
const [isFileTransfer] = eventFiles(event)
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
const {pageY, target} = event;
|
const { pageY, target } = event
|
||||||
if (!isHTMLElement(target)) {
|
if (!isHTMLElement(target)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
const targetBlockElem = getBlockElement(
|
const targetBlockElem = getBlockElement(anchorElem, editor, new Point(event.pageX, pageY))
|
||||||
anchorElem,
|
const targetLineElem = targetLineRef.current
|
||||||
editor,
|
|
||||||
new Point(event.pageX, pageY),
|
|
||||||
);
|
|
||||||
const targetLineElem = targetLineRef.current;
|
|
||||||
if (targetBlockElem === null || targetLineElem === null) {
|
if (targetBlockElem === null || targetLineElem === null) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem);
|
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem)
|
||||||
// Prevent default event to be able to trigger onDrop events
|
// Prevent default event to be able to trigger onDrop events
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop(event: DragEvent): boolean {
|
function onDrop(event: DragEvent): boolean {
|
||||||
const [isFileTransfer] = eventFiles(event);
|
const [isFileTransfer] = eventFiles(event)
|
||||||
if (isFileTransfer) {
|
if (isFileTransfer) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const {target, dataTransfer, pageY} = event;
|
const { target, dataTransfer, pageY } = event
|
||||||
if (!isHTMLElement(target)) {
|
if (!isHTMLElement(target)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || '';
|
const dragData = dataTransfer?.getData(DRAG_DATA_FORMAT) || ''
|
||||||
const draggedNode = $getNodeByKey(dragData);
|
const draggedNode = $getNodeByKey(dragData)
|
||||||
if (!draggedNode) {
|
if (!draggedNode) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetBlockElem = getBlockElement(
|
const targetBlockElem = getBlockElement(anchorElem, editor, new Point(event.pageX, pageY))
|
||||||
anchorElem,
|
|
||||||
editor,
|
|
||||||
new Point(event.pageX, pageY),
|
|
||||||
);
|
|
||||||
if (!targetBlockElem) {
|
if (!targetBlockElem) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem)
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
if (targetNode === draggedNode) {
|
if (targetNode === draggedNode) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
insertDraggedNode(draggedNode, targetNode, targetBlockElem, event.pageY);
|
insertDraggedNode(draggedNode, targetNode, targetBlockElem, event.pageY)
|
||||||
|
|
||||||
setDraggableBlockElem(null);
|
setDraggableBlockElem(null)
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeRegister(
|
return mergeRegister(
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
DRAGOVER_COMMAND,
|
DRAGOVER_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
return onDragover(event);
|
return onDragover(event)
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
),
|
),
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
DROP_COMMAND,
|
DROP_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
return onDrop(event);
|
return onDrop(event)
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_HIGH,
|
COMMAND_PRIORITY_HIGH,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}, [anchorElem, editor, insertDraggedNode]);
|
}, [anchorElem, editor, insertDraggedNode])
|
||||||
|
|
||||||
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
|
function onDragStart(event: ReactDragEvent<HTMLDivElement>): void {
|
||||||
const dataTransfer = event.dataTransfer;
|
const dataTransfer = event.dataTransfer
|
||||||
if (!dataTransfer || !draggableBlockElem) {
|
if (!dataTransfer || !draggableBlockElem) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setDragImage(dataTransfer, draggableBlockElem);
|
setDragImage(dataTransfer, draggableBlockElem)
|
||||||
let nodeKey = '';
|
let nodeKey = ''
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const node = $getNearestNodeFromDOMNode(draggableBlockElem);
|
const node = $getNearestNodeFromDOMNode(draggableBlockElem)
|
||||||
if (node) {
|
if (node) {
|
||||||
nodeKey = node.getKey();
|
nodeKey = node.getKey()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey);
|
dataTransfer.setData(DRAG_DATA_FORMAT, nodeKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragEnd(): void {
|
function onDragEnd(): void {
|
||||||
hideTargetLine(targetLineRef.current);
|
hideTargetLine(targetLineRef.current)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchStart(): void {
|
function onTouchStart(): void {
|
||||||
if (!draggableBlockElem) {
|
if (!draggableBlockElem) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const node = $getNearestNodeFromDOMNode(draggableBlockElem);
|
const node = $getNearestNodeFromDOMNode(draggableBlockElem)
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const nodeKey = node.getKey();
|
const nodeKey = node.getKey()
|
||||||
dragDataRef.current = nodeKey;
|
dragDataRef.current = nodeKey
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchMove(event: TouchEvent) {
|
function onTouchMove(event: TouchEvent) {
|
||||||
const {pageX, pageY} = event.targetTouches[0];
|
const { pageX, pageY } = event.targetTouches[0]
|
||||||
const rootElement = editor.getRootElement();
|
const rootElement = editor.getRootElement()
|
||||||
if (rootElement) {
|
if (rootElement) {
|
||||||
const {top, bottom} = rootElement.getBoundingClientRect();
|
const { top, bottom } = rootElement.getBoundingClientRect()
|
||||||
const scrollOffset = 20;
|
const scrollOffset = 20
|
||||||
if (pageY - top < scrollOffset) {
|
if (pageY - top < scrollOffset) {
|
||||||
rootElement.scrollTop -= scrollOffset;
|
rootElement.scrollTop -= scrollOffset
|
||||||
} else if (bottom - pageY < scrollOffset) {
|
} else if (bottom - pageY < scrollOffset) {
|
||||||
rootElement.scrollTop += scrollOffset;
|
rootElement.scrollTop += scrollOffset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const targetBlockElem = getBlockElement(
|
const targetBlockElem = getBlockElement(anchorElem, editor, new Point(pageX, pageY))
|
||||||
anchorElem,
|
const targetLineElem = targetLineRef.current
|
||||||
editor,
|
|
||||||
new Point(pageX, pageY),
|
|
||||||
);
|
|
||||||
const targetLineElem = targetLineRef.current;
|
|
||||||
if (targetBlockElem === null || targetLineElem === null) {
|
if (targetBlockElem === null || targetLineElem === null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem);
|
setTargetLine(targetLineElem, targetBlockElem, pageY, anchorElem)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTouchEnd(event: TouchEvent): void {
|
function onTouchEnd(event: TouchEvent): void {
|
||||||
hideTargetLine(targetLineRef.current);
|
hideTargetLine(targetLineRef.current)
|
||||||
|
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const {pageX, pageY} = event.changedTouches[0];
|
const { pageX, pageY } = event.changedTouches[0]
|
||||||
|
|
||||||
const dragData = dragDataRef.current || '';
|
const dragData = dragDataRef.current || ''
|
||||||
const draggedNode = $getNodeByKey(dragData);
|
const draggedNode = $getNodeByKey(dragData)
|
||||||
if (!draggedNode) {
|
if (!draggedNode) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetBlockElem = getBlockElement(
|
const targetBlockElem = getBlockElement(anchorElem, editor, new Point(pageX, pageY))
|
||||||
anchorElem,
|
|
||||||
editor,
|
|
||||||
new Point(pageX, pageY),
|
|
||||||
);
|
|
||||||
if (!targetBlockElem) {
|
if (!targetBlockElem) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem);
|
const targetNode = $getNearestNodeFromDOMNode(targetBlockElem)
|
||||||
|
|
||||||
if (!targetNode) {
|
if (!targetNode) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (targetNode === draggedNode) {
|
if (targetNode === draggedNode) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
insertDraggedNode(draggedNode, targetNode, targetBlockElem, pageY);
|
insertDraggedNode(draggedNode, targetNode, targetBlockElem, pageY)
|
||||||
});
|
})
|
||||||
|
|
||||||
setDraggableBlockElem(null);
|
setDraggableBlockElem(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@@ -505,7 +440,8 @@ function useDraggableBlockMenu(
|
|||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onTouchStart={onTouchStart}
|
onTouchStart={onTouchStart}
|
||||||
onTouchMove={onTouchMove}
|
onTouchMove={onTouchMove}
|
||||||
onTouchEnd={onTouchEnd}>
|
onTouchEnd={onTouchEnd}
|
||||||
|
>
|
||||||
<div className={isEditable ? 'icon' : ''}>
|
<div className={isEditable ? 'icon' : ''}>
|
||||||
<BlockIcon className="text-text pointer-events-none" />
|
<BlockIcon className="text-text pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
@@ -513,14 +449,14 @@ function useDraggableBlockMenu(
|
|||||||
<div className="draggable-block-target-line" ref={targetLineRef} />
|
<div className="draggable-block-target-line" ref={targetLineRef} />
|
||||||
</>,
|
</>,
|
||||||
anchorElem,
|
anchorElem,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DraggableBlockPlugin({
|
export default function DraggableBlockPlugin({
|
||||||
anchorElem = document.body,
|
anchorElem = document.body,
|
||||||
}: {
|
}: {
|
||||||
anchorElem?: HTMLElement;
|
anchorElem?: HTMLElement
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
return useDraggableBlockMenu(editor, anchorElem, editor._editable);
|
return useDraggableBlockMenu(editor, anchorElem, editor._editable)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import {$isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
import { $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
|
import { $findMatchingParent, mergeRegister } from '@lexical/utils'
|
||||||
import {
|
import {
|
||||||
$getSelection,
|
$getSelection,
|
||||||
$isRangeSelection,
|
$isRangeSelection,
|
||||||
@@ -18,54 +18,46 @@ import {
|
|||||||
NodeSelection,
|
NodeSelection,
|
||||||
RangeSelection,
|
RangeSelection,
|
||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import {createPortal} from 'react-dom';
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
import LinkPreview from '../../UI/LinkPreview';
|
import LinkPreview from '../../UI/LinkPreview'
|
||||||
import {getSelectedNode} from '../../Utils/getSelectedNode';
|
import { getSelectedNode } from '../../Utils/getSelectedNode'
|
||||||
import {sanitizeUrl} from '../../Utils/sanitizeUrl';
|
import { sanitizeUrl } from '../../Utils/sanitizeUrl'
|
||||||
import {setFloatingElemPosition} from '../../Utils/setFloatingElemPosition';
|
import { setFloatingElemPosition } from '../../Utils/setFloatingElemPosition'
|
||||||
import {LexicalPencilFill} from '@standardnotes/icons';
|
import { LexicalPencilFill } from '@standardnotes/icons'
|
||||||
import {IconComponent} from '../../../Lexical/Theme/IconComponent';
|
import { IconComponent } from '../../../Lexical/Theme/IconComponent'
|
||||||
|
|
||||||
function FloatingLinkEditor({
|
function FloatingLinkEditor({ editor, anchorElem }: { editor: LexicalEditor; anchorElem: HTMLElement }): JSX.Element {
|
||||||
editor,
|
const editorRef = useRef<HTMLDivElement | null>(null)
|
||||||
anchorElem,
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
}: {
|
const [linkUrl, setLinkUrl] = useState('')
|
||||||
editor: LexicalEditor;
|
const [isEditMode, setEditMode] = useState(false)
|
||||||
anchorElem: HTMLElement;
|
const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null)
|
||||||
}): JSX.Element {
|
|
||||||
const editorRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [linkUrl, setLinkUrl] = useState('');
|
|
||||||
const [isEditMode, setEditMode] = useState(false);
|
|
||||||
const [lastSelection, setLastSelection] = useState<
|
|
||||||
RangeSelection | GridSelection | NodeSelection | null
|
|
||||||
>(null);
|
|
||||||
|
|
||||||
const updateLinkEditor = useCallback(() => {
|
const updateLinkEditor = useCallback(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
if ($isRangeSelection(selection)) {
|
if ($isRangeSelection(selection)) {
|
||||||
const node = getSelectedNode(selection);
|
const node = getSelectedNode(selection)
|
||||||
const parent = node.getParent();
|
const parent = node.getParent()
|
||||||
if ($isLinkNode(parent)) {
|
if ($isLinkNode(parent)) {
|
||||||
setLinkUrl(parent.getURL());
|
setLinkUrl(parent.getURL())
|
||||||
} else if ($isLinkNode(node)) {
|
} else if ($isLinkNode(node)) {
|
||||||
setLinkUrl(node.getURL());
|
setLinkUrl(node.getURL())
|
||||||
} else {
|
} else {
|
||||||
setLinkUrl('');
|
setLinkUrl('')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const editorElem = editorRef.current;
|
const editorElem = editorRef.current
|
||||||
const nativeSelection = window.getSelection();
|
const nativeSelection = window.getSelection()
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement
|
||||||
|
|
||||||
if (editorElem === null) {
|
if (editorElem === null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootElement = editor.getRootElement();
|
const rootElement = editor.getRootElement()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selection !== null &&
|
selection !== null &&
|
||||||
@@ -73,86 +65,86 @@ function FloatingLinkEditor({
|
|||||||
rootElement !== null &&
|
rootElement !== null &&
|
||||||
rootElement.contains(nativeSelection.anchorNode)
|
rootElement.contains(nativeSelection.anchorNode)
|
||||||
) {
|
) {
|
||||||
const domRange = nativeSelection.getRangeAt(0);
|
const domRange = nativeSelection.getRangeAt(0)
|
||||||
let rect;
|
let rect
|
||||||
if (nativeSelection.anchorNode === rootElement) {
|
if (nativeSelection.anchorNode === rootElement) {
|
||||||
let inner = rootElement;
|
let inner = rootElement
|
||||||
while (inner.firstElementChild != null) {
|
while (inner.firstElementChild != null) {
|
||||||
inner = inner.firstElementChild as HTMLElement;
|
inner = inner.firstElementChild as HTMLElement
|
||||||
}
|
}
|
||||||
rect = inner.getBoundingClientRect();
|
rect = inner.getBoundingClientRect()
|
||||||
} else {
|
} else {
|
||||||
rect = domRange.getBoundingClientRect();
|
rect = domRange.getBoundingClientRect()
|
||||||
}
|
}
|
||||||
|
|
||||||
setFloatingElemPosition(rect, editorElem, anchorElem);
|
setFloatingElemPosition(rect, editorElem, anchorElem)
|
||||||
setLastSelection(selection);
|
setLastSelection(selection)
|
||||||
} else if (!activeElement || activeElement.className !== 'link-input') {
|
} else if (!activeElement || activeElement.className !== 'link-input') {
|
||||||
if (rootElement !== null) {
|
if (rootElement !== null) {
|
||||||
setFloatingElemPosition(null, editorElem, anchorElem);
|
setFloatingElemPosition(null, editorElem, anchorElem)
|
||||||
}
|
}
|
||||||
setLastSelection(null);
|
setLastSelection(null)
|
||||||
setEditMode(false);
|
setEditMode(false)
|
||||||
setLinkUrl('');
|
setLinkUrl('')
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
}, [anchorElem, editor]);
|
}, [anchorElem, editor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollerElem = anchorElem.parentElement;
|
const scrollerElem = anchorElem.parentElement
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
updateLinkEditor();
|
updateLinkEditor()
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', update);
|
window.addEventListener('resize', update)
|
||||||
|
|
||||||
if (scrollerElem) {
|
if (scrollerElem) {
|
||||||
scrollerElem.addEventListener('scroll', update);
|
scrollerElem.addEventListener('scroll', update)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', update);
|
window.removeEventListener('resize', update)
|
||||||
|
|
||||||
if (scrollerElem) {
|
if (scrollerElem) {
|
||||||
scrollerElem.removeEventListener('scroll', update);
|
scrollerElem.removeEventListener('scroll', update)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [anchorElem.parentElement, editor, updateLinkEditor]);
|
}, [anchorElem.parentElement, editor, updateLinkEditor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return mergeRegister(
|
return mergeRegister(
|
||||||
editor.registerUpdateListener(({editorState}) => {
|
editor.registerUpdateListener(({ editorState }) => {
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
updateLinkEditor();
|
updateLinkEditor()
|
||||||
});
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
() => {
|
() => {
|
||||||
updateLinkEditor();
|
updateLinkEditor()
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}, [editor, updateLinkEditor]);
|
}, [editor, updateLinkEditor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
updateLinkEditor();
|
updateLinkEditor()
|
||||||
});
|
})
|
||||||
}, [editor, updateLinkEditor]);
|
}, [editor, updateLinkEditor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditMode && inputRef.current) {
|
if (isEditMode && inputRef.current) {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus()
|
||||||
}
|
}
|
||||||
}, [isEditMode]);
|
}, [isEditMode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={editorRef} className="link-editor">
|
<div ref={editorRef} className="link-editor">
|
||||||
@@ -162,23 +154,20 @@ function FloatingLinkEditor({
|
|||||||
className="link-input"
|
className="link-input"
|
||||||
value={linkUrl}
|
value={linkUrl}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setLinkUrl(event.target.value);
|
setLinkUrl(event.target.value)
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
if (lastSelection !== null) {
|
if (lastSelection !== null) {
|
||||||
if (linkUrl !== '') {
|
if (linkUrl !== '') {
|
||||||
editor.dispatchCommand(
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(linkUrl))
|
||||||
TOGGLE_LINK_COMMAND,
|
|
||||||
sanitizeUrl(linkUrl),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
setEditMode(false);
|
setEditMode(false)
|
||||||
}
|
}
|
||||||
} else if (event.key === 'Escape') {
|
} else if (event.key === 'Escape') {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
setEditMode(false);
|
setEditMode(false)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -194,8 +183,9 @@ function FloatingLinkEditor({
|
|||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onMouseDown={(event) => event.preventDefault()}
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditMode(true);
|
setEditMode(true)
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<IconComponent size={15}>
|
<IconComponent size={15}>
|
||||||
<LexicalPencilFill />
|
<LexicalPencilFill />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
@@ -205,57 +195,49 @@ function FloatingLinkEditor({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFloatingLinkEditorToolbar(
|
function useFloatingLinkEditorToolbar(editor: LexicalEditor, anchorElem: HTMLElement): JSX.Element | null {
|
||||||
editor: LexicalEditor,
|
const [activeEditor, setActiveEditor] = useState(editor)
|
||||||
anchorElem: HTMLElement,
|
const [isLink, setIsLink] = useState(false)
|
||||||
): JSX.Element | null {
|
|
||||||
const [activeEditor, setActiveEditor] = useState(editor);
|
|
||||||
const [isLink, setIsLink] = useState(false);
|
|
||||||
|
|
||||||
const updateToolbar = useCallback(() => {
|
const updateToolbar = useCallback(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
if ($isRangeSelection(selection)) {
|
if ($isRangeSelection(selection)) {
|
||||||
const node = getSelectedNode(selection);
|
const node = getSelectedNode(selection)
|
||||||
const linkParent = $findMatchingParent(node, $isLinkNode);
|
const linkParent = $findMatchingParent(node, $isLinkNode)
|
||||||
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode);
|
const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode)
|
||||||
|
|
||||||
// We don't want this menu to open for auto links.
|
// We don't want this menu to open for auto links.
|
||||||
if (linkParent != null && autoLinkParent == null) {
|
if (linkParent != null && autoLinkParent == null) {
|
||||||
setIsLink(true);
|
setIsLink(true)
|
||||||
} else {
|
} else {
|
||||||
setIsLink(false);
|
setIsLink(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerCommand(
|
return editor.registerCommand(
|
||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
(_payload, newEditor) => {
|
(_payload, newEditor) => {
|
||||||
updateToolbar();
|
updateToolbar()
|
||||||
setActiveEditor(newEditor);
|
setActiveEditor(newEditor)
|
||||||
return false;
|
return false
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_CRITICAL,
|
COMMAND_PRIORITY_CRITICAL,
|
||||||
);
|
)
|
||||||
}, [editor, updateToolbar]);
|
}, [editor, updateToolbar])
|
||||||
|
|
||||||
return isLink
|
return isLink ? createPortal(<FloatingLinkEditor editor={activeEditor} anchorElem={anchorElem} />, anchorElem) : null
|
||||||
? createPortal(
|
|
||||||
<FloatingLinkEditor editor={activeEditor} anchorElem={anchorElem} />,
|
|
||||||
anchorElem,
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FloatingLinkEditorPlugin({
|
export default function FloatingLinkEditorPlugin({
|
||||||
anchorElem = document.body,
|
anchorElem = document.body,
|
||||||
}: {
|
}: {
|
||||||
anchorElem?: HTMLElement;
|
anchorElem?: HTMLElement
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
return useFloatingLinkEditorToolbar(editor, anchorElem);
|
return useFloatingLinkEditorToolbar(editor, anchorElem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,12 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './index.css';
|
import './index.css'
|
||||||
|
|
||||||
import {$isCodeHighlightNode} from '@lexical/code';
|
import { $isCodeHighlightNode } from '@lexical/code'
|
||||||
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {
|
import { mergeRegister, $findMatchingParent, $getNearestNodeOfType } from '@lexical/utils'
|
||||||
mergeRegister,
|
|
||||||
$findMatchingParent,
|
|
||||||
$getNearestNodeOfType,
|
|
||||||
} from '@lexical/utils';
|
|
||||||
import {
|
import {
|
||||||
$getSelection,
|
$getSelection,
|
||||||
$isRangeSelection,
|
$isRangeSelection,
|
||||||
@@ -26,21 +22,21 @@ import {
|
|||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
$isRootOrShadowRoot,
|
$isRootOrShadowRoot,
|
||||||
COMMAND_PRIORITY_CRITICAL,
|
COMMAND_PRIORITY_CRITICAL,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
import {$isHeadingNode} from '@lexical/rich-text';
|
import { $isHeadingNode } from '@lexical/rich-text'
|
||||||
import {
|
import {
|
||||||
INSERT_UNORDERED_LIST_COMMAND,
|
INSERT_UNORDERED_LIST_COMMAND,
|
||||||
REMOVE_LIST_COMMAND,
|
REMOVE_LIST_COMMAND,
|
||||||
$isListNode,
|
$isListNode,
|
||||||
ListNode,
|
ListNode,
|
||||||
INSERT_ORDERED_LIST_COMMAND,
|
INSERT_ORDERED_LIST_COMMAND,
|
||||||
} from '@lexical/list';
|
} from '@lexical/list'
|
||||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import {createPortal} from 'react-dom';
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
import {getDOMRangeRect} from '../../Utils/getDOMRangeRect';
|
import { getDOMRangeRect } from '../../Utils/getDOMRangeRect'
|
||||||
import {getSelectedNode} from '../../Utils/getSelectedNode';
|
import { getSelectedNode } from '../../Utils/getSelectedNode'
|
||||||
import {setFloatingElemPosition} from '../../Utils/setFloatingElemPosition';
|
import { setFloatingElemPosition } from '../../Utils/setFloatingElemPosition'
|
||||||
import {
|
import {
|
||||||
BoldIcon,
|
BoldIcon,
|
||||||
ItalicIcon,
|
ItalicIcon,
|
||||||
@@ -52,9 +48,9 @@ import {
|
|||||||
SubscriptIcon,
|
SubscriptIcon,
|
||||||
ListBulleted,
|
ListBulleted,
|
||||||
ListNumbered,
|
ListNumbered,
|
||||||
} from '@standardnotes/icons';
|
} from '@standardnotes/icons'
|
||||||
import {IconComponent} from '../../Theme/IconComponent';
|
import { IconComponent } from '../../Theme/IconComponent'
|
||||||
import {sanitizeUrl} from '../../Utils/sanitizeUrl';
|
import { sanitizeUrl } from '../../Utils/sanitizeUrl'
|
||||||
|
|
||||||
const blockTypeToBlockName = {
|
const blockTypeToBlockName = {
|
||||||
bullet: 'Bulleted List',
|
bullet: 'Bulleted List',
|
||||||
@@ -69,9 +65,9 @@ const blockTypeToBlockName = {
|
|||||||
number: 'Numbered List',
|
number: 'Numbered List',
|
||||||
paragraph: 'Normal',
|
paragraph: 'Normal',
|
||||||
quote: 'Quote',
|
quote: 'Quote',
|
||||||
};
|
}
|
||||||
|
|
||||||
const IconSize = 15;
|
const IconSize = 15
|
||||||
|
|
||||||
function TextFormatFloatingToolbar({
|
function TextFormatFloatingToolbar({
|
||||||
editor,
|
editor,
|
||||||
@@ -87,64 +83,64 @@ function TextFormatFloatingToolbar({
|
|||||||
isBulletedList,
|
isBulletedList,
|
||||||
isNumberedList,
|
isNumberedList,
|
||||||
}: {
|
}: {
|
||||||
editor: LexicalEditor;
|
editor: LexicalEditor
|
||||||
anchorElem: HTMLElement;
|
anchorElem: HTMLElement
|
||||||
isBold: boolean;
|
isBold: boolean
|
||||||
isCode: boolean;
|
isCode: boolean
|
||||||
isItalic: boolean;
|
isItalic: boolean
|
||||||
isLink: boolean;
|
isLink: boolean
|
||||||
isStrikethrough: boolean;
|
isStrikethrough: boolean
|
||||||
isSubscript: boolean;
|
isSubscript: boolean
|
||||||
isSuperscript: boolean;
|
isSuperscript: boolean
|
||||||
isUnderline: boolean;
|
isUnderline: boolean
|
||||||
isBulletedList: boolean;
|
isBulletedList: boolean
|
||||||
isNumberedList: boolean;
|
isNumberedList: boolean
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
const insertLink = useCallback(() => {
|
const insertLink = useCallback(() => {
|
||||||
if (!isLink) {
|
if (!isLink) {
|
||||||
editor.update(() => {
|
editor.update(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
const textContent = selection?.getTextContent();
|
const textContent = selection?.getTextContent()
|
||||||
if (!textContent) {
|
if (!textContent) {
|
||||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(textContent));
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(textContent))
|
||||||
});
|
})
|
||||||
} else {
|
} else {
|
||||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null)
|
||||||
}
|
}
|
||||||
}, [editor, isLink]);
|
}, [editor, isLink])
|
||||||
|
|
||||||
const formatBulletList = useCallback(() => {
|
const formatBulletList = useCallback(() => {
|
||||||
if (!isBulletedList) {
|
if (!isBulletedList) {
|
||||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
|
||||||
} else {
|
} else {
|
||||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined)
|
||||||
}
|
}
|
||||||
}, [isBulletedList]);
|
}, [editor, isBulletedList])
|
||||||
|
|
||||||
const formatNumberedList = useCallback(() => {
|
const formatNumberedList = useCallback(() => {
|
||||||
if (!isNumberedList) {
|
if (!isNumberedList) {
|
||||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined)
|
||||||
} else {
|
} else {
|
||||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined)
|
||||||
}
|
}
|
||||||
}, [isNumberedList]);
|
}, [editor, isNumberedList])
|
||||||
|
|
||||||
const updateTextFormatFloatingToolbar = useCallback(() => {
|
const updateTextFormatFloatingToolbar = useCallback(() => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
|
|
||||||
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
|
const popupCharStylesEditorElem = popupCharStylesEditorRef.current
|
||||||
const nativeSelection = window.getSelection();
|
const nativeSelection = window.getSelection()
|
||||||
|
|
||||||
if (popupCharStylesEditorElem === null) {
|
if (popupCharStylesEditorElem === null) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootElement = editor.getRootElement();
|
const rootElement = editor.getRootElement()
|
||||||
if (
|
if (
|
||||||
selection !== null &&
|
selection !== null &&
|
||||||
nativeSelection !== null &&
|
nativeSelection !== null &&
|
||||||
@@ -152,55 +148,55 @@ function TextFormatFloatingToolbar({
|
|||||||
rootElement !== null &&
|
rootElement !== null &&
|
||||||
rootElement.contains(nativeSelection.anchorNode)
|
rootElement.contains(nativeSelection.anchorNode)
|
||||||
) {
|
) {
|
||||||
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
|
const rangeRect = getDOMRangeRect(nativeSelection, rootElement)
|
||||||
|
|
||||||
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem);
|
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem)
|
||||||
}
|
}
|
||||||
}, [editor, anchorElem]);
|
}, [editor, anchorElem])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const scrollerElem = anchorElem.parentElement;
|
const scrollerElem = anchorElem.parentElement
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
updateTextFormatFloatingToolbar();
|
updateTextFormatFloatingToolbar()
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', update);
|
window.addEventListener('resize', update)
|
||||||
if (scrollerElem) {
|
if (scrollerElem) {
|
||||||
scrollerElem.addEventListener('scroll', update);
|
scrollerElem.addEventListener('scroll', update)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', update);
|
window.removeEventListener('resize', update)
|
||||||
if (scrollerElem) {
|
if (scrollerElem) {
|
||||||
scrollerElem.removeEventListener('scroll', update);
|
scrollerElem.removeEventListener('scroll', update)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [editor, updateTextFormatFloatingToolbar, anchorElem]);
|
}, [editor, updateTextFormatFloatingToolbar, anchorElem])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
updateTextFormatFloatingToolbar();
|
updateTextFormatFloatingToolbar()
|
||||||
});
|
})
|
||||||
return mergeRegister(
|
return mergeRegister(
|
||||||
editor.registerUpdateListener(({editorState}) => {
|
editor.registerUpdateListener(({ editorState }) => {
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
updateTextFormatFloatingToolbar();
|
updateTextFormatFloatingToolbar()
|
||||||
});
|
})
|
||||||
}),
|
}),
|
||||||
|
|
||||||
editor.registerCommand(
|
editor.registerCommand(
|
||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
() => {
|
() => {
|
||||||
updateTextFormatFloatingToolbar();
|
updateTextFormatFloatingToolbar()
|
||||||
return false;
|
return false
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_LOW,
|
COMMAND_PRIORITY_LOW,
|
||||||
),
|
),
|
||||||
);
|
)
|
||||||
}, [editor, updateTextFormatFloatingToolbar]);
|
}, [editor, updateTextFormatFloatingToolbar])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup">
|
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup">
|
||||||
@@ -208,72 +204,79 @@ function TextFormatFloatingToolbar({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
|
||||||
}}
|
}}
|
||||||
className={'popup-item spaced ' + (isBold ? 'active' : '')}
|
className={'popup-item spaced ' + (isBold ? 'active' : '')}
|
||||||
aria-label="Format text as bold">
|
aria-label="Format text as bold"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize}>
|
<IconComponent size={IconSize}>
|
||||||
<BoldIcon />
|
<BoldIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
|
||||||
}}
|
}}
|
||||||
className={'popup-item spaced ' + (isItalic ? 'active' : '')}
|
className={'popup-item spaced ' + (isItalic ? 'active' : '')}
|
||||||
aria-label="Format text as italics">
|
aria-label="Format text as italics"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize}>
|
<IconComponent size={IconSize}>
|
||||||
<ItalicIcon />
|
<ItalicIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')
|
||||||
}}
|
}}
|
||||||
className={'popup-item spaced ' + (isUnderline ? 'active' : '')}
|
className={'popup-item spaced ' + (isUnderline ? 'active' : '')}
|
||||||
aria-label="Format text to underlined">
|
aria-label="Format text to underlined"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize + 1}>
|
<IconComponent size={IconSize + 1}>
|
||||||
<UnderlineIcon />
|
<UnderlineIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough')
|
||||||
}}
|
}}
|
||||||
className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')}
|
className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')}
|
||||||
aria-label="Format text with a strikethrough">
|
aria-label="Format text with a strikethrough"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize}>
|
<IconComponent size={IconSize}>
|
||||||
<StrikethroughIcon />
|
<StrikethroughIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript')
|
||||||
}}
|
}}
|
||||||
className={'popup-item spaced ' + (isSubscript ? 'active' : '')}
|
className={'popup-item spaced ' + (isSubscript ? 'active' : '')}
|
||||||
title="Subscript"
|
title="Subscript"
|
||||||
aria-label="Format Subscript">
|
aria-label="Format Subscript"
|
||||||
|
>
|
||||||
<IconComponent paddingTop={4} size={IconSize - 2}>
|
<IconComponent paddingTop={4} size={IconSize - 2}>
|
||||||
<SubscriptIcon />
|
<SubscriptIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript')
|
||||||
}}
|
}}
|
||||||
className={'popup-item spaced ' + (isSuperscript ? 'active' : '')}
|
className={'popup-item spaced ' + (isSuperscript ? 'active' : '')}
|
||||||
title="Superscript"
|
title="Superscript"
|
||||||
aria-label="Format Superscript">
|
aria-label="Format Superscript"
|
||||||
|
>
|
||||||
<IconComponent paddingTop={1} size={IconSize - 2}>
|
<IconComponent paddingTop={1} size={IconSize - 2}>
|
||||||
<SuperscriptIcon />
|
<SuperscriptIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code')
|
||||||
}}
|
}}
|
||||||
className={'popup-item spaced ' + (isCode ? 'active' : '')}
|
className={'popup-item spaced ' + (isCode ? 'active' : '')}
|
||||||
aria-label="Insert code block">
|
aria-label="Insert code block"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize}>
|
<IconComponent size={IconSize}>
|
||||||
<CodeIcon />
|
<CodeIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
@@ -281,7 +284,8 @@ function TextFormatFloatingToolbar({
|
|||||||
<button
|
<button
|
||||||
onClick={insertLink}
|
onClick={insertLink}
|
||||||
className={'popup-item spaced ' + (isLink ? 'active' : '')}
|
className={'popup-item spaced ' + (isLink ? 'active' : '')}
|
||||||
aria-label="Insert link">
|
aria-label="Insert link"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize}>
|
<IconComponent size={IconSize}>
|
||||||
<LinkIcon />
|
<LinkIcon />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
@@ -289,7 +293,8 @@ function TextFormatFloatingToolbar({
|
|||||||
<button
|
<button
|
||||||
onClick={formatBulletList}
|
onClick={formatBulletList}
|
||||||
className={'popup-item spaced ' + (isBulletedList ? 'active' : '')}
|
className={'popup-item spaced ' + (isBulletedList ? 'active' : '')}
|
||||||
aria-label="Insert bulleted list">
|
aria-label="Insert bulleted list"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize}>
|
<IconComponent size={IconSize}>
|
||||||
<ListBulleted />
|
<ListBulleted />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
@@ -297,7 +302,8 @@ function TextFormatFloatingToolbar({
|
|||||||
<button
|
<button
|
||||||
onClick={formatNumberedList}
|
onClick={formatNumberedList}
|
||||||
className={'popup-item spaced ' + (isNumberedList ? 'active' : '')}
|
className={'popup-item spaced ' + (isNumberedList ? 'active' : '')}
|
||||||
aria-label="Insert numbered list">
|
aria-label="Insert numbered list"
|
||||||
|
>
|
||||||
<IconComponent size={IconSize}>
|
<IconComponent size={IconSize}>
|
||||||
<ListNumbered />
|
<ListNumbered />
|
||||||
</IconComponent>
|
</IconComponent>
|
||||||
@@ -305,143 +311,127 @@ function TextFormatFloatingToolbar({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function useFloatingTextFormatToolbar(
|
function useFloatingTextFormatToolbar(editor: LexicalEditor, anchorElem: HTMLElement): JSX.Element | null {
|
||||||
editor: LexicalEditor,
|
const [activeEditor, setActiveEditor] = useState(editor)
|
||||||
anchorElem: HTMLElement,
|
const [isText, setIsText] = useState(false)
|
||||||
): JSX.Element | null {
|
const [isLink, setIsLink] = useState(false)
|
||||||
const [activeEditor, setActiveEditor] = useState(editor);
|
const [isBold, setIsBold] = useState(false)
|
||||||
const [isText, setIsText] = useState(false);
|
const [isItalic, setIsItalic] = useState(false)
|
||||||
const [isLink, setIsLink] = useState(false);
|
const [isUnderline, setIsUnderline] = useState(false)
|
||||||
const [isBold, setIsBold] = useState(false);
|
const [isStrikethrough, setIsStrikethrough] = useState(false)
|
||||||
const [isItalic, setIsItalic] = useState(false);
|
const [isSubscript, setIsSubscript] = useState(false)
|
||||||
const [isUnderline, setIsUnderline] = useState(false);
|
const [isSuperscript, setIsSuperscript] = useState(false)
|
||||||
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
const [isCode, setIsCode] = useState(false)
|
||||||
const [isSubscript, setIsSubscript] = useState(false);
|
const [blockType, setBlockType] = useState<keyof typeof blockTypeToBlockName>('paragraph')
|
||||||
const [isSuperscript, setIsSuperscript] = useState(false);
|
|
||||||
const [isCode, setIsCode] = useState(false);
|
|
||||||
const [blockType, setBlockType] =
|
|
||||||
useState<keyof typeof blockTypeToBlockName>('paragraph');
|
|
||||||
|
|
||||||
const updatePopup = useCallback(() => {
|
const updatePopup = useCallback(() => {
|
||||||
editor.getEditorState().read(() => {
|
editor.getEditorState().read(() => {
|
||||||
// Should not to pop up the floating toolbar when using IME input
|
// Should not to pop up the floating toolbar when using IME input
|
||||||
if (editor.isComposing()) {
|
if (editor.isComposing()) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
const nativeSelection = window.getSelection();
|
const nativeSelection = window.getSelection()
|
||||||
const rootElement = editor.getRootElement();
|
const rootElement = editor.getRootElement()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nativeSelection !== null &&
|
nativeSelection !== null &&
|
||||||
(!$isRangeSelection(selection) ||
|
(!$isRangeSelection(selection) || rootElement === null || !rootElement.contains(nativeSelection.anchorNode))
|
||||||
rootElement === null ||
|
|
||||||
!rootElement.contains(nativeSelection.anchorNode))
|
|
||||||
) {
|
) {
|
||||||
setIsText(false);
|
setIsText(false)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const anchorNode = selection.anchor.getNode();
|
const anchorNode = selection.anchor.getNode()
|
||||||
let element =
|
let element =
|
||||||
anchorNode.getKey() === 'root'
|
anchorNode.getKey() === 'root'
|
||||||
? anchorNode
|
? anchorNode
|
||||||
: $findMatchingParent(anchorNode, (e) => {
|
: $findMatchingParent(anchorNode, (e) => {
|
||||||
const parent = e.getParent();
|
const parent = e.getParent()
|
||||||
return parent !== null && $isRootOrShadowRoot(parent);
|
return parent !== null && $isRootOrShadowRoot(parent)
|
||||||
});
|
})
|
||||||
|
|
||||||
if (element === null) {
|
if (element === null) {
|
||||||
element = anchorNode.getTopLevelElementOrThrow();
|
element = anchorNode.getTopLevelElementOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementKey = element.getKey();
|
const elementKey = element.getKey()
|
||||||
const elementDOM = activeEditor.getElementByKey(elementKey);
|
const elementDOM = activeEditor.getElementByKey(elementKey)
|
||||||
|
|
||||||
if (elementDOM !== null) {
|
if (elementDOM !== null) {
|
||||||
if ($isListNode(element)) {
|
if ($isListNode(element)) {
|
||||||
const parentList = $getNearestNodeOfType<ListNode>(
|
const parentList = $getNearestNodeOfType<ListNode>(anchorNode, ListNode)
|
||||||
anchorNode,
|
const type = parentList ? parentList.getListType() : element.getListType()
|
||||||
ListNode,
|
setBlockType(type)
|
||||||
);
|
|
||||||
const type = parentList
|
|
||||||
? parentList.getListType()
|
|
||||||
: element.getListType();
|
|
||||||
setBlockType(type);
|
|
||||||
} else {
|
} else {
|
||||||
const type = $isHeadingNode(element)
|
const type = $isHeadingNode(element) ? element.getTag() : element.getType()
|
||||||
? element.getTag()
|
|
||||||
: element.getType();
|
|
||||||
if (type in blockTypeToBlockName) {
|
if (type in blockTypeToBlockName) {
|
||||||
setBlockType(type as keyof typeof blockTypeToBlockName);
|
setBlockType(type as keyof typeof blockTypeToBlockName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = getSelectedNode(selection);
|
const node = getSelectedNode(selection)
|
||||||
|
|
||||||
// Update text format
|
// Update text format
|
||||||
setIsBold(selection.hasFormat('bold'));
|
setIsBold(selection.hasFormat('bold'))
|
||||||
setIsItalic(selection.hasFormat('italic'));
|
setIsItalic(selection.hasFormat('italic'))
|
||||||
setIsUnderline(selection.hasFormat('underline'));
|
setIsUnderline(selection.hasFormat('underline'))
|
||||||
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
setIsStrikethrough(selection.hasFormat('strikethrough'))
|
||||||
setIsSubscript(selection.hasFormat('subscript'));
|
setIsSubscript(selection.hasFormat('subscript'))
|
||||||
setIsSuperscript(selection.hasFormat('superscript'));
|
setIsSuperscript(selection.hasFormat('superscript'))
|
||||||
setIsCode(selection.hasFormat('code'));
|
setIsCode(selection.hasFormat('code'))
|
||||||
|
|
||||||
// Update links
|
// Update links
|
||||||
const parent = node.getParent();
|
const parent = node.getParent()
|
||||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||||
setIsLink(true);
|
setIsLink(true)
|
||||||
} else {
|
} else {
|
||||||
setIsLink(false);
|
setIsLink(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!$isCodeHighlightNode(selection.anchor.getNode()) && selection.getTextContent() !== '') {
|
||||||
!$isCodeHighlightNode(selection.anchor.getNode()) &&
|
setIsText($isTextNode(node))
|
||||||
selection.getTextContent() !== ''
|
|
||||||
) {
|
|
||||||
setIsText($isTextNode(node));
|
|
||||||
} else {
|
} else {
|
||||||
setIsText(false);
|
setIsText(false)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}, [editor, activeEditor]);
|
}, [editor, activeEditor])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerCommand(
|
return editor.registerCommand(
|
||||||
SELECTION_CHANGE_COMMAND,
|
SELECTION_CHANGE_COMMAND,
|
||||||
(_payload, newEditor) => {
|
(_payload, newEditor) => {
|
||||||
setActiveEditor(newEditor);
|
setActiveEditor(newEditor)
|
||||||
updatePopup();
|
updatePopup()
|
||||||
return false;
|
return false
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_CRITICAL,
|
COMMAND_PRIORITY_CRITICAL,
|
||||||
);
|
)
|
||||||
}, [editor, updatePopup]);
|
}, [editor, updatePopup])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return mergeRegister(
|
return mergeRegister(
|
||||||
editor.registerUpdateListener(() => {
|
editor.registerUpdateListener(() => {
|
||||||
updatePopup();
|
updatePopup()
|
||||||
}),
|
}),
|
||||||
editor.registerRootListener(() => {
|
editor.registerRootListener(() => {
|
||||||
if (editor.getRootElement() === null) {
|
if (editor.getRootElement() === null) {
|
||||||
setIsText(false);
|
setIsText(false)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
)
|
||||||
}, [editor, updatePopup]);
|
}, [editor, updatePopup])
|
||||||
|
|
||||||
if (!isText || isLink) {
|
if (!isText || isLink) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return createPortal(
|
return createPortal(
|
||||||
@@ -460,14 +450,14 @@ function useFloatingTextFormatToolbar(
|
|||||||
isNumberedList={blockType === 'number'}
|
isNumberedList={blockType === 'number'}
|
||||||
/>,
|
/>,
|
||||||
anchorElem,
|
anchorElem,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function FloatingTextFormatToolbarPlugin({
|
export default function FloatingTextFormatToolbarPlugin({
|
||||||
anchorElem = document.body,
|
anchorElem = document.body,
|
||||||
}: {
|
}: {
|
||||||
anchorElem?: HTMLElement;
|
anchorElem?: HTMLElement
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
return useFloatingTextFormatToolbar(editor, anchorElem);
|
return useFloatingTextFormatToolbar(editor, anchorElem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,46 +6,36 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {
|
import { $createHorizontalRuleNode, INSERT_HORIZONTAL_RULE_COMMAND } from '@lexical/react/LexicalHorizontalRuleNode'
|
||||||
$createHorizontalRuleNode,
|
import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR } from 'lexical'
|
||||||
INSERT_HORIZONTAL_RULE_COMMAND,
|
import { useEffect } from 'react'
|
||||||
} from '@lexical/react/LexicalHorizontalRuleNode';
|
|
||||||
import {
|
|
||||||
$getSelection,
|
|
||||||
$isRangeSelection,
|
|
||||||
COMMAND_PRIORITY_EDITOR,
|
|
||||||
} from 'lexical';
|
|
||||||
import {useEffect} from 'react';
|
|
||||||
|
|
||||||
export default function HorizontalRulePlugin(): null {
|
export default function HorizontalRulePlugin(): null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerCommand(
|
return editor.registerCommand(
|
||||||
INSERT_HORIZONTAL_RULE_COMMAND,
|
INSERT_HORIZONTAL_RULE_COMMAND,
|
||||||
(type) => {
|
(_type) => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
|
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const focusNode = selection.focus.getNode();
|
const focusNode = selection.focus.getNode()
|
||||||
|
|
||||||
if (focusNode !== null) {
|
if (focusNode !== null) {
|
||||||
const horizontalRuleNode = $createHorizontalRuleNode();
|
const horizontalRuleNode = $createHorizontalRuleNode()
|
||||||
selection.focus
|
selection.focus.getNode().getTopLevelElementOrThrow().insertBefore(horizontalRuleNode)
|
||||||
.getNode()
|
|
||||||
.getTopLevelElementOrThrow()
|
|
||||||
.insertBefore(horizontalRuleNode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
);
|
)
|
||||||
}, [editor]);
|
}, [editor])
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {$getListDepth, $isListItemNode, $isListNode} from '@lexical/list';
|
import { $getListDepth, $isListItemNode, $isListNode } from '@lexical/list'
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {
|
import {
|
||||||
RangeSelection,
|
RangeSelection,
|
||||||
$getSelection,
|
$getSelection,
|
||||||
@@ -8,70 +8,60 @@ import {
|
|||||||
COMMAND_PRIORITY_CRITICAL,
|
COMMAND_PRIORITY_CRITICAL,
|
||||||
ElementNode,
|
ElementNode,
|
||||||
INDENT_CONTENT_COMMAND,
|
INDENT_CONTENT_COMMAND,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
import {useEffect} from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
maxDepth: number | null | undefined;
|
maxDepth: number | null | undefined
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
function getElementNodesInSelection(
|
function getElementNodesInSelection(selection: RangeSelection): Set<ElementNode> {
|
||||||
selection: RangeSelection,
|
const nodesInSelection = selection.getNodes()
|
||||||
): Set<ElementNode> {
|
|
||||||
const nodesInSelection = selection.getNodes();
|
|
||||||
|
|
||||||
if (nodesInSelection.length === 0) {
|
if (nodesInSelection.length === 0) {
|
||||||
return new Set([
|
return new Set([selection.anchor.getNode().getParentOrThrow(), selection.focus.getNode().getParentOrThrow()])
|
||||||
selection.anchor.getNode().getParentOrThrow(),
|
|
||||||
selection.focus.getNode().getParentOrThrow(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Set(
|
return new Set(nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow())))
|
||||||
nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow())),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIndentPermitted(maxDepth: number): boolean {
|
function isIndentPermitted(maxDepth: number): boolean {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
|
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const elementNodesInSelection: Set<ElementNode> =
|
const elementNodesInSelection: Set<ElementNode> = getElementNodesInSelection(selection)
|
||||||
getElementNodesInSelection(selection);
|
|
||||||
|
|
||||||
let totalDepth = 0;
|
let totalDepth = 0
|
||||||
|
|
||||||
for (const elementNode of elementNodesInSelection) {
|
for (const elementNode of elementNodesInSelection) {
|
||||||
if ($isListNode(elementNode)) {
|
if ($isListNode(elementNode)) {
|
||||||
totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth);
|
totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth)
|
||||||
} else if ($isListItemNode(elementNode)) {
|
} else if ($isListItemNode(elementNode)) {
|
||||||
const parent = elementNode.getParent();
|
const parent = elementNode.getParent()
|
||||||
|
|
||||||
if (!$isListNode(parent)) {
|
if (!$isListNode(parent)) {
|
||||||
throw new Error(
|
throw new Error('ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent.')
|
||||||
'ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
totalDepth = Math.max($getListDepth(parent) + 1, totalDepth);
|
totalDepth = Math.max($getListDepth(parent) + 1, totalDepth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return totalDepth <= maxDepth;
|
return totalDepth <= maxDepth
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ListMaxIndentLevelPlugin({maxDepth}: Props): null {
|
export default function ListMaxIndentLevelPlugin({ maxDepth }: Props): null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerCommand(
|
return editor.registerCommand(
|
||||||
INDENT_CONTENT_COMMAND,
|
INDENT_CONTENT_COMMAND,
|
||||||
() => !isIndentPermitted(maxDepth ?? 7),
|
() => !isIndentPermitted(maxDepth ?? 7),
|
||||||
COMMAND_PRIORITY_CRITICAL,
|
COMMAND_PRIORITY_CRITICAL,
|
||||||
);
|
)
|
||||||
}, [editor, maxDepth]);
|
}, [editor, maxDepth])
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {
|
import {
|
||||||
$getSelection,
|
$getSelection,
|
||||||
$isRangeSelection,
|
$isRangeSelection,
|
||||||
@@ -14,8 +14,8 @@ import {
|
|||||||
INDENT_CONTENT_COMMAND,
|
INDENT_CONTENT_COMMAND,
|
||||||
KEY_TAB_COMMAND,
|
KEY_TAB_COMMAND,
|
||||||
OUTDENT_CONTENT_COMMAND,
|
OUTDENT_CONTENT_COMMAND,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
import {useEffect} from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugin adds the ability to indent content using the tab key. Generally, we don't
|
* This plugin adds the ability to indent content using the tab key. Generally, we don't
|
||||||
@@ -23,28 +23,25 @@ import {useEffect} from 'react';
|
|||||||
* users, causing focus to become trapped within the editor.
|
* users, causing focus to become trapped within the editor.
|
||||||
*/
|
*/
|
||||||
export function TabIndentationPlugin(): null {
|
export function TabIndentationPlugin(): null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return editor.registerCommand<KeyboardEvent>(
|
return editor.registerCommand<KeyboardEvent>(
|
||||||
KEY_TAB_COMMAND,
|
KEY_TAB_COMMAND,
|
||||||
(event) => {
|
(event) => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
|
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
|
|
||||||
return editor.dispatchCommand(
|
return editor.dispatchCommand(event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND, undefined)
|
||||||
event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
);
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {INSERT_TABLE_COMMAND} from '@lexical/table';
|
import { INSERT_TABLE_COMMAND } from '@lexical/table'
|
||||||
import {
|
import {
|
||||||
$createNodeSelection,
|
$createNodeSelection,
|
||||||
$createParagraphNode,
|
$createParagraphNode,
|
||||||
@@ -22,41 +22,38 @@ import {
|
|||||||
LexicalCommand,
|
LexicalCommand,
|
||||||
LexicalEditor,
|
LexicalEditor,
|
||||||
LexicalNode,
|
LexicalNode,
|
||||||
} from 'lexical';
|
} from 'lexical'
|
||||||
import {createContext, useContext, useEffect, useMemo, useState} from 'react';
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||||
import * as React from 'react';
|
import * as React from 'react'
|
||||||
import invariant from '../Shared/invariant';
|
import invariant from '../Shared/invariant'
|
||||||
|
|
||||||
import {$createTableNodeWithDimensions, TableNode} from '../Nodes/TableNode';
|
import { $createTableNodeWithDimensions, TableNode } from '../Nodes/TableNode'
|
||||||
import Button from '../UI/Button';
|
import Button from '../UI/Button'
|
||||||
import {DialogActions} from '../UI/Dialog';
|
import { DialogActions } from '../UI/Dialog'
|
||||||
import TextInput from '../UI/TextInput';
|
import TextInput from '../UI/TextInput'
|
||||||
|
|
||||||
export type InsertTableCommandPayload = Readonly<{
|
export type InsertTableCommandPayload = Readonly<{
|
||||||
columns: string;
|
columns: string
|
||||||
rows: string;
|
rows: string
|
||||||
includeHeaders?: boolean;
|
includeHeaders?: boolean
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
export type CellContextShape = {
|
export type CellContextShape = {
|
||||||
cellEditorConfig: null | CellEditorConfig;
|
cellEditorConfig: null | CellEditorConfig
|
||||||
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>;
|
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>
|
||||||
set: (
|
set: (cellEditorConfig: null | CellEditorConfig, cellEditorPlugins: null | JSX.Element | Array<JSX.Element>) => void
|
||||||
cellEditorConfig: null | CellEditorConfig,
|
}
|
||||||
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>,
|
|
||||||
) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CellEditorConfig = Readonly<{
|
export type CellEditorConfig = Readonly<{
|
||||||
namespace: string;
|
namespace: string
|
||||||
nodes?: ReadonlyArray<Klass<LexicalNode>>;
|
nodes?: ReadonlyArray<Klass<LexicalNode>>
|
||||||
onError: (error: Error, editor: LexicalEditor) => void;
|
onError: (error: Error, editor: LexicalEditor) => void
|
||||||
readOnly?: boolean;
|
readOnly?: boolean
|
||||||
theme?: EditorThemeClasses;
|
theme?: EditorThemeClasses
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
export const INSERT_NEW_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =
|
export const INSERT_NEW_TABLE_COMMAND: LexicalCommand<InsertTableCommandPayload> =
|
||||||
createCommand('INSERT_NEW_TABLE_COMMAND');
|
createCommand('INSERT_NEW_TABLE_COMMAND')
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore: not sure why TS doesn't like using null as the value?
|
// @ts-ignore: not sure why TS doesn't like using null as the value?
|
||||||
@@ -66,16 +63,16 @@ export const CellContext: React.Context<CellContextShape> = createContext({
|
|||||||
set: () => {
|
set: () => {
|
||||||
// Empty
|
// Empty
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
export function TableContext({children}: {children: JSX.Element}) {
|
export function TableContext({ children }: { children: JSX.Element }) {
|
||||||
const [contextValue, setContextValue] = useState<{
|
const [contextValue, setContextValue] = useState<{
|
||||||
cellEditorConfig: null | CellEditorConfig;
|
cellEditorConfig: null | CellEditorConfig
|
||||||
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>;
|
cellEditorPlugins: null | JSX.Element | Array<JSX.Element>
|
||||||
}>({
|
}>({
|
||||||
cellEditorConfig: null,
|
cellEditorConfig: null,
|
||||||
cellEditorPlugins: null,
|
cellEditorPlugins: null,
|
||||||
});
|
})
|
||||||
return (
|
return (
|
||||||
<CellContext.Provider
|
<CellContext.Provider
|
||||||
value={useMemo(
|
value={useMemo(
|
||||||
@@ -83,30 +80,31 @@ export function TableContext({children}: {children: JSX.Element}) {
|
|||||||
cellEditorConfig: contextValue.cellEditorConfig,
|
cellEditorConfig: contextValue.cellEditorConfig,
|
||||||
cellEditorPlugins: contextValue.cellEditorPlugins,
|
cellEditorPlugins: contextValue.cellEditorPlugins,
|
||||||
set: (cellEditorConfig, cellEditorPlugins) => {
|
set: (cellEditorConfig, cellEditorPlugins) => {
|
||||||
setContextValue({cellEditorConfig, cellEditorPlugins});
|
setContextValue({ cellEditorConfig, cellEditorPlugins })
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[contextValue.cellEditorConfig, contextValue.cellEditorPlugins],
|
[contextValue.cellEditorConfig, contextValue.cellEditorPlugins],
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</CellContext.Provider>
|
</CellContext.Provider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InsertTableDialog({
|
export function InsertTableDialog({
|
||||||
activeEditor,
|
activeEditor,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
activeEditor: LexicalEditor;
|
activeEditor: LexicalEditor
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [rows, setRows] = useState('5');
|
const [rows, setRows] = useState('5')
|
||||||
const [columns, setColumns] = useState('5');
|
const [columns, setColumns] = useState('5')
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
activeEditor.dispatchCommand(INSERT_TABLE_COMMAND, {columns, rows});
|
activeEditor.dispatchCommand(INSERT_TABLE_COMMAND, { columns, rows })
|
||||||
onClose();
|
onClose()
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -116,23 +114,23 @@ export function InsertTableDialog({
|
|||||||
<Button onClick={onClick}>Confirm</Button>
|
<Button onClick={onClick}>Confirm</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InsertNewTableDialog({
|
export function InsertNewTableDialog({
|
||||||
activeEditor,
|
activeEditor,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
activeEditor: LexicalEditor;
|
activeEditor: LexicalEditor
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [rows, setRows] = useState('5');
|
const [rows, setRows] = useState('5')
|
||||||
const [columns, setColumns] = useState('5');
|
const [columns, setColumns] = useState('5')
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
activeEditor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, {columns, rows});
|
activeEditor.dispatchCommand(INSERT_NEW_TABLE_COMMAND, { columns, rows })
|
||||||
onClose();
|
onClose()
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -142,71 +140,67 @@ export function InsertNewTableDialog({
|
|||||||
<Button onClick={onClick}>Confirm</Button>
|
<Button onClick={onClick}>Confirm</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TablePlugin({
|
export function TablePlugin({
|
||||||
cellEditorConfig,
|
cellEditorConfig,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
cellEditorConfig: CellEditorConfig;
|
cellEditorConfig: CellEditorConfig
|
||||||
children: JSX.Element | Array<JSX.Element>;
|
children: JSX.Element | Array<JSX.Element>
|
||||||
}): JSX.Element | null {
|
}): JSX.Element | null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
const cellContext = useContext(CellContext);
|
const cellContext = useContext(CellContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor.hasNodes([TableNode])) {
|
if (!editor.hasNodes([TableNode])) {
|
||||||
invariant(false, 'TablePlugin: TableNode is not registered on editor');
|
invariant(false, 'TablePlugin: TableNode is not registered on editor')
|
||||||
}
|
}
|
||||||
|
|
||||||
cellContext.set(cellEditorConfig, children);
|
cellContext.set(cellEditorConfig, children)
|
||||||
|
|
||||||
return editor.registerCommand<InsertTableCommandPayload>(
|
return editor.registerCommand<InsertTableCommandPayload>(
|
||||||
INSERT_TABLE_COMMAND,
|
INSERT_TABLE_COMMAND,
|
||||||
({columns, rows, includeHeaders}) => {
|
({ columns, rows, includeHeaders }) => {
|
||||||
const selection = $getSelection();
|
const selection = $getSelection()
|
||||||
|
|
||||||
if (!$isRangeSelection(selection)) {
|
if (!$isRangeSelection(selection)) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const focus = selection.focus;
|
const focus = selection.focus
|
||||||
const focusNode = focus.getNode();
|
const focusNode = focus.getNode()
|
||||||
|
|
||||||
if (focusNode !== null) {
|
if (focusNode !== null) {
|
||||||
const tableNode = $createTableNodeWithDimensions(
|
const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders)
|
||||||
Number(rows),
|
|
||||||
Number(columns),
|
|
||||||
includeHeaders,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($isRootOrShadowRoot(focusNode)) {
|
if ($isRootOrShadowRoot(focusNode)) {
|
||||||
const target = focusNode.getChildAtIndex(focus.offset);
|
const target = focusNode.getChildAtIndex(focus.offset)
|
||||||
|
|
||||||
if (target !== null) {
|
if (target !== null) {
|
||||||
target.insertBefore(tableNode);
|
target.insertBefore(tableNode)
|
||||||
} else {
|
} else {
|
||||||
focusNode.append(tableNode);
|
focusNode.append(tableNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableNode.insertBefore($createParagraphNode());
|
tableNode.insertBefore($createParagraphNode())
|
||||||
} else {
|
} else {
|
||||||
const topLevelNode = focusNode.getTopLevelElementOrThrow();
|
const topLevelNode = focusNode.getTopLevelElementOrThrow()
|
||||||
topLevelNode.insertAfter(tableNode);
|
topLevelNode.insertAfter(tableNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
tableNode.insertAfter($createParagraphNode());
|
tableNode.insertAfter($createParagraphNode())
|
||||||
const nodeSelection = $createNodeSelection();
|
const nodeSelection = $createNodeSelection()
|
||||||
nodeSelection.add(tableNode.getKey());
|
nodeSelection.add(tableNode.getKey())
|
||||||
$setSelection(nodeSelection);
|
$setSelection(nodeSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
);
|
)
|
||||||
}, [cellContext, cellEditorConfig, children, editor]);
|
}, [cellContext, cellEditorConfig, children, editor])
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,36 +6,34 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {$insertNodeToNearestRoot} from '@lexical/utils';
|
import { $insertNodeToNearestRoot } from '@lexical/utils'
|
||||||
import {COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand} from 'lexical';
|
import { COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand } from 'lexical'
|
||||||
import {useEffect} from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import {$createTweetNode, TweetNode} from '../../Nodes/TweetNode';
|
import { $createTweetNode, TweetNode } from '../../Nodes/TweetNode'
|
||||||
|
|
||||||
export const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand(
|
export const INSERT_TWEET_COMMAND: LexicalCommand<string> = createCommand('INSERT_TWEET_COMMAND')
|
||||||
'INSERT_TWEET_COMMAND',
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function TwitterPlugin(): JSX.Element | null {
|
export default function TwitterPlugin(): JSX.Element | null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor.hasNodes([TweetNode])) {
|
if (!editor.hasNodes([TweetNode])) {
|
||||||
throw new Error('TwitterPlugin: TweetNode not registered on editor');
|
throw new Error('TwitterPlugin: TweetNode not registered on editor')
|
||||||
}
|
}
|
||||||
|
|
||||||
return editor.registerCommand<string>(
|
return editor.registerCommand<string>(
|
||||||
INSERT_TWEET_COMMAND,
|
INSERT_TWEET_COMMAND,
|
||||||
(payload) => {
|
(payload) => {
|
||||||
const tweetNode = $createTweetNode(payload);
|
const tweetNode = $createTweetNode(payload)
|
||||||
$insertNodeToNearestRoot(tweetNode);
|
$insertNodeToNearestRoot(tweetNode)
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
);
|
)
|
||||||
}, [editor]);
|
}, [editor])
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,36 +6,34 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
import {$insertNodeToNearestRoot} from '@lexical/utils';
|
import { $insertNodeToNearestRoot } from '@lexical/utils'
|
||||||
import {COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand} from 'lexical';
|
import { COMMAND_PRIORITY_EDITOR, createCommand, LexicalCommand } from 'lexical'
|
||||||
import {useEffect} from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import {$createYouTubeNode, YouTubeNode} from '../../Nodes/YouTubeNode';
|
import { $createYouTubeNode, YouTubeNode } from '../../Nodes/YouTubeNode'
|
||||||
|
|
||||||
export const INSERT_YOUTUBE_COMMAND: LexicalCommand<string> = createCommand(
|
export const INSERT_YOUTUBE_COMMAND: LexicalCommand<string> = createCommand('INSERT_YOUTUBE_COMMAND')
|
||||||
'INSERT_YOUTUBE_COMMAND',
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function YouTubePlugin(): JSX.Element | null {
|
export default function YouTubePlugin(): JSX.Element | null {
|
||||||
const [editor] = useLexicalComposerContext();
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor.hasNodes([YouTubeNode])) {
|
if (!editor.hasNodes([YouTubeNode])) {
|
||||||
throw new Error('YouTubePlugin: YouTubeNode not registered on editor');
|
throw new Error('YouTubePlugin: YouTubeNode not registered on editor')
|
||||||
}
|
}
|
||||||
|
|
||||||
return editor.registerCommand<string>(
|
return editor.registerCommand<string>(
|
||||||
INSERT_YOUTUBE_COMMAND,
|
INSERT_YOUTUBE_COMMAND,
|
||||||
(payload) => {
|
(payload) => {
|
||||||
const youTubeNode = $createYouTubeNode(payload);
|
const youTubeNode = $createYouTubeNode(payload)
|
||||||
$insertNodeToNearestRoot(youTubeNode);
|
$insertNodeToNearestRoot(youTubeNode)
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
},
|
},
|
||||||
COMMAND_PRIORITY_EDITOR,
|
COMMAND_PRIORITY_EDITOR,
|
||||||
);
|
)
|
||||||
}, [editor]);
|
}, [editor])
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,4 @@
|
|||||||
export const CAN_USE_DOM: boolean =
|
export const CAN_USE_DOM: boolean =
|
||||||
typeof window !== 'undefined' &&
|
typeof window !== 'undefined' &&
|
||||||
typeof window.document !== 'undefined' &&
|
typeof window.document !== 'undefined' &&
|
||||||
typeof window.document.createElement !== 'undefined';
|
typeof window.document.createElement !== 'undefined'
|
||||||
|
|||||||
@@ -6,39 +6,30 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {CAN_USE_DOM} from './canUseDOM';
|
import { CAN_USE_DOM } from './canUseDOM'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Document {
|
interface Document {
|
||||||
documentMode?: string;
|
documentMode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
MSStream?: unknown;
|
MSStream?: unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const documentMode =
|
const documentMode = CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null
|
||||||
CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
|
|
||||||
|
|
||||||
export const IS_APPLE: boolean =
|
export const IS_APPLE: boolean = CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform)
|
||||||
CAN_USE_DOM && /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
|
||||||
|
|
||||||
export const IS_FIREFOX: boolean =
|
export const IS_FIREFOX: boolean = CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent)
|
||||||
CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
|
|
||||||
|
|
||||||
export const CAN_USE_BEFORE_INPUT: boolean =
|
export const CAN_USE_BEFORE_INPUT: boolean =
|
||||||
CAN_USE_DOM && 'InputEvent' in window && !documentMode
|
CAN_USE_DOM && 'InputEvent' in window && !documentMode ? 'getTargetRanges' in new window.InputEvent('input') : false
|
||||||
? 'getTargetRanges' in new window.InputEvent('input')
|
|
||||||
: false;
|
|
||||||
|
|
||||||
export const IS_SAFARI: boolean =
|
export const IS_SAFARI: boolean = CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent)
|
||||||
CAN_USE_DOM && /Version\/[\d.]+.*Safari/.test(navigator.userAgent);
|
|
||||||
|
|
||||||
export const IS_IOS: boolean =
|
export const IS_IOS: boolean = CAN_USE_DOM && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream
|
||||||
CAN_USE_DOM &&
|
|
||||||
/iPad|iPhone|iPod/.test(navigator.userAgent) &&
|
|
||||||
!window.MSStream;
|
|
||||||
|
|
||||||
// Keep these in case we need to use them in the future.
|
// Keep these in case we need to use them in the future.
|
||||||
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
|
// export const IS_WINDOWS: boolean = CAN_USE_DOM && /Win/.test(navigator.platform);
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
// invariant(condition, message) will refine types based on "condition", and
|
// invariant(condition, message) will refine types based on "condition", and
|
||||||
// if "condition" is false will throw an error. This function is special-cased
|
// if "condition" is false will throw an error. This function is special-cased
|
||||||
// in flow itself, so we can't name it anything else.
|
// in flow itself, so we can't name it anything else.
|
||||||
export default function invariant(
|
export default function invariant(cond?: boolean, _message?: string, ..._args: string[]): asserts cond {
|
||||||
cond?: boolean,
|
|
||||||
message?: string,
|
|
||||||
...args: string[]
|
|
||||||
): asserts cond {
|
|
||||||
if (cond) {
|
if (cond) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Internal Lexical error: invariant() is meant to be replaced at compile ' +
|
'Internal Lexical error: invariant() is meant to be replaced at compile ' + 'time. There is no runtime version.',
|
||||||
'time. There is no runtime version.',
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ export const IconComponent = ({
|
|||||||
size = 20,
|
size = 20,
|
||||||
paddingTop = 0,
|
paddingTop = 0,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode
|
||||||
size?: number;
|
size?: number
|
||||||
paddingTop?: number;
|
paddingTop?: number
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<span className="svg-icon" style={{width: size, height: size, paddingTop}}>
|
<span className="svg-icon" style={{ width: size, height: size, paddingTop }}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type {EditorThemeClasses} from 'lexical';
|
import type { EditorThemeClasses } from 'lexical'
|
||||||
|
|
||||||
const BlocksEditorTheme: EditorThemeClasses = {
|
const BlocksEditorTheme: EditorThemeClasses = {
|
||||||
characterLimit: 'Lexical__characterLimit',
|
characterLimit: 'Lexical__characterLimit',
|
||||||
@@ -57,13 +57,7 @@ const BlocksEditorTheme: EditorThemeClasses = {
|
|||||||
nested: {
|
nested: {
|
||||||
listitem: 'Lexical__nestedListItem',
|
listitem: 'Lexical__nestedListItem',
|
||||||
},
|
},
|
||||||
olDepth: [
|
olDepth: ['Lexical__ol1', 'Lexical__ol2', 'Lexical__ol3', 'Lexical__ol4', 'Lexical__ol5'],
|
||||||
'Lexical__ol1',
|
|
||||||
'Lexical__ol2',
|
|
||||||
'Lexical__ol3',
|
|
||||||
'Lexical__ol4',
|
|
||||||
'Lexical__ol5',
|
|
||||||
],
|
|
||||||
ul: 'Lexical__ul',
|
ul: 'Lexical__ul',
|
||||||
},
|
},
|
||||||
ltr: 'Lexical__ltr',
|
ltr: 'Lexical__ltr',
|
||||||
@@ -96,6 +90,6 @@ const BlocksEditorTheme: EditorThemeClasses = {
|
|||||||
underline: 'Lexical__textUnderline',
|
underline: 'Lexical__textUnderline',
|
||||||
underlineStrikethrough: 'Lexical__textUnderlineStrikethrough',
|
underlineStrikethrough: 'Lexical__textUnderlineStrikethrough',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|
||||||
export default BlocksEditorTheme;
|
export default BlocksEditorTheme
|
||||||
|
|||||||
@@ -921,24 +921,12 @@ body {
|
|||||||
|
|
||||||
.sticky-note.yellow {
|
.sticky-note.yellow {
|
||||||
border-top: 1px solid #fdfd86;
|
border-top: 1px solid #fdfd86;
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg, #ffff88 81%, #ffff88 82%, #ffff88 82%, #ffffc6 100%);
|
||||||
135deg,
|
|
||||||
#ffff88 81%,
|
|
||||||
#ffff88 82%,
|
|
||||||
#ffff88 82%,
|
|
||||||
#ffffc6 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-note.pink {
|
.sticky-note.pink {
|
||||||
border-top: 1px solid #e7d1e4;
|
border-top: 1px solid #e7d1e4;
|
||||||
background: linear-gradient(
|
background: linear-gradient(135deg, #f7cbe8 81%, #f7cbe8 82%, #f7cbe8 82%, #e7bfe1 100%);
|
||||||
135deg,
|
|
||||||
#f7cbe8 81%,
|
|
||||||
#f7cbe8 82%,
|
|
||||||
#f7cbe8 82%,
|
|
||||||
#e7bfe1 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-note-container.dragging {
|
.sticky-note-container.dragging {
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './Button.css';
|
import './Button.css'
|
||||||
|
|
||||||
import {ReactNode} from 'react';
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
import joinClasses from '../Utils/join-classes';
|
import joinClasses from '../Utils/join-classes'
|
||||||
|
|
||||||
export default function Button({
|
export default function Button({
|
||||||
'data-test-id': dataTestId,
|
'data-test-id': dataTestId,
|
||||||
@@ -21,28 +21,24 @@ export default function Button({
|
|||||||
small,
|
small,
|
||||||
title,
|
title,
|
||||||
}: {
|
}: {
|
||||||
'data-test-id'?: string;
|
'data-test-id'?: string
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
className?: string;
|
className?: string
|
||||||
disabled?: boolean;
|
disabled?: boolean
|
||||||
onClick: () => void;
|
onClick: () => void
|
||||||
small?: boolean;
|
small?: boolean
|
||||||
title?: string;
|
title?: string
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={joinClasses(
|
className={joinClasses('Button__root', disabled && 'Button__disabled', small && 'Button__small', className)}
|
||||||
'Button__root',
|
|
||||||
disabled && 'Button__disabled',
|
|
||||||
small && 'Button__small',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={title}
|
title={title}
|
||||||
aria-label={title}
|
aria-label={title}
|
||||||
{...(dataTestId && {'data-test-id': dataTestId})}>
|
{...(dataTestId && { 'data-test-id': dataTestId })}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,23 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './Dialog.css';
|
import './Dialog.css'
|
||||||
|
|
||||||
import {ReactNode} from 'react';
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
'data-test-id'?: string;
|
'data-test-id'?: string
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
export function DialogButtonsList({children}: Props): JSX.Element {
|
export function DialogButtonsList({ children }: Props): JSX.Element {
|
||||||
return <div className="DialogButtonsList">{children}</div>;
|
return <div className="DialogButtonsList">{children}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DialogActions({
|
export function DialogActions({ 'data-test-id': dataTestId, children }: Props): JSX.Element {
|
||||||
'data-test-id': dataTestId,
|
|
||||||
children,
|
|
||||||
}: Props): JSX.Element {
|
|
||||||
return (
|
return (
|
||||||
<div className="DialogActions" data-test-id={dataTestId}>
|
<div className="DialogActions" data-test-id={dataTestId}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,85 +6,73 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './LinkPreview.css';
|
import './LinkPreview.css'
|
||||||
|
|
||||||
import {CSSProperties, Suspense} from 'react';
|
import { CSSProperties, Suspense } from 'react'
|
||||||
|
|
||||||
type Preview = {
|
type Preview = {
|
||||||
title: string;
|
title: string
|
||||||
description: string;
|
description: string
|
||||||
img: string;
|
img: string
|
||||||
domain: string;
|
domain: string
|
||||||
} | null;
|
} | null
|
||||||
|
|
||||||
// Cached responses or running request promises
|
// Cached responses or running request promises
|
||||||
const PREVIEW_CACHE: Record<string, Promise<Preview> | {preview: Preview}> = {};
|
const PREVIEW_CACHE: Record<string, Promise<Preview> | { preview: Preview }> = {}
|
||||||
|
|
||||||
const URL_MATCHER =
|
const URL_MATCHER =
|
||||||
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/
|
||||||
|
|
||||||
function useSuspenseRequest(url: string) {
|
function useSuspenseRequest(url: string) {
|
||||||
let cached = PREVIEW_CACHE[url];
|
let cached = PREVIEW_CACHE[url]
|
||||||
|
|
||||||
if (!url.match(URL_MATCHER)) {
|
if (!url.match(URL_MATCHER)) {
|
||||||
return {preview: null};
|
return { preview: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
cached = PREVIEW_CACHE[url] = fetch(
|
cached = PREVIEW_CACHE[url] = fetch(`/api/link-preview?url=${encodeURI(url)}`)
|
||||||
`/api/link-preview?url=${encodeURI(url)}`,
|
|
||||||
)
|
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((preview) => {
|
.then((preview) => {
|
||||||
PREVIEW_CACHE[url] = preview;
|
PREVIEW_CACHE[url] = preview
|
||||||
return preview;
|
return preview
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
PREVIEW_CACHE[url] = {preview: null};
|
PREVIEW_CACHE[url] = { preview: null }
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cached instanceof Promise) {
|
if (cached instanceof Promise) {
|
||||||
throw cached;
|
throw cached
|
||||||
}
|
}
|
||||||
|
|
||||||
return cached;
|
return cached
|
||||||
}
|
}
|
||||||
|
|
||||||
function LinkPreviewContent({
|
function LinkPreviewContent({
|
||||||
url,
|
url,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
url: string;
|
url: string
|
||||||
}>): JSX.Element | null {
|
}>): JSX.Element | null {
|
||||||
const {preview} = useSuspenseRequest(url);
|
const { preview } = useSuspenseRequest(url)
|
||||||
if (preview === null) {
|
if (preview === null) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="LinkPreview__container">
|
<div className="LinkPreview__container">
|
||||||
{preview.img && (
|
{preview.img && (
|
||||||
<div className="LinkPreview__imageWrapper">
|
<div className="LinkPreview__imageWrapper">
|
||||||
<img
|
<img src={preview.img} alt={preview.title} className="LinkPreview__image" />
|
||||||
src={preview.img}
|
|
||||||
alt={preview.title}
|
|
||||||
className="LinkPreview__image"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{preview.domain && (
|
{preview.domain && <div className="LinkPreview__domain">{preview.domain}</div>}
|
||||||
<div className="LinkPreview__domain">{preview.domain}</div>
|
{preview.title && <div className="LinkPreview__title">{preview.title}</div>}
|
||||||
)}
|
{preview.description && <div className="LinkPreview__description">{preview.description}</div>}
|
||||||
{preview.title && (
|
|
||||||
<div className="LinkPreview__title">{preview.title}</div>
|
|
||||||
)}
|
|
||||||
{preview.description && (
|
|
||||||
<div className="LinkPreview__description">{preview.description}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Glimmer(props: {style: CSSProperties; index: number}): JSX.Element {
|
function Glimmer(props: { style: CSSProperties; index: number }): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="LinkPreview__glimmer"
|
className="LinkPreview__glimmer"
|
||||||
@@ -94,24 +82,25 @@ function Glimmer(props: {style: CSSProperties; index: number}): JSX.Element {
|
|||||||
...(props.style || {}),
|
...(props.style || {}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LinkPreview({
|
export default function LinkPreview({
|
||||||
url,
|
url,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
url: string;
|
url: string
|
||||||
}>): JSX.Element {
|
}>): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<Glimmer style={{height: '80px'}} index={0} />
|
<Glimmer style={{ height: '80px' }} index={0} />
|
||||||
<Glimmer style={{width: '60%'}} index={1} />
|
<Glimmer style={{ width: '60%' }} index={1} />
|
||||||
<Glimmer style={{width: '80%'}} index={2} />
|
<Glimmer style={{ width: '80%' }} index={2} />
|
||||||
</>
|
</>
|
||||||
}>
|
}
|
||||||
|
>
|
||||||
<LinkPreviewContent url={url} />
|
<LinkPreviewContent url={url} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
border-radius: 0px;
|
border-radius: 0px;
|
||||||
}
|
}
|
||||||
.Modal__title {
|
.Modal__title {
|
||||||
color:var(--sn-stylekit-foreground-color);
|
color: var(--sn-stylekit-foreground-color);
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
border-bottom: 1px solid var(--sn-stylekit-border-color);
|
border-bottom: 1px solid var(--sn-stylekit-border-color);
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './Modal.css';
|
import './Modal.css'
|
||||||
|
|
||||||
import {ReactNode, useEffect, useRef} from 'react';
|
import { ReactNode, useEffect, useRef } from 'react'
|
||||||
import {createPortal} from 'react-dom';
|
import { createPortal } from 'react-dom'
|
||||||
|
|
||||||
function PortalImpl({
|
function PortalImpl({
|
||||||
onClose,
|
onClose,
|
||||||
@@ -17,68 +17,60 @@ function PortalImpl({
|
|||||||
title,
|
title,
|
||||||
closeOnClickOutside,
|
closeOnClickOutside,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
closeOnClickOutside: boolean;
|
closeOnClickOutside: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
title: string;
|
title: string
|
||||||
}) {
|
}) {
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
const modalRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modalRef.current !== null) {
|
if (modalRef.current !== null) {
|
||||||
modalRef.current.focus();
|
modalRef.current.focus()
|
||||||
}
|
}
|
||||||
}, []);
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let modalOverlayElement: HTMLElement | null = null;
|
let modalOverlayElement: HTMLElement | null = null
|
||||||
const handler = (event: KeyboardEvent) => {
|
const handler = (event: KeyboardEvent) => {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
onClose();
|
onClose()
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
const clickOutsideHandler = (event: MouseEvent) => {
|
const clickOutsideHandler = (event: MouseEvent) => {
|
||||||
const target = event.target;
|
const target = event.target
|
||||||
if (
|
if (modalRef.current !== null && !modalRef.current.contains(target as Node) && closeOnClickOutside) {
|
||||||
modalRef.current !== null &&
|
onClose()
|
||||||
!modalRef.current.contains(target as Node) &&
|
|
||||||
closeOnClickOutside
|
|
||||||
) {
|
|
||||||
onClose();
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
if (modalRef.current !== null) {
|
if (modalRef.current !== null) {
|
||||||
modalOverlayElement = modalRef.current?.parentElement;
|
modalOverlayElement = modalRef.current?.parentElement
|
||||||
if (modalOverlayElement !== null) {
|
if (modalOverlayElement !== null) {
|
||||||
modalOverlayElement?.addEventListener('click', clickOutsideHandler);
|
modalOverlayElement?.addEventListener('click', clickOutsideHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', handler);
|
window.addEventListener('keydown', handler)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', handler);
|
window.removeEventListener('keydown', handler)
|
||||||
if (modalOverlayElement !== null) {
|
if (modalOverlayElement !== null) {
|
||||||
modalOverlayElement?.removeEventListener('click', clickOutsideHandler);
|
modalOverlayElement?.removeEventListener('click', clickOutsideHandler)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}, [closeOnClickOutside, onClose]);
|
}, [closeOnClickOutside, onClose])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Modal__overlay" role="dialog">
|
<div className="Modal__overlay" role="dialog">
|
||||||
<div className="Modal__modal" tabIndex={-1} ref={modalRef}>
|
<div className="Modal__modal" tabIndex={-1} ref={modalRef}>
|
||||||
<h2 className="Modal__title">{title}</h2>
|
<h2 className="Modal__title">{title}</h2>
|
||||||
<button
|
<button className="Modal__closeButton" aria-label="Close modal" type="button" onClick={onClose}>
|
||||||
className="Modal__closeButton"
|
|
||||||
aria-label="Close modal"
|
|
||||||
type="button"
|
|
||||||
onClick={onClose}>
|
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
<div className="Modal__content">{children}</div>
|
<div className="Modal__content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({
|
export default function Modal({
|
||||||
@@ -87,18 +79,15 @@ export default function Modal({
|
|||||||
title,
|
title,
|
||||||
closeOnClickOutside = false,
|
closeOnClickOutside = false,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
closeOnClickOutside?: boolean;
|
closeOnClickOutside?: boolean
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
title: string;
|
title: string
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return createPortal(
|
return createPortal(
|
||||||
<PortalImpl
|
<PortalImpl onClose={onClose} title={title} closeOnClickOutside={closeOnClickOutside}>
|
||||||
onClose={onClose}
|
|
||||||
title={title}
|
|
||||||
closeOnClickOutside={closeOnClickOutside}>
|
|
||||||
{children}
|
{children}
|
||||||
</PortalImpl>,
|
</PortalImpl>,
|
||||||
document.body,
|
document.body,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './Input.css';
|
import './Input.css'
|
||||||
|
|
||||||
type Props = Readonly<{
|
type Props = Readonly<{
|
||||||
'data-test-id'?: string;
|
'data-test-id'?: string
|
||||||
label: string;
|
label: string
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void
|
||||||
placeholder?: string;
|
placeholder?: string
|
||||||
value: string;
|
value: string
|
||||||
}>;
|
}>
|
||||||
|
|
||||||
export default function TextInput({
|
export default function TextInput({
|
||||||
label,
|
label,
|
||||||
@@ -32,10 +32,10 @@ export default function TextInput({
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
onChange(e.target.value);
|
onChange(e.target.value)
|
||||||
}}
|
}}
|
||||||
data-test-id={dataTestId}
|
data-test-id={dataTestId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,20 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function getDOMRangeRect(
|
export function getDOMRangeRect(nativeSelection: Selection, rootElement: HTMLElement): DOMRect {
|
||||||
nativeSelection: Selection,
|
const domRange = nativeSelection.getRangeAt(0)
|
||||||
rootElement: HTMLElement,
|
|
||||||
): DOMRect {
|
|
||||||
const domRange = nativeSelection.getRangeAt(0);
|
|
||||||
|
|
||||||
let rect;
|
let rect
|
||||||
|
|
||||||
if (nativeSelection.anchorNode === rootElement) {
|
if (nativeSelection.anchorNode === rootElement) {
|
||||||
let inner = rootElement;
|
let inner = rootElement
|
||||||
while (inner.firstElementChild != null) {
|
while (inner.firstElementChild != null) {
|
||||||
inner = inner.firstElementChild as HTMLElement;
|
inner = inner.firstElementChild as HTMLElement
|
||||||
}
|
}
|
||||||
rect = inner.getBoundingClientRect();
|
rect = inner.getBoundingClientRect()
|
||||||
} else {
|
} else {
|
||||||
rect = domRange.getBoundingClientRect();
|
rect = domRange.getBoundingClientRect()
|
||||||
}
|
}
|
||||||
|
|
||||||
return rect;
|
return rect
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,23 +5,21 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import {$isAtNodeEnd} from '@lexical/selection';
|
import { $isAtNodeEnd } from '@lexical/selection'
|
||||||
import {ElementNode, RangeSelection, TextNode} from 'lexical';
|
import { ElementNode, RangeSelection, TextNode } from 'lexical'
|
||||||
|
|
||||||
export function getSelectedNode(
|
export function getSelectedNode(selection: RangeSelection): TextNode | ElementNode {
|
||||||
selection: RangeSelection,
|
const anchor = selection.anchor
|
||||||
): TextNode | ElementNode {
|
const focus = selection.focus
|
||||||
const anchor = selection.anchor;
|
const anchorNode = selection.anchor.getNode()
|
||||||
const focus = selection.focus;
|
const focusNode = selection.focus.getNode()
|
||||||
const anchorNode = selection.anchor.getNode();
|
|
||||||
const focusNode = selection.focus.getNode();
|
|
||||||
if (anchorNode === focusNode) {
|
if (anchorNode === focusNode) {
|
||||||
return anchorNode;
|
return anchorNode
|
||||||
}
|
}
|
||||||
const isBackward = selection.isBackward();
|
const isBackward = selection.isBackward()
|
||||||
if (isBackward) {
|
if (isBackward) {
|
||||||
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
return $isAtNodeEnd(focus) ? anchorNode : focusNode
|
||||||
} else {
|
} else {
|
||||||
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
|
return $isAtNodeEnd(anchor) ? focusNode : anchorNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export function isHTMLElement(x: unknown): x is HTMLElement {
|
export function isHTMLElement(x: unknown): x is HTMLElement {
|
||||||
return x instanceof HTMLElement;
|
return x instanceof HTMLElement
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function joinClasses(
|
export default function joinClasses(...args: Array<string | boolean | null | undefined>) {
|
||||||
...args: Array<string | boolean | null | undefined>
|
return args.filter(Boolean).join(' ')
|
||||||
) {
|
|
||||||
return args.filter(Boolean).join(' ');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,50 +6,47 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class Point {
|
export class Point {
|
||||||
private readonly _x: number;
|
private readonly _x: number
|
||||||
private readonly _y: number;
|
private readonly _y: number
|
||||||
|
|
||||||
constructor(x: number, y: number) {
|
constructor(x: number, y: number) {
|
||||||
this._x = x;
|
this._x = x
|
||||||
this._y = y;
|
this._y = y
|
||||||
}
|
}
|
||||||
|
|
||||||
get x(): number {
|
get x(): number {
|
||||||
return this._x;
|
return this._x
|
||||||
}
|
}
|
||||||
|
|
||||||
get y(): number {
|
get y(): number {
|
||||||
return this._y;
|
return this._y
|
||||||
}
|
}
|
||||||
|
|
||||||
public equals({x, y}: Point): boolean {
|
public equals({ x, y }: Point): boolean {
|
||||||
return this.x === x && this.y === y;
|
return this.x === x && this.y === y
|
||||||
}
|
}
|
||||||
|
|
||||||
public calcDeltaXTo({x}: Point): number {
|
public calcDeltaXTo({ x }: Point): number {
|
||||||
return this.x - x;
|
return this.x - x
|
||||||
}
|
}
|
||||||
|
|
||||||
public calcDeltaYTo({y}: Point): number {
|
public calcDeltaYTo({ y }: Point): number {
|
||||||
return this.y - y;
|
return this.y - y
|
||||||
}
|
}
|
||||||
|
|
||||||
public calcHorizontalDistanceTo(point: Point): number {
|
public calcHorizontalDistanceTo(point: Point): number {
|
||||||
return Math.abs(this.calcDeltaXTo(point));
|
return Math.abs(this.calcDeltaXTo(point))
|
||||||
}
|
}
|
||||||
|
|
||||||
public calcVerticalDistance(point: Point): number {
|
public calcVerticalDistance(point: Point): number {
|
||||||
return Math.abs(this.calcDeltaYTo(point));
|
return Math.abs(this.calcDeltaYTo(point))
|
||||||
}
|
}
|
||||||
|
|
||||||
public calcDistanceTo(point: Point): number {
|
public calcDistanceTo(point: Point): number {
|
||||||
return Math.sqrt(
|
return Math.sqrt(Math.pow(this.calcDeltaXTo(point), 2) + Math.pow(this.calcDeltaYTo(point), 2))
|
||||||
Math.pow(this.calcDeltaXTo(point), 2) +
|
|
||||||
Math.pow(this.calcDeltaYTo(point), 2),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isPoint(x: unknown): x is Point {
|
export function isPoint(x: unknown): x is Point {
|
||||||
return x instanceof Point;
|
return x instanceof Point
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,83 +5,75 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import {isPoint, Point} from './point';
|
import { isPoint, Point } from './point'
|
||||||
|
|
||||||
export type ContainsPointReturn = {
|
export type ContainsPointReturn = {
|
||||||
result: boolean;
|
result: boolean
|
||||||
reason: {
|
reason: {
|
||||||
isOnTopSide: boolean;
|
isOnTopSide: boolean
|
||||||
isOnBottomSide: boolean;
|
isOnBottomSide: boolean
|
||||||
isOnLeftSide: boolean;
|
isOnLeftSide: boolean
|
||||||
isOnRightSide: boolean;
|
isOnRightSide: boolean
|
||||||
};
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export class Rect {
|
export class Rect {
|
||||||
private readonly _left: number;
|
private readonly _left: number
|
||||||
private readonly _top: number;
|
private readonly _top: number
|
||||||
private readonly _right: number;
|
private readonly _right: number
|
||||||
private readonly _bottom: number;
|
private readonly _bottom: number
|
||||||
|
|
||||||
constructor(left: number, top: number, right: number, bottom: number) {
|
constructor(left: number, top: number, right: number, bottom: number) {
|
||||||
const [physicTop, physicBottom] =
|
const [physicTop, physicBottom] = top <= bottom ? [top, bottom] : [bottom, top]
|
||||||
top <= bottom ? [top, bottom] : [bottom, top];
|
|
||||||
|
|
||||||
const [physicLeft, physicRight] =
|
const [physicLeft, physicRight] = left <= right ? [left, right] : [right, left]
|
||||||
left <= right ? [left, right] : [right, left];
|
|
||||||
|
|
||||||
this._top = physicTop;
|
this._top = physicTop
|
||||||
this._right = physicRight;
|
this._right = physicRight
|
||||||
this._left = physicLeft;
|
this._left = physicLeft
|
||||||
this._bottom = physicBottom;
|
this._bottom = physicBottom
|
||||||
}
|
}
|
||||||
|
|
||||||
get top(): number {
|
get top(): number {
|
||||||
return this._top;
|
return this._top
|
||||||
}
|
}
|
||||||
|
|
||||||
get right(): number {
|
get right(): number {
|
||||||
return this._right;
|
return this._right
|
||||||
}
|
}
|
||||||
|
|
||||||
get bottom(): number {
|
get bottom(): number {
|
||||||
return this._bottom;
|
return this._bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
get left(): number {
|
get left(): number {
|
||||||
return this._left;
|
return this._left
|
||||||
}
|
}
|
||||||
|
|
||||||
get width(): number {
|
get width(): number {
|
||||||
return Math.abs(this._left - this._right);
|
return Math.abs(this._left - this._right)
|
||||||
}
|
}
|
||||||
|
|
||||||
get height(): number {
|
get height(): number {
|
||||||
return Math.abs(this._bottom - this._top);
|
return Math.abs(this._bottom - this._top)
|
||||||
}
|
}
|
||||||
|
|
||||||
public equals({top, left, bottom, right}: Rect): boolean {
|
public equals({ top, left, bottom, right }: Rect): boolean {
|
||||||
return (
|
return top === this._top && bottom === this._bottom && left === this._left && right === this._right
|
||||||
top === this._top &&
|
|
||||||
bottom === this._bottom &&
|
|
||||||
left === this._left &&
|
|
||||||
right === this._right
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public contains({x, y}: Point): ContainsPointReturn;
|
public contains({ x, y }: Point): ContainsPointReturn
|
||||||
public contains({top, left, bottom, right}: Rect): boolean;
|
public contains({ top, left, bottom, right }: Rect): boolean
|
||||||
public contains(target: Point | Rect): boolean | ContainsPointReturn {
|
public contains(target: Point | Rect): boolean | ContainsPointReturn {
|
||||||
if (isPoint(target)) {
|
if (isPoint(target)) {
|
||||||
const {x, y} = target;
|
const { x, y } = target
|
||||||
|
|
||||||
const isOnTopSide = y < this._top;
|
const isOnTopSide = y < this._top
|
||||||
const isOnBottomSide = y > this._bottom;
|
const isOnBottomSide = y > this._bottom
|
||||||
const isOnLeftSide = x < this._left;
|
const isOnLeftSide = x < this._left
|
||||||
const isOnRightSide = x > this._right;
|
const isOnRightSide = x > this._right
|
||||||
|
|
||||||
const result =
|
const result = !isOnTopSide && !isOnBottomSide && !isOnLeftSide && !isOnRightSide
|
||||||
!isOnTopSide && !isOnBottomSide && !isOnLeftSide && !isOnRightSide;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reason: {
|
reason: {
|
||||||
@@ -91,9 +83,9 @@ export class Rect {
|
|||||||
isOnTopSide,
|
isOnTopSide,
|
||||||
},
|
},
|
||||||
result,
|
result,
|
||||||
};
|
}
|
||||||
} else {
|
} else {
|
||||||
const {top, left, bottom, right} = target;
|
const { top, left, bottom, right } = target
|
||||||
|
|
||||||
return (
|
return (
|
||||||
top >= this._top &&
|
top >= this._top &&
|
||||||
@@ -104,55 +96,40 @@ export class Rect {
|
|||||||
left <= this._right &&
|
left <= this._right &&
|
||||||
right >= this._left &&
|
right >= this._left &&
|
||||||
right <= this._right
|
right <= this._right
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public intersectsWith(rect: Rect): boolean {
|
public intersectsWith(rect: Rect): boolean {
|
||||||
const {left: x1, top: y1, width: w1, height: h1} = rect;
|
const { left: x1, top: y1, width: w1, height: h1 } = rect
|
||||||
const {left: x2, top: y2, width: w2, height: h2} = this;
|
const { left: x2, top: y2, width: w2, height: h2 } = this
|
||||||
const maxX = x1 + w1 >= x2 + w2 ? x1 + w1 : x2 + w2;
|
const maxX = x1 + w1 >= x2 + w2 ? x1 + w1 : x2 + w2
|
||||||
const maxY = y1 + h1 >= y2 + h2 ? y1 + h1 : y2 + h2;
|
const maxY = y1 + h1 >= y2 + h2 ? y1 + h1 : y2 + h2
|
||||||
const minX = x1 <= x2 ? x1 : x2;
|
const minX = x1 <= x2 ? x1 : x2
|
||||||
const minY = y1 <= y2 ? y1 : y2;
|
const minY = y1 <= y2 ? y1 : y2
|
||||||
return maxX - minX <= w1 + w2 && maxY - minY <= h1 + h2;
|
return maxX - minX <= w1 + w2 && maxY - minY <= h1 + h2
|
||||||
}
|
}
|
||||||
|
|
||||||
public generateNewRect({
|
public generateNewRect({ left = this.left, top = this.top, right = this.right, bottom = this.bottom }): Rect {
|
||||||
left = this.left,
|
return new Rect(left, top, right, bottom)
|
||||||
top = this.top,
|
|
||||||
right = this.right,
|
|
||||||
bottom = this.bottom,
|
|
||||||
}): Rect {
|
|
||||||
return new Rect(left, top, right, bottom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromLTRB(
|
static fromLTRB(left: number, top: number, right: number, bottom: number): Rect {
|
||||||
left: number,
|
return new Rect(left, top, right, bottom)
|
||||||
top: number,
|
|
||||||
right: number,
|
|
||||||
bottom: number,
|
|
||||||
): Rect {
|
|
||||||
return new Rect(left, top, right, bottom);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromLWTH(
|
static fromLWTH(left: number, width: number, top: number, height: number): Rect {
|
||||||
left: number,
|
return new Rect(left, top, left + width, top + height)
|
||||||
width: number,
|
|
||||||
top: number,
|
|
||||||
height: number,
|
|
||||||
): Rect {
|
|
||||||
return new Rect(left, top, left + width, top + height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromPoints(startPoint: Point, endPoint: Point): Rect {
|
static fromPoints(startPoint: Point, endPoint: Point): Rect {
|
||||||
const {y: top, x: left} = startPoint;
|
const { y: top, x: left } = startPoint
|
||||||
const {y: bottom, x: right} = endPoint;
|
const { y: bottom, x: right } = endPoint
|
||||||
return Rect.fromLTRB(left, top, right, bottom);
|
return Rect.fromLTRB(left, top, right, bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromDOM(dom: HTMLElement): Rect {
|
static fromDOM(dom: HTMLElement): Rect {
|
||||||
const {top, width, left, height} = dom.getBoundingClientRect();
|
const { top, width, left, height } = dom.getBoundingClientRect()
|
||||||
return Rect.fromLWTH(left, width, top, height);
|
return Rect.fromLWTH(left, width, top, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,17 @@
|
|||||||
|
|
||||||
export const sanitizeUrl = (url: string): string => {
|
export const sanitizeUrl = (url: string): string => {
|
||||||
/** A pattern that matches safe URLs. */
|
/** A pattern that matches safe URLs. */
|
||||||
const SAFE_URL_PATTERN =
|
const SAFE_URL_PATTERN = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi
|
||||||
/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi;
|
|
||||||
|
|
||||||
/** A pattern that matches safe data URLs. */
|
/** A pattern that matches safe data URLs. */
|
||||||
const DATA_URL_PATTERN =
|
const DATA_URL_PATTERN =
|
||||||
/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;
|
/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i
|
||||||
|
|
||||||
url = String(url).trim();
|
url = String(url).trim()
|
||||||
|
|
||||||
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url;
|
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
return `https://`;
|
return 'https://'
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const VERTICAL_GAP = 10;
|
const VERTICAL_GAP = 10
|
||||||
const HORIZONTAL_OFFSET = 5;
|
const HORIZONTAL_OFFSET = 5
|
||||||
|
|
||||||
export function setFloatingElemPosition(
|
export function setFloatingElemPosition(
|
||||||
targetRect: ClientRect | null,
|
targetRect: ClientRect | null,
|
||||||
@@ -15,32 +15,32 @@ export function setFloatingElemPosition(
|
|||||||
verticalGap: number = VERTICAL_GAP,
|
verticalGap: number = VERTICAL_GAP,
|
||||||
horizontalOffset: number = HORIZONTAL_OFFSET,
|
horizontalOffset: number = HORIZONTAL_OFFSET,
|
||||||
): void {
|
): void {
|
||||||
const scrollerElem = anchorElem.parentElement;
|
const scrollerElem = anchorElem.parentElement
|
||||||
|
|
||||||
if (targetRect === null || !scrollerElem) {
|
if (targetRect === null || !scrollerElem) {
|
||||||
floatingElem.style.opacity = '0';
|
floatingElem.style.opacity = '0'
|
||||||
floatingElem.style.transform = 'translate(-10000px, -10000px)';
|
floatingElem.style.transform = 'translate(-10000px, -10000px)'
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const floatingElemRect = floatingElem.getBoundingClientRect();
|
const floatingElemRect = floatingElem.getBoundingClientRect()
|
||||||
const anchorElementRect = anchorElem.getBoundingClientRect();
|
const anchorElementRect = anchorElem.getBoundingClientRect()
|
||||||
const editorScrollerRect = scrollerElem.getBoundingClientRect();
|
const editorScrollerRect = scrollerElem.getBoundingClientRect()
|
||||||
|
|
||||||
let top = targetRect.top - floatingElemRect.height - verticalGap;
|
let top = targetRect.top - floatingElemRect.height - verticalGap
|
||||||
let left = targetRect.left - horizontalOffset;
|
let left = targetRect.left - horizontalOffset
|
||||||
|
|
||||||
if (top < editorScrollerRect.top) {
|
if (top < editorScrollerRect.top) {
|
||||||
top += floatingElemRect.height + targetRect.height + verticalGap * 2;
|
top += floatingElemRect.height + targetRect.height + verticalGap * 2
|
||||||
}
|
}
|
||||||
|
|
||||||
if (left + floatingElemRect.width > editorScrollerRect.right) {
|
if (left + floatingElemRect.width > editorScrollerRect.right) {
|
||||||
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
|
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
top -= anchorElementRect.top;
|
top -= anchorElementRect.top
|
||||||
left -= anchorElementRect.left;
|
left -= anchorElementRect.left
|
||||||
|
|
||||||
floatingElem.style.opacity = '1';
|
floatingElem.style.opacity = '1'
|
||||||
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
floatingElem.style.transform = `translate(${left}px, ${top}px)`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export * from './Editor/BlocksEditor';
|
export * from './Editor/BlocksEditor'
|
||||||
export * from './Editor/BlocksEditorComposer';
|
export * from './Editor/BlocksEditorComposer'
|
||||||
export * from './Editor/Constants';
|
export * from './Editor/Constants'
|
||||||
export * from './Editor/MarkdownTransformers';
|
export * from './Editor/MarkdownTransformers'
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"@babel/plugin-transform-react-jsx": "^7.19.0",
|
"@babel/plugin-transform-react-jsx": "^7.19.0",
|
||||||
"@babel/preset-env": "*",
|
"@babel/preset-env": "*",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@lexical/react": "0.7.5",
|
"@lexical/react": "0.7.6",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
|
||||||
"@reach/alert": "^0.18.0",
|
"@reach/alert": "^0.18.0",
|
||||||
"@reach/alert-dialog": "^0.18.0",
|
"@reach/alert-dialog": "^0.18.0",
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
"identity-obj-proxy": "^3.0.0",
|
"identity-obj-proxy": "^3.0.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jest-environment-jsdom": "^29.3.1",
|
"jest-environment-jsdom": "^29.3.1",
|
||||||
"lexical": "0.7.5",
|
"lexical": "0.7.6",
|
||||||
"lint-staged": ">=13",
|
"lint-staged": ">=13",
|
||||||
"mini-css-extract-plugin": "^2.7.2",
|
"mini-css-extract-plugin": "^2.7.2",
|
||||||
"minimatch": "^5.1.1",
|
"minimatch": "^5.1.1",
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.19",
|
||||||
"postcss-loader": "^7.0.2",
|
"postcss-loader": "^7.0.2",
|
||||||
"prettier": "*",
|
"prettier": "*",
|
||||||
"prettier-plugin-tailwindcss": "^0.2.0",
|
"prettier-plugin-tailwindcss": "^0.2.1",
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
|||||||
348
yarn.lock
348
yarn.lock
@@ -3912,245 +3912,245 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/clipboard@npm:0.7.5":
|
"@lexical/clipboard@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/clipboard@npm:0.7.5"
|
resolution: "@lexical/clipboard@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/html": 0.7.5
|
"@lexical/html": 0.7.6
|
||||||
"@lexical/list": 0.7.5
|
"@lexical/list": 0.7.6
|
||||||
"@lexical/selection": 0.7.5
|
"@lexical/selection": 0.7.6
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: a3fb01ecd9d64b3f789fa8323e24d2cb70c50e1602e77d9daec2dfecd7794c9ca28115ed50046aca30f94f722ea5a57036b9777557efc5ae699e9315235f1716
|
checksum: 2cc08a28f9b8752a9decb9ea6909f3c3e36d7ef875d6bf2430e253b98c131ca15688843af747246446d23b391574c6379ffb4a3f7b52c235b9fbefbc5162408a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/code@npm:0.7.5":
|
"@lexical/code@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/code@npm:0.7.5"
|
resolution: "@lexical/code@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
prismjs: ^1.27.0
|
prismjs: ^1.27.0
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 14522373aeb65acad1596c2159ef555347466c6687f464acf26fbd96f8c623c1d3582b1b7a1fcd592872549a3538220ed2ade122f8d184f3a7dc9e2f16f3c91b
|
checksum: 67c688ae05812bbc612614027e4269be791541d4612a041c2f4ee8bb2ee53da6b9d39e64a5a5f3094b8d77bc8be5c53effdd6a5f28b9c052edaf99b46834a95c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/dragon@npm:0.7.5":
|
"@lexical/dragon@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/dragon@npm:0.7.5"
|
resolution: "@lexical/dragon@npm:0.7.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 690e051f4441fdd776c00e362dc9f6ed62624a68fc719913c65ebaec22212da5e20ef811fce0d44769f4811d55f85d4747900cb2f93c48b4a64fd7c323fbe99a
|
checksum: d565cc502fc00e48b63ca397a5fb5c049ed3146401f822faca85f86cc4332af9f14a9d08611c8add7996c7bc1f077dd7efe7eecfd09f0bb2904db715ea4ce193
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/hashtag@npm:0.7.5":
|
"@lexical/hashtag@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/hashtag@npm:0.7.5"
|
resolution: "@lexical/hashtag@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 783fd2d6c085fabd399d9e42cc333d0cd10a30ff40291eb99ad7e5f67895cc2a9dca800cba17ded6ee7f77af4ddf25e48bf5992b3441ebdb7b24f0738e23099f
|
checksum: 352251fc413b96223facddac3177e5a00a806e15e921afbdb9c2e3576a3817bfa8b4ba527d05e35c0e1054467c994d10e3235817378fe0bd64f9d45709841f4b
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/history@npm:0.7.5":
|
"@lexical/history@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/history@npm:0.7.5"
|
resolution: "@lexical/history@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 34569fc29d5f4ae7aae501ca492ef0d6c51d2d364947e54e4c5515e9b38bbea47935594651abf73e8c93e5f059ae122fce49d5aae40dddc7622742ae43c0d9d1
|
checksum: 7461b6ace7e9cdc35577c35159f3bda692231e1e7b6b13755705fdd1b226848782b0b210fdd03a61613c6eab300b1139f0ed5dc04908265575cef96a95a13fc0
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/html@npm:0.7.5":
|
"@lexical/html@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/html@npm:0.7.5"
|
resolution: "@lexical/html@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/selection": 0.7.5
|
"@lexical/selection": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: bf88318aacf5613f436a24c9698bc4f5784073ad75c7a05ecab38893c7765f2a57fcba9661a260bc7d15c040fc33c1a505f508a5bb7509d9001a62873272d062
|
checksum: a53778ab121a58ff2e3833c32d30c39375a709aaa4dc69f4d040256472f22e73565ab522cbef925ba4eb86b574d447eb9ebe4cbfa56fb99db8cb99b2c44c3e25
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/link@npm:0.7.5":
|
"@lexical/link@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/link@npm:0.7.5"
|
resolution: "@lexical/link@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 153f88575ae657d9bda57d77c62cf98bed2f6c14a76f2b3dbf7b96adcc5f86a68898869a56dd8dc7a2f63ec8bb1a0056749539861337b36e735e8c810525c581
|
checksum: ecf487bfd9bc0bfb2e04565e02cd2754e55ce80250473877a626d8bd5bad2ecedd4f3b954dff89f0bc15215a41e6c43c768032ff3707fef9865ceec1a75cc672
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/list@npm:0.7.5":
|
"@lexical/list@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/list@npm:0.7.5"
|
resolution: "@lexical/list@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: f44602977ad4194019de5fe3cee6b3fa5ce29604a4db375f184376009d0e2e6bb1d25764e1335193e5783314b9ea90b568d0ee23472c26ff34c3eadbfcf52604
|
checksum: 74b02536f70b01c3104ec3fb9b3e51ba33f6c5ff0b7417529c8c8bd20ad5af760f0c2b0163fa88da216e1c9148eb6536b4a04e91551c67498e00bee9a971b6a4
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/mark@npm:0.7.5":
|
"@lexical/mark@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/mark@npm:0.7.5"
|
resolution: "@lexical/mark@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 3e8641a71c295945f4414992a3acca7e9363c0a1fbf57abdfb150692e873a646f13f7d963f4b22236951491eacd23074f55e471457eef2402b6bc57e76c51d54
|
checksum: fe361ce1920e65f67205962eb3b594fd22c02cf90e82f4ab463789aa39023817aedf2eb02eb09f20e88d38d511f0e4011304c8089d6cc903fb66dd47714b47e7
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/markdown@npm:0.7.5":
|
"@lexical/markdown@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/markdown@npm:0.7.5"
|
resolution: "@lexical/markdown@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/code": 0.7.5
|
"@lexical/code": 0.7.6
|
||||||
"@lexical/link": 0.7.5
|
"@lexical/link": 0.7.6
|
||||||
"@lexical/list": 0.7.5
|
"@lexical/list": 0.7.6
|
||||||
"@lexical/rich-text": 0.7.5
|
"@lexical/rich-text": 0.7.6
|
||||||
"@lexical/text": 0.7.5
|
"@lexical/text": 0.7.6
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: ac27bd53090802e8027355e3ed5cf43e98d15bd4f67d92eb560de70e86c406c871c85d616ad2dc8ba2bc997d4caf99bfdce85adbb6ebfaf18d5aad4f3b7ff515
|
checksum: 1d73027e84f5c344781a12885255134f7497ae9d37b1d89fb043255299b1b46f2def6e316680d3866bc163f18983d14a61abff81e7f2c639141309544542bf62
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/offset@npm:0.7.5":
|
"@lexical/offset@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/offset@npm:0.7.5"
|
resolution: "@lexical/offset@npm:0.7.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: f5b713e551b8e66745ced24c0d9176c911c6c76705887b1cfea8cec4cee8752d12e6601260e15fdddbe9c6b0529dfb308e80467964a3552c3be7b7e4a1c9d03d
|
checksum: 249a2684dedd4a889074fb05086b5c236b786bab4349cb199e9e41ab009d6d66d8145b89995b6f648fa21c691eeb0d7bf9a7181489775e70ef9807f6d6937447
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/overflow@npm:0.7.5":
|
"@lexical/overflow@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/overflow@npm:0.7.5"
|
resolution: "@lexical/overflow@npm:0.7.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: a4694a96b6e8b47ad3f91c51ab12eb19920fdfc675607f4e89e6c931a6d55fa140298f0ba285ac9512c66ee2fc92ba920fd76f5e616c41bbeb0401fd2bd1475f
|
checksum: 77755f1ed96db43604bedb2327cf046eabd23df8901c16ac1aab72ffda316e4bc8dafc1232a94c2402a3205aa3e647dc75d8772fa9bca7e91ad669d841999352
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/plain-text@npm:0.7.5":
|
"@lexical/plain-text@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/plain-text@npm:0.7.5"
|
resolution: "@lexical/plain-text@npm:0.7.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@lexical/clipboard": 0.7.5
|
"@lexical/clipboard": 0.7.6
|
||||||
"@lexical/selection": 0.7.5
|
"@lexical/selection": 0.7.6
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 506d87b7f188b9d46dca7996125537e75f576c475d378b4247a236adf8589ba74ba93c4febc580201d6a1ddfee1e85d5d708a85f657537191bb9f28de9ab366d
|
checksum: f9d3cd04be4d9dc12e9c87dfadb7cbd7bcd918baf4690938943c3fae330cd10f7d6b16832d8e5815f54d6f91dd1f7b7b55b34a7db5d87ca6e40ff5b70aebe42f
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/react@npm:0.7.5":
|
"@lexical/react@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/react@npm:0.7.5"
|
resolution: "@lexical/react@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/clipboard": 0.7.5
|
"@lexical/clipboard": 0.7.6
|
||||||
"@lexical/code": 0.7.5
|
"@lexical/code": 0.7.6
|
||||||
"@lexical/dragon": 0.7.5
|
"@lexical/dragon": 0.7.6
|
||||||
"@lexical/hashtag": 0.7.5
|
"@lexical/hashtag": 0.7.6
|
||||||
"@lexical/history": 0.7.5
|
"@lexical/history": 0.7.6
|
||||||
"@lexical/link": 0.7.5
|
"@lexical/link": 0.7.6
|
||||||
"@lexical/list": 0.7.5
|
"@lexical/list": 0.7.6
|
||||||
"@lexical/mark": 0.7.5
|
"@lexical/mark": 0.7.6
|
||||||
"@lexical/markdown": 0.7.5
|
"@lexical/markdown": 0.7.6
|
||||||
"@lexical/overflow": 0.7.5
|
"@lexical/overflow": 0.7.6
|
||||||
"@lexical/plain-text": 0.7.5
|
"@lexical/plain-text": 0.7.6
|
||||||
"@lexical/rich-text": 0.7.5
|
"@lexical/rich-text": 0.7.6
|
||||||
"@lexical/selection": 0.7.5
|
"@lexical/selection": 0.7.6
|
||||||
"@lexical/table": 0.7.5
|
"@lexical/table": 0.7.6
|
||||||
"@lexical/text": 0.7.5
|
"@lexical/text": 0.7.6
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
"@lexical/yjs": 0.7.5
|
"@lexical/yjs": 0.7.6
|
||||||
react-error-boundary: ^3.1.4
|
react-error-boundary: ^3.1.4
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
react: ">=17.x"
|
react: ">=17.x"
|
||||||
react-dom: ">=17.x"
|
react-dom: ">=17.x"
|
||||||
checksum: 0df2bd2c3d7fcafc5d29294bf4e9adac2ebf8ce211eb8f988df929005d95c29f061d2572d0b208ac5e43b068d1a516dbaca4899f4801b1e73773e4baa650a6a1
|
checksum: f98a47b09ec14a0f4ccbba638d8e9ceb3d9728bb36377a262139703386e5703e5632f7d2424831a623feca5cc21c0c1c8195139443ca0e34b7dc8cb1ddd70bf2
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/rich-text@npm:0.7.5":
|
"@lexical/rich-text@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/rich-text@npm:0.7.5"
|
resolution: "@lexical/rich-text@npm:0.7.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@lexical/clipboard": 0.7.5
|
"@lexical/clipboard": 0.7.6
|
||||||
"@lexical/selection": 0.7.5
|
"@lexical/selection": 0.7.6
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 8b58eae1161301ae2c01b7c97e044a8934ef501b90d5e90578b29ce7e0b5ddb85e66349eb6ccfda0b0bb19827095701133d0062155b12b180c7b0e5a1a23ede5
|
checksum: 53ddbd4e2a068cc026d3821efab873fb2e47aa5142e8b1c34659c64a40d35c27a5853b0d452371f548d8c6f242743d566c768a5dd7f394e10e09024334a727a1
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/selection@npm:0.7.5":
|
"@lexical/selection@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/selection@npm:0.7.5"
|
resolution: "@lexical/selection@npm:0.7.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 57907d740daacdff0a66f141cfc9bd1827a07dcbe07d2517bb7c0ae1258d7219ae349a1cecd3c22e4a4b1346a827a2a3948aebdc9642ebb68193fe4c7c0fd5b6
|
checksum: 522d6ea559ec1f5826b3827bfa3d7381e27bbd0458e9e8f85529e78a46fd404c4eb3bcd389d30fd28c24244ba6fa0c8255fde530488b96271a05cc761dd96204
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/table@npm:0.7.5":
|
"@lexical/table@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/table@npm:0.7.5"
|
resolution: "@lexical/table@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/utils": 0.7.5
|
"@lexical/utils": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 6d0b3177d419e2f8ad833edcaa3525f3df337e29d83c65ed1c61db50101ea1b5117f94bb33f364f72947e20cfd9988270751fd9367dbc106c3f7efe9b0030615
|
checksum: 2c6e93516b71de2be823884d8cab2d4e0a553a03d7d16fed3f6c7121b159a3b4e5bca8552891633b320f7142f00bd21c43106e1f81a29a21ac9c10ee9786e3a7
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/text@npm:0.7.5":
|
"@lexical/text@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/text@npm:0.7.5"
|
resolution: "@lexical/text@npm:0.7.6"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 445d9dd3cce8a816f9a96637ff09e3cf437ddb9b5f5e0ae107ca18713875db0edf75e3abc16ff1f92961f9c413ddccae721288f045571e8c495210226c3c60b7
|
checksum: f8d645dfbd71ae49db26e1a043630b12f90b6e067852541507124f172daea134b2ca161c5118e084dde6d97aa1b26a3961ebebc4633e1894aa7d01f19215d135
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/utils@npm:0.7.5":
|
"@lexical/utils@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/utils@npm:0.7.5"
|
resolution: "@lexical/utils@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/list": 0.7.5
|
"@lexical/list": 0.7.6
|
||||||
"@lexical/table": 0.7.5
|
"@lexical/table": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
checksum: 9f46fe564198f52777f5c90b672909df557e52a1e05c4d3ca4d5268f3b6ee8d155d24115fb06e3b9e16cc5fe03c8b64e8eeac8416c4985edd49b8e28d1814b51
|
checksum: 7562549583102d26e3d48c263fb726e2f24f73e94574264e5565c1a2cc4aeb45ede1d55e809f4a16f0611221ac595c3110be956c5ed59718d9357d7e39b8e1dd
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@lexical/yjs@npm:0.7.5":
|
"@lexical/yjs@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "@lexical/yjs@npm:0.7.5"
|
resolution: "@lexical/yjs@npm:0.7.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/offset": 0.7.5
|
"@lexical/offset": 0.7.6
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
yjs: ">=13.5.22"
|
yjs: ">=13.5.22"
|
||||||
checksum: af25e2613072682d316e438d3f6ab81c98e67c009eb8474116478a28e6b7f4e86421752acb4caf33d05294df7f2f5a1f754402c2979c0b499a20fca6a6a0dfd8
|
checksum: 9337f066ad145db85d0cebed61d2938d26136e56630bbf782cca6c351c636b158ca9c4279cf55a7f51fc01dcad1f390d8ca643e8f4e4e1d521f7806ffd6eba9a
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -5464,13 +5464,16 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@standardnotes/blocks-editor@workspace:packages/blocks-editor"
|
resolution: "@standardnotes/blocks-editor@workspace:packages/blocks-editor"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@lexical/react": 0.7.5
|
"@lexical/react": 0.7.6
|
||||||
"@standardnotes/icons": "workspace:*"
|
"@standardnotes/icons": "workspace:*"
|
||||||
"@types/react": ^18.0.26
|
"@types/react": ^18.0.26
|
||||||
"@types/react-dom": ^18.0.9
|
"@types/react-dom": ^18.0.9
|
||||||
eslint: "*"
|
eslint: "*"
|
||||||
lexical: 0.7.5
|
eslint-plugin-react: "*"
|
||||||
|
eslint-plugin-react-hooks: "*"
|
||||||
|
lexical: 0.7.6
|
||||||
prettier: "*"
|
prettier: "*"
|
||||||
|
prettier-plugin-tailwindcss: "*"
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
typescript: "*"
|
typescript: "*"
|
||||||
@@ -6279,7 +6282,7 @@ __metadata:
|
|||||||
"@babel/plugin-transform-react-jsx": ^7.19.0
|
"@babel/plugin-transform-react-jsx": ^7.19.0
|
||||||
"@babel/preset-env": "*"
|
"@babel/preset-env": "*"
|
||||||
"@babel/preset-typescript": ^7.18.6
|
"@babel/preset-typescript": ^7.18.6
|
||||||
"@lexical/react": 0.7.5
|
"@lexical/react": 0.7.6
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.10
|
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.10
|
||||||
"@reach/alert": ^0.18.0
|
"@reach/alert": ^0.18.0
|
||||||
"@reach/alert-dialog": ^0.18.0
|
"@reach/alert-dialog": ^0.18.0
|
||||||
@@ -6335,7 +6338,7 @@ __metadata:
|
|||||||
identity-obj-proxy: ^3.0.0
|
identity-obj-proxy: ^3.0.0
|
||||||
jest: ^29.3.1
|
jest: ^29.3.1
|
||||||
jest-environment-jsdom: ^29.3.1
|
jest-environment-jsdom: ^29.3.1
|
||||||
lexical: 0.7.5
|
lexical: 0.7.6
|
||||||
lint-staged: ">=13"
|
lint-staged: ">=13"
|
||||||
mini-css-extract-plugin: ^2.7.2
|
mini-css-extract-plugin: ^2.7.2
|
||||||
minimatch: ^5.1.1
|
minimatch: ^5.1.1
|
||||||
@@ -6346,7 +6349,7 @@ __metadata:
|
|||||||
postcss: ^8.4.19
|
postcss: ^8.4.19
|
||||||
postcss-loader: ^7.0.2
|
postcss-loader: ^7.0.2
|
||||||
prettier: "*"
|
prettier: "*"
|
||||||
prettier-plugin-tailwindcss: ^0.2.0
|
prettier-plugin-tailwindcss: ^0.2.1
|
||||||
qrcode.react: ^3.1.0
|
qrcode.react: ^3.1.0
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
react-dom: ^18.2.0
|
react-dom: ^18.2.0
|
||||||
@@ -14237,7 +14240,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"eslint-plugin-react-hooks@npm:^4.6.0":
|
"eslint-plugin-react-hooks@npm:*, eslint-plugin-react-hooks@npm:^4.6.0":
|
||||||
version: 4.6.0
|
version: 4.6.0
|
||||||
resolution: "eslint-plugin-react-hooks@npm:4.6.0"
|
resolution: "eslint-plugin-react-hooks@npm:4.6.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -14265,6 +14268,31 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"eslint-plugin-react@npm:*":
|
||||||
|
version: 7.32.0
|
||||||
|
resolution: "eslint-plugin-react@npm:7.32.0"
|
||||||
|
dependencies:
|
||||||
|
array-includes: ^3.1.6
|
||||||
|
array.prototype.flatmap: ^1.3.1
|
||||||
|
array.prototype.tosorted: ^1.1.1
|
||||||
|
doctrine: ^2.1.0
|
||||||
|
estraverse: ^5.3.0
|
||||||
|
jsx-ast-utils: ^2.4.1 || ^3.0.0
|
||||||
|
minimatch: ^3.1.2
|
||||||
|
object.entries: ^1.1.6
|
||||||
|
object.fromentries: ^2.0.6
|
||||||
|
object.hasown: ^1.1.2
|
||||||
|
object.values: ^1.1.6
|
||||||
|
prop-types: ^15.8.1
|
||||||
|
resolve: ^2.0.0-next.4
|
||||||
|
semver: ^6.3.0
|
||||||
|
string.prototype.matchall: ^4.0.8
|
||||||
|
peerDependencies:
|
||||||
|
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
|
||||||
|
checksum: b81ce2623b50a936287d8e21997bd855094e643856c99b42a9f0c10e1c7b123e469c3d75f77df9eefb719fee2b47a763862f1cdca1e7cc26edc7cde2fb8cba87
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"eslint-plugin-react@npm:^7.30.1":
|
"eslint-plugin-react@npm:^7.30.1":
|
||||||
version: 7.31.10
|
version: 7.31.10
|
||||||
resolution: "eslint-plugin-react@npm:7.31.10"
|
resolution: "eslint-plugin-react@npm:7.31.10"
|
||||||
@@ -19573,10 +19601,10 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lexical@npm:0.7.5":
|
"lexical@npm:0.7.6":
|
||||||
version: 0.7.5
|
version: 0.7.6
|
||||||
resolution: "lexical@npm:0.7.5"
|
resolution: "lexical@npm:0.7.6"
|
||||||
checksum: fa6955a6c97b3baf0277c2f873762136c6cc9d639ab8f63cfc7f7f1c1c2a8ab4419e79572c6591b6cdc41c465300998aebbcbfd18a983807b7f6916b6a534cd1
|
checksum: 594423da85fa64842b34f73bca06da80f03c5c61e9cb49db5e8e2c49db9d10f4dfa2e6afbb5edad60a25b43cf7eb24db948284fa503e20507afd3073b0c4050c
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -24663,12 +24691,12 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"prettier-plugin-tailwindcss@npm:^0.2.0":
|
"prettier-plugin-tailwindcss@npm:*, prettier-plugin-tailwindcss@npm:^0.2.1":
|
||||||
version: 0.2.0
|
version: 0.2.1
|
||||||
resolution: "prettier-plugin-tailwindcss@npm:0.2.0"
|
resolution: "prettier-plugin-tailwindcss@npm:0.2.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
prettier: ">=2.2.0"
|
prettier: ">=2.2.0"
|
||||||
checksum: 427cd16e5c664e45965d0742e524ccf9b345bcb91a28d77bf03fa8e249eb0ecf6412ef6e91fb628b7773c8b60b1ed2c47d00211034918ddd4456f27305b9d5e7
|
checksum: 5a04b26f50baea552aaff938b6413bf66d0050c3ca5a0d5bc432a7efc8f8e4ba194a23b1aabd4d39d36228afaf368ba2bce24ebb87f4972459a3679eff6942e8
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -26622,7 +26650,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"resolve@npm:^2.0.0-next.3":
|
"resolve@npm:^2.0.0-next.3, resolve@npm:^2.0.0-next.4":
|
||||||
version: 2.0.0-next.4
|
version: 2.0.0-next.4
|
||||||
resolution: "resolve@npm:2.0.0-next.4"
|
resolution: "resolve@npm:2.0.0-next.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -26648,7 +26676,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
|
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>, resolve@patch:resolve@^2.0.0-next.4#~builtin<compat/resolve>":
|
||||||
version: 2.0.0-next.4
|
version: 2.0.0-next.4
|
||||||
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=07638b"
|
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=07638b"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user