feat:添加表格

This commit is contained in:
shixiaohua 2024-02-29 09:05:34 +08:00
parent b6ea89712c
commit 8dd57ec5b4
18 changed files with 2323 additions and 52 deletions

315
package-lock.json generated
View File

@ -25,7 +25,7 @@
"@electron-forge/maker-rpm": "^6.0.4",
"@electron-forge/maker-squirrel": "^6.0.4",
"@electron-forge/maker-zip": "^6.0.4",
"@lexical/react": "^0.12.2",
"@lexical/react": "^0.12.6",
"@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
@ -39,7 +39,7 @@
"echarts-for-react": "^3.0.2",
"electron": "^22.1.0",
"formik": "^2.2.9",
"lexical": "^0.12.2",
"lexical": "^0.12.6",
"localStorage": "^1.0.4",
"nanoid": "^4.0.2",
"prop-types": "^15.8.1",
@ -52,7 +52,9 @@
"redux-thunk": "^2.4.2",
"umi-request": "^1.4.0",
"wait-on": "^3.3.0",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"y-websocket": ">=1.3.x",
"yjs": ">=13.5.42"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -6953,6 +6955,23 @@
"resolved": "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
"node_modules/abstract-leveldown": {
"version": "6.2.3",
"resolved": "https://registry.npmmirror.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz",
"integrity": "sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ==",
"dev": true,
"optional": true,
"dependencies": {
"buffer": "^5.5.0",
"immediate": "^3.2.3",
"level-concat-iterator": "~2.0.0",
"level-supports": "~1.0.0",
"xtend": "~4.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
@ -7599,6 +7618,13 @@
"node": "*"
}
},
"node_modules/async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/async-limiter/-/async-limiter-1.0.1.tgz",
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
"dev": true,
"optional": true
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
@ -10369,6 +10395,20 @@
"node": ">=10"
}
},
"node_modules/deferred-leveldown": {
"version": "5.3.0",
"resolved": "https://registry.npmmirror.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz",
"integrity": "sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw==",
"dev": true,
"optional": true,
"dependencies": {
"abstract-leveldown": "~6.2.1",
"inherits": "^2.0.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/define-data-property": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.1.tgz",
@ -11419,6 +11459,22 @@
"iconv-lite": "^0.6.2"
}
},
"node_modules/encoding-down": {
"version": "6.3.0",
"resolved": "https://registry.npmmirror.com/encoding-down/-/encoding-down-6.3.0.tgz",
"integrity": "sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw==",
"dev": true,
"optional": true,
"dependencies": {
"abstract-leveldown": "^6.2.1",
"inherits": "^2.0.3",
"level-codec": "^9.0.0",
"level-errors": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.4.tgz",
@ -14167,6 +14223,13 @@
"node": ">=0.10.0"
}
},
"node_modules/immediate": {
"version": "3.3.0",
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.3.0.tgz",
"integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==",
"dev": true,
"optional": true
},
"node_modules/immer": {
"version": "9.0.21",
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
@ -14759,8 +14822,7 @@
"version": "0.2.5",
"resolved": "https://registry.npmmirror.com/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/isstream": {
"version": "0.1.2",
@ -17474,6 +17536,157 @@
"webpack": "^4.0.0 || ^5.0.0"
}
},
"node_modules/level": {
"version": "6.0.1",
"resolved": "https://registry.npmmirror.com/level/-/level-6.0.1.tgz",
"integrity": "sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw==",
"dev": true,
"optional": true,
"dependencies": {
"level-js": "^5.0.0",
"level-packager": "^5.1.0",
"leveldown": "^5.4.0"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/level-codec": {
"version": "9.0.2",
"resolved": "https://registry.npmmirror.com/level-codec/-/level-codec-9.0.2.tgz",
"integrity": "sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ==",
"dev": true,
"optional": true,
"dependencies": {
"buffer": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/level-concat-iterator": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz",
"integrity": "sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw==",
"dev": true,
"optional": true,
"engines": {
"node": ">=6"
}
},
"node_modules/level-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/level-errors/-/level-errors-2.0.1.tgz",
"integrity": "sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw==",
"dev": true,
"optional": true,
"dependencies": {
"errno": "~0.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/level-iterator-stream": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz",
"integrity": "sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q==",
"dev": true,
"optional": true,
"dependencies": {
"inherits": "^2.0.4",
"readable-stream": "^3.4.0",
"xtend": "^4.0.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/level-js": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/level-js/-/level-js-5.0.2.tgz",
"integrity": "sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg==",
"dev": true,
"optional": true,
"dependencies": {
"abstract-leveldown": "~6.2.3",
"buffer": "^5.5.0",
"inherits": "^2.0.3",
"ltgt": "^2.1.2"
}
},
"node_modules/level-packager": {
"version": "5.1.1",
"resolved": "https://registry.npmmirror.com/level-packager/-/level-packager-5.1.1.tgz",
"integrity": "sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ==",
"dev": true,
"optional": true,
"dependencies": {
"encoding-down": "^6.3.0",
"levelup": "^4.3.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/level-supports": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/level-supports/-/level-supports-1.0.1.tgz",
"integrity": "sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg==",
"dev": true,
"optional": true,
"dependencies": {
"xtend": "^4.0.2"
},
"engines": {
"node": ">=6"
}
},
"node_modules/leveldown": {
"version": "5.6.0",
"resolved": "https://registry.npmmirror.com/leveldown/-/leveldown-5.6.0.tgz",
"integrity": "sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"abstract-leveldown": "~6.2.1",
"napi-macros": "~2.0.0",
"node-gyp-build": "~4.1.0"
},
"engines": {
"node": ">=8.6.0"
}
},
"node_modules/leveldown/node_modules/node-gyp-build": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz",
"integrity": "sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ==",
"dev": true,
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/levelup": {
"version": "4.4.0",
"resolved": "https://registry.npmmirror.com/levelup/-/levelup-4.4.0.tgz",
"integrity": "sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ==",
"dev": true,
"optional": true,
"dependencies": {
"deferred-leveldown": "~5.3.0",
"level-errors": "~2.0.0",
"level-iterator-stream": "~4.0.0",
"level-supports": "~1.0.0",
"xtend": "~4.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/leven/-/leven-3.1.0.tgz",
@ -17507,7 +17720,6 @@
"resolved": "https://registry.npmmirror.com/lib0/-/lib0-0.2.88.tgz",
"integrity": "sha512-KyroiEvCeZcZEMx5Ys+b4u4eEBbA1ch7XUaBhYpwa/nPMrzTjUhI4RfcytmQfYoTBPcdyx+FX6WFNIoNuJzJfQ==",
"dev": true,
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
@ -17797,6 +18009,13 @@
"node": ">=10"
}
},
"node_modules/ltgt": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/ltgt/-/ltgt-2.2.1.tgz",
"integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==",
"dev": true,
"optional": true
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz",
@ -18563,6 +18782,13 @@
"node": "^14 || ^16 || >=18"
}
},
"node_modules/napi-macros": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/napi-macros/-/napi-macros-2.0.0.tgz",
"integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==",
"dev": true,
"optional": true
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
@ -26698,6 +26924,82 @@
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
"dev": true
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"dev": true,
"optional": true,
"engines": {
"node": ">=0.4"
}
},
"node_modules/y-leveldb": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/y-leveldb/-/y-leveldb-0.1.2.tgz",
"integrity": "sha512-6ulEn5AXfXJYi89rXPEg2mMHAyyw8+ZfeMMdOtBbV8FJpQ1NOrcgi6DTAcXof0dap84NjHPT2+9d0rb6cFsjEg==",
"dev": true,
"optional": true,
"dependencies": {
"level": "^6.0.1",
"lib0": "^0.2.31"
},
"peerDependencies": {
"yjs": "^13.0.0"
}
},
"node_modules/y-protocols": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/y-protocols/-/y-protocols-1.0.6.tgz",
"integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==",
"dev": true,
"dependencies": {
"lib0": "^0.2.85"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"peerDependencies": {
"yjs": "^13.0.0"
}
},
"node_modules/y-websocket": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/y-websocket/-/y-websocket-1.5.4.tgz",
"integrity": "sha512-Y3021uy0anOIHqAPyAZbNDoR05JuMEGjRNI8c+K9MHzVS8dWoImdJUjccljAznc8H2L7WkIXhRHZ1igWNRSgPw==",
"dev": true,
"dependencies": {
"lib0": "^0.2.52",
"lodash.debounce": "^4.0.8",
"y-protocols": "^1.0.5"
},
"bin": {
"y-websocket": "bin/server.js",
"y-websocket-server": "bin/server.js"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"ws": "^6.2.1",
"y-leveldb": "^0.1.0"
},
"peerDependencies": {
"yjs": "^13.5.6"
}
},
"node_modules/y-websocket/node_modules/ws": {
"version": "6.2.2",
"resolved": "https://registry.npmmirror.com/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"dev": true,
"optional": true,
"dependencies": {
"async-limiter": "~1.0.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
@ -26844,7 +27146,6 @@
"resolved": "https://registry.npmmirror.com/yjs/-/yjs-13.6.10.tgz",
"integrity": "sha512-1JcyQek1vaMyrDm7Fqfa+pvHg/DURSbVo4VmeN7wjnTKB/lZrfIPhdCj7d8sboK6zLfRBJXegTjc9JlaDd8/Zw==",
"dev": true,
"peer": true,
"dependencies": {
"lib0": "^0.2.86"
},

View File

@ -13,7 +13,7 @@
"@electron-forge/maker-rpm": "^6.0.4",
"@electron-forge/maker-squirrel": "^6.0.4",
"@electron-forge/maker-zip": "^6.0.4",
"@lexical/react": "^0.12.2",
"@lexical/react": "^0.12.6",
"@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
@ -27,7 +27,7 @@
"echarts-for-react": "^3.0.2",
"electron": "^22.1.0",
"formik": "^2.2.9",
"lexical": "^0.12.2",
"lexical": "^0.12.6",
"localStorage": "^1.0.4",
"nanoid": "^4.0.2",
"prop-types": "^15.8.1",

View File

@ -0,0 +1,15 @@
export default function invariant(
cond,
message,
...args
){
if (cond) {
return;
}
throw new Error(
'Internal Lexical error: invariant() is meant to be replaced at compile ' +
'time. There is no runtime version. Error: ' +
message,
);
}

View File

@ -1,33 +0,0 @@
// import {WebsocketProvider} from 'y-websocket';
// import {Doc} from 'yjs';
//
// const url = new URL(window.location.href);
// const params = new URLSearchParams(url.search);
// const WEBSOCKET_ENDPOINT =
// params.get('collabEndpoint') || 'ws://localhost:1234';
// const WEBSOCKET_SLUG = 'playground';
// const WEBSOCKET_ID = params.get('collabId') || '0';
//
// export function createWebsocketProvider(
// id,
// yjsDocMap,
// ) {
// let doc = yjsDocMap.get(id);
//
// if (doc === undefined) {
// doc = new Doc();
// yjsDocMap.set(id, doc);
// } else {
// doc.load();
// }
//
// // @ts-ignore
// return new WebsocketProvider(
// WEBSOCKET_ENDPOINT,
// WEBSOCKET_SLUG + '/' + WEBSOCKET_ID + '/' + id,
// doc,
// {
// connect: false,
// },
// );
// }

View File

@ -25,9 +25,11 @@ import SaveFilePlugin from "./plugins/SaveFilePlugin";
import {TabIndentationPlugin} from "@lexical/react/LexicalTabIndentationPlugin";
import UsefulNodes from "./nodes/UsefulNodes";
import ImagesPlugin from "./plugins/ImagesPlugin";
import {TablePlugin} from "./plugins/TablePlugin";
import {HorizontalRulePlugin} from "@lexical/react/LexicalHorizontalRulePlugin"
import InlineImagePlugin from "./plugins/InlineImagePlugin";
import {TablePlugin} from "@lexical/react/LexicalTablePlugin";
import TableCellActionMenuPlugin from './plugins/TableActionMenuPlugin';
function Placeholder() {
return <div className="editor-placeholder">Enter some rich text...</div>;
}
@ -71,7 +73,15 @@ export default function Hlexical(props) {
<LinkPlugin/>
<AutoLinkPlugin/>
<ListMaxIndentLevelPlugin maxDepth={7}/>
<TablePlugin/>
{/* 表格操作 */}
<TablePlugin
hasCellMerge={true}
hasCellBackgroundColor={true}
/>
{/* 表格单元格操作 */}
<TableCellActionMenuPlugin/>
<TabIndentationPlugin />
{/*markdown 快捷键*/}
<MarkdownShortcutPlugin transformers={TRANSFORMERS}/>

View File

@ -749,4 +749,164 @@ body {
i.justify-align {
background-image: url(images/icons/justify.svg);
}
.editor-table {
border-collapse: collapse;
border-spacing: 0;
overflow-y: scroll;
overflow-x: scroll;
table-layout: fixed;
width: max-content;
margin: 30px 0;
}
.editor-tableSelection *::selection {
background-color: transparent;
}
.editor-tableSelected {
outline: 2px solid rgb(60, 132, 244);
}
.editor-tableCell {
border: 1px solid #bbb;
width: 75px;
min-width: 75px;
vertical-align: top;
text-align: start;
padding: 6px 8px;
position: relative;
outline: none;
}
.editor-tableCellSortedIndicator {
display: block;
opacity: 0.5;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 4px;
background-color: #999;
}
.editor-tableCellResizer {
position: absolute;
right: -4px;
height: 100%;
width: 8px;
cursor: ew-resize;
z-index: 10;
top: 0;
}
.editor-tableCellHeader {
background-color: #f2f3f5;
text-align: start;
}
.editor-tableCellSelected {
background-color: #c9dbf0;
}
.editor-tableCellPrimarySelected {
border: 2px solid rgb(60, 132, 244);
display: block;
height: calc(100% - 2px);
position: absolute;
width: calc(100% - 2px);
left: -1px;
top: -1px;
z-index: 2;
}
.editor-tableCellEditing {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.4);
border-radius: 3px;
}
.editor-tableAddColumns {
position: absolute;
top: 0;
width: 20px;
background-color: #eee;
height: 100%;
right: -25px;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
}
.editor-tableAddColumns:after {
background-image: url(images/icons/plus.svg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
display: block;
content: ' ';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.4;
}
.editor-tableAddColumns:hover {
background-color: #c9dbf0;
}
.editor-tableAddRows {
position: absolute;
bottom: -25px;
width: calc(100% - 25px);
background-color: #eee;
height: 20px;
left: 0;
animation: table-controls 0.2s ease;
border: 0;
cursor: pointer;
}
.editor-tableAddRows:after {
background-image: url(images/icons/plus.svg);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
display: block;
content: ' ';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.4;
}
.editor-tableAddRows:hover {
background-color: #c9dbf0;
}
@keyframes table-controls {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.editor-tableCellResizeRuler {
display: block;
position: absolute;
width: 1px;
background-color: rgb(60, 132, 244);
height: 100%;
top: 0;
}
.editor-tableCellActionButtonContainer {
display: block;
right: 5px;
top: 6px;
position: absolute;
z-index: 4;
width: 20px;
height: 20px;
}
.editor-tableCellActionButton {
background-color: #eee;
display: block;
border: 0;
border-radius: 20px;
width: 20px;
height: 20px;
color: #222;
cursor: pointer;
}
.editor-tableCellActionButton:hover {
background-color: #ddd;
}

View File

@ -686,10 +686,8 @@ export default function TableComponent({
_rows.unshift(rawRows[0]);
return _rows;
}, [rawRows, sortingOptions]);
const [primarySelectedCellID, setPrimarySelectedCellID] = useState<
null | string
>(null);
const cellEditor = useMemo<null | LexicalEditor>(() => {
const [primarySelectedCellID, setPrimarySelectedCellID] = useState(null);
const cellEditor = useMemo(() => {
if (cellEditorConfig === null) {
return null;
}

View File

@ -1,11 +1,11 @@
import {HeadingNode, QuoteNode} from "@lexical/rich-text";
import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
import {ListItemNode, ListNode} from "@lexical/list";
import {CodeHighlightNode, CodeNode, $createCodeNode, $isCodeNode} from "@lexical/code";
import {AutoLinkNode, LinkNode} from "@lexical/link";
import {ImageNode} from "./ImageNode";
import {HorizontalRuleNode} from "@lexical/react/LexicalHorizontalRuleNode";
import {InlineImageNode} from "./InlineImageNode";
import {TableNode,TableCellNode, TableRowNode} from "@lexical/table";
const UsefulNodes=[
HeadingNode,

View File

@ -0,0 +1,307 @@
import './index.less';
import {useEffect, useMemo, useRef, useState} from 'react';
import * as React from 'react';
import TextInput from '../TextInput';
const basicColors = [
'#d0021b',
'#f5a623',
'#f8e71c',
'#8b572a',
'#7ed321',
'#417505',
'#bd10e0',
'#9013fe',
'#4a90e2',
'#50e3c2',
'#b8e986',
'#000000',
'#4a4a4a',
'#9b9b9b',
'#ffffff',
];
const WIDTH = 214;
const HEIGHT = 150;
export default function ColorPicker({
color,
onChange,
}) {
const [selfColor, setSelfColor] = useState(transformColor('hex', color));
const [inputColor, setInputColor] = useState(color);
const innerDivRef = useRef(null);
const saturationPosition = useMemo(
() => ({
x: (selfColor.hsv.s / 100) * WIDTH,
y: ((100 - selfColor.hsv.v) / 100) * HEIGHT,
}),
[selfColor.hsv.s, selfColor.hsv.v],
);
const huePosition = useMemo(
() => ({
x: (selfColor.hsv.h / 360) * WIDTH,
}),
[selfColor.hsv],
);
const onSetHex = (hex) => {
setInputColor(hex);
if (/^#[0-9A-Fa-f]{6}$/i.test(hex)) {
const newColor = transformColor('hex', hex);
setSelfColor(newColor);
}
};
const onMoveSaturation = ({x, y}) => {
const newHsv = {
...selfColor.hsv,
s: (x / WIDTH) * 100,
v: 100 - (y / HEIGHT) * 100,
};
const newColor = transformColor('hsv', newHsv);
setSelfColor(newColor);
setInputColor(newColor.hex);
};
const onMoveHue = ({x}) => {
const newHsv = {...selfColor.hsv, h: (x / WIDTH) * 360};
const newColor = transformColor('hsv', newHsv);
setSelfColor(newColor);
setInputColor(newColor.hex);
};
useEffect(() => {
// Check if the dropdown is actually active
if (innerDivRef.current !== null && onChange) {
onChange(selfColor.hex);
setInputColor(selfColor.hex);
}
}, [selfColor, onChange]);
useEffect(() => {
if (color === undefined) return;
const newColor = transformColor('hex', color);
setSelfColor(newColor);
setInputColor(newColor.hex);
}, [color]);
return (
<div
className="color-picker-wrapper"
style={{width: WIDTH}}
ref={innerDivRef}>
<TextInput label="Hex" onChange={onSetHex} value={inputColor} />
<div className="color-picker-basic-color">
{basicColors.map((basicColor) => (
<button
className={basicColor === selfColor.hex ? ' active' : ''}
key={basicColor}
style={{backgroundColor: basicColor}}
onClick={() => {
setInputColor(basicColor);
setSelfColor(transformColor('hex', basicColor));
}}
/>
))}
</div>
<MoveWrapper
className="color-picker-saturation"
style={{backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`}}
onChange={onMoveSaturation}>
<div
className="color-picker-saturation_cursor"
style={{
backgroundColor: selfColor.hex,
left: saturationPosition.x,
top: saturationPosition.y,
}}
/>
</MoveWrapper>
<MoveWrapper className="color-picker-hue" onChange={onMoveHue}>
<div
className="color-picker-hue_cursor"
style={{
backgroundColor: `hsl(${selfColor.hsv.h}, 100%, 50%)`,
left: huePosition.x,
}}
/>
</MoveWrapper>
<div
className="color-picker-color"
style={{backgroundColor: selfColor.hex}}
/>
</div>
);
}
function MoveWrapper({className, style, onChange, children}) {
const divRef = useRef(null);
const move = (e) => {
if (divRef.current) {
const {current: div} = divRef;
const {width, height, left, top} = div.getBoundingClientRect();
const x = clamp(e.clientX - left, width, 0);
const y = clamp(e.clientY - top, height, 0);
onChange({x, y});
}
};
const onMouseDown = (e) => {
if (e.button !== 0) return;
move(e);
const onMouseMove = (_e) => {
move(_e);
};
const onMouseUp = (_e) => {
document.removeEventListener('mousemove', onMouseMove, false);
document.removeEventListener('mouseup', onMouseUp, false);
move(_e);
};
document.addEventListener('mousemove', onMouseMove, false);
document.addEventListener('mouseup', onMouseUp, false);
};
return (
<div
ref={divRef}
className={className}
style={style}
onMouseDown={onMouseDown}>
{children}
</div>
);
}
function clamp(value, max, min) {
return value > max ? max : value < min ? min : value;
}
export function toHex(value) {
if (!value.startsWith('#')) {
const ctx = document.createElement('canvas').getContext('2d');
if (!ctx) {
throw new Error('2d context not supported or canvas already initialized');
}
ctx.fillStyle = value;
return ctx.fillStyle;
} else if (value.length === 4 || value.length === 5) {
value = value
.split('')
.map((v, i) => (i ? v + v : '#'))
.join('');
return value;
} else if (value.length === 7 || value.length === 9) {
return value;
}
return '#000000';
}
function hex2rgb(hex) {
const rbgArr = (
hex
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => '#' + r + r + g + g + b + b,
)
.substring(1)
.match(/.{2}/g) || []
).map((x) => parseInt(x, 16));
return {
b: rbgArr[2],
g: rbgArr[1],
r: rbgArr[0],
};
}
function rgb2hsv({r, g, b}) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const d = max - Math.min(r, g, b);
const h = d
? (max === r
? (g - b) / d + (g < b ? 6 : 0)
: max === g
? 2 + (b - r) / d
: 4 + (r - g) / d) * 60
: 0;
const s = max ? (d / max) * 100 : 0;
const v = max * 100;
return {h, s, v};
}
function hsv2rgb({h, s, v}) {
s /= 100;
v /= 100;
const i = ~~(h / 60);
const f = h / 60 - i;
const p = v * (1 - s);
const q = v * (1 - s * f);
const t = v * (1 - s * (1 - f));
const index = i % 6;
const r = Math.round([v, q, p, p, t, v][index] * 255);
const g = Math.round([t, v, v, q, p, p][index] * 255);
const b = Math.round([p, p, t, v, v, q][index] * 255);
return {b, g, r};
}
function rgb2hex({b, g, r}) {
return '#' + [r, g, b].map((x) => x.toString(16).padStart(2, '0')).join('');
}
function transformColor(
format,
color,
) {
let hex = toHex('#121212');
let rgb = hex2rgb(hex);
let hsv = rgb2hsv(rgb);
if (format === 'hex') {
const value = color;
hex = toHex(value);
rgb = hex2rgb(hex);
hsv = rgb2hsv(rgb);
} else if (format === 'rgb') {
const value = color;
rgb = value;
hex = rgb2hex(rgb);
hsv = rgb2hsv(rgb);
} else if (format === 'hsv') {
const value = color;
hsv = value;
rgb = hsv2rgb(hsv);
hex = rgb2hex(rgb);
}
return {hex, hsv, rgb};
}

View File

@ -0,0 +1,88 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
.color-picker-wrapper {
padding: 20px;
}
.color-picker-basic-color {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 0;
padding: 0;
}
.color-picker-basic-color button {
border: 1px solid #ccc;
border-radius: 4px;
height: 16px;
width: 16px;
cursor: pointer;
list-style-type: none;
}
.color-picker-basic-color button.active {
box-shadow: 0px 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.color-picker-saturation {
width: 100%;
position: relative;
margin-top: 15px;
height: 150px;
background-image: linear-gradient(transparent, black),
linear-gradient(to right, white, transparent);
user-select: none;
}
.color-picker-saturation_cursor {
position: absolute;
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: 0 0 15px #00000026;
box-sizing: border-box;
transform: translate(-10px, -10px);
}
.color-picker-hue {
width: 100%;
position: relative;
margin-top: 15px;
height: 12px;
background-image: linear-gradient(
to right,
rgb(255, 0, 0),
rgb(255, 255, 0),
rgb(0, 255, 0),
rgb(0, 255, 255),
rgb(0, 0, 255),
rgb(255, 0, 255),
rgb(255, 0, 0)
);
user-select: none;
border-radius: 12px;
}
.color-picker-hue_cursor {
position: absolute;
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: #0003 0 0 0 0.5px;
box-sizing: border-box;
transform: translate(-10px, -4px);
}
.color-picker-color {
border: 1px solid #ccc;
margin-top: 15px;
width: 100%;
height: 20px;
}

View File

@ -0,0 +1,710 @@
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
import "./index.less"
import {
$deleteTableColumn__EXPERIMENTAL,
$deleteTableRow__EXPERIMENTAL,
$getTableCellNodeFromLexicalNode,
$getTableColumnIndexFromTableCellNode,
$getTableNodeFromLexicalNodeOrThrow,
$getTableRowIndexFromTableCellNode,
$insertTableColumn__EXPERIMENTAL,
$insertTableRow__EXPERIMENTAL,
$isTableCellNode,
$isTableRowNode,
$unmergeCell,
getTableSelectionFromTableElement,
$isGridSelection,
TableCellHeaderStates,
TableCellNode,
} from '@lexical/table';
import {
$createParagraphNode,
$getRoot,
$getSelection,
$isElementNode,
$isParagraphNode,
$isRangeSelection,
$isTextNode,
DEPRECATED_$getNodeTriplet,
DEPRECATED_$isGridCellNode,
} from 'lexical';
import * as React from 'react';
import {ReactPortal, useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
import invariant from '../../context/shared/invariant';
import useModal from '../../hook/userModal';
import ColorPicker from '../../plugins/Input/ColorPicker';
function computeSelectionCount(selection) {
const selectionShape = selection.getShape();
return {
columns: selectionShape.toX - selectionShape.fromX + 1,
rows: selectionShape.toY - selectionShape.fromY + 1,
};
}
function isGridSelectionRectangular(selection) {
const nodes = selection.getNodes();
const currentRows = [];
let currentRow = null;
let expectedColumns = null;
let currentColumns = 0;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if ($isTableCellNode(node)) {
const row = node.getParentOrThrow();
invariant(
$isTableRowNode(row),
'Expected CellNode to have a RowNode parent',
);
if (currentRow !== row) {
if (expectedColumns !== null && currentColumns !== expectedColumns) {
return false;
}
if (currentRow !== null) {
expectedColumns = currentColumns;
}
currentRow = row;
currentColumns = 0;
}
const colSpan = node.__colSpan;
for (let j = 0; j < colSpan; j++) {
if (currentRows[currentColumns + j] === undefined) {
currentRows[currentColumns + j] = 0;
}
currentRows[currentColumns + j] += node.__rowSpan;
}
currentColumns += colSpan;
}
}
return (
(expectedColumns === null || currentColumns === expectedColumns) &&
currentRows.every((v) => v === currentRows[0])
);
}
function $canUnmerge() {
const selection = $getSelection();
if (
($isRangeSelection(selection) && !selection.isCollapsed()) ||
($isGridSelection(selection) &&
!selection.anchor.is(selection.focus)) ||
(!$isRangeSelection(selection) && !$isGridSelection(selection))
) {
return false;
}
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor);
return cell.__colSpan > 1 || cell.__rowSpan > 1;
}
function $cellContainsEmptyParagraph(cell) {
if (cell.getChildrenSize() !== 1) {
return false;
}
const firstChild = cell.getFirstChildOrThrow();
if (!$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
return false;
}
return true;
}
function $selectLastDescendant(node) {
const lastDescendant = node.getLastDescendant();
if ($isTextNode(lastDescendant)) {
lastDescendant.select();
} else if ($isElementNode(lastDescendant)) {
lastDescendant.selectEnd();
} else if (lastDescendant !== null) {
lastDescendant.selectNext();
}
}
function currentCellBackgroundColor(editor) {
return editor.getEditorState().read(() => {
const selection = $getSelection();
if (
$isRangeSelection(selection) || $isGridSelection(selection)
) {
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor);
if ($isTableCellNode(cell)) {
return cell.getBackgroundColor();
}
}
return null;
});
}
function TableActionMenu({
onClose,
tableCellNodeParam,
setIsMenuOpen,
contextRef,
cellMerge,
showColorPickerModal,
}) {
const [editor] = useLexicalComposerContext();
const dropDownRef = useRef(null);
const [tableCellNode, updateTableCellNode] = useState(tableCellNodeParam);
const [selectionCounts, updateSelectionCounts] = useState({
columns: 1,
rows: 1,
});
const [canMergeCells, setCanMergeCells] = useState(false);
const [canUnmergeCell, setCanUnmergeCell] = useState(false);
const [backgroundColor, setBackgroundColor] = useState(
() => currentCellBackgroundColor(editor) || '',
);
useEffect(() => {
return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
const nodeUpdated =
nodeMutations.get(tableCellNode.getKey()) === 'updated';
if (nodeUpdated) {
editor.getEditorState().read(() => {
updateTableCellNode(tableCellNode.getLatest());
});
setBackgroundColor(currentCellBackgroundColor(editor) || '');
}
});
}, [editor, tableCellNode]);
useEffect(() => {
editor.getEditorState().read(() => {
const selection = $getSelection();
if ($isGridSelection(selection)) {
const currentSelectionCounts = computeSelectionCount(selection);
updateSelectionCounts(computeSelectionCount(selection));
setCanMergeCells(
isGridSelectionRectangular(selection) &&
(currentSelectionCounts.columns > 1 ||
currentSelectionCounts.rows > 1),
);
}
setCanUnmergeCell($canUnmerge());
});
}, [editor]);
useEffect(() => {
const menuButtonElement = contextRef.current;
const dropDownElement = dropDownRef.current;
const rootElement = editor.getRootElement();
if (
menuButtonElement != null &&
dropDownElement != null &&
rootElement != null
) {
const rootEleRect = rootElement.getBoundingClientRect();
const menuButtonRect = menuButtonElement.getBoundingClientRect();
dropDownElement.style.opacity = '1';
const dropDownElementRect = dropDownElement.getBoundingClientRect();
const margin = 5;
let leftPosition = menuButtonRect.right + margin;
if (
leftPosition + dropDownElementRect.width > window.innerWidth ||
leftPosition + dropDownElementRect.width > rootEleRect.right
) {
const position =
menuButtonRect.left - dropDownElementRect.width - margin;
leftPosition = (position < 0 ? margin : position) + window.pageXOffset;
}
dropDownElement.style.left = `${leftPosition + window.pageXOffset}px`;
let topPosition = menuButtonRect.top;
if (topPosition + dropDownElementRect.height > window.innerHeight) {
const position = menuButtonRect.bottom - dropDownElementRect.height;
topPosition = (position < 0 ? margin : position) + window.pageYOffset;
}
dropDownElement.style.top = `${topPosition + +window.pageYOffset}px`;
}
}, [contextRef, dropDownRef, editor]);
useEffect(() => {
function handleClickOutside(event) {
if (
dropDownRef.current != null &&
contextRef.current != null &&
!dropDownRef.current.contains(event.target) &&
!contextRef.current.contains(event.target)
) {
setIsMenuOpen(false);
}
}
window.addEventListener('click', handleClickOutside);
return () => window.removeEventListener('click', handleClickOutside);
}, [setIsMenuOpen, contextRef]);
const clearTableSelection = useCallback(() => {
editor.update(() => {
if (tableCellNode.isAttached()) {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableElement = editor.getElementByKey(
tableNode.getKey(),
);
if (!tableElement) {
throw new Error('Expected to find tableElement in DOM');
}
const tableSelection = getTableSelectionFromTableElement(tableElement);
if (tableSelection !== null) {
tableSelection.clearHighlight();
}
tableNode.markDirty();
updateTableCellNode(tableCellNode.getLatest());
}
const rootNode = $getRoot();
rootNode.selectStart();
});
}, [editor, tableCellNode]);
const mergeTableCellsAtSelection = () => {
editor.update(() => {
const selection = $getSelection();
if ($isGridSelection(selection)) {
const {columns, rows} = computeSelectionCount(selection);
const nodes = selection.getNodes();
let firstCell = null;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (DEPRECATED_$isGridCellNode(node)) {
if (firstCell === null) {
node.setColSpan(columns).setRowSpan(rows);
firstCell = node;
const isEmpty = $cellContainsEmptyParagraph(node);
let firstChild;
if (
isEmpty &&
$isParagraphNode((firstChild = node.getFirstChild()))
) {
firstChild.remove();
}
} else if (DEPRECATED_$isGridCellNode(firstCell)) {
const isEmpty = $cellContainsEmptyParagraph(node);
if (!isEmpty) {
firstCell.append(...node.getChildren());
}
node.remove();
}
}
}
if (firstCell !== null) {
if (firstCell.getChildrenSize() === 0) {
firstCell.append($createParagraphNode());
}
$selectLastDescendant(firstCell);
}
onClose();
}
});
};
const unmergeTableCellsAtSelection = () => {
editor.update(() => {
$unmergeCell();
});
};
const insertTableRowAtSelection = useCallback(
(shouldInsertAfter) => {
editor.update(() => {
$insertTableRow__EXPERIMENTAL(shouldInsertAfter);
onClose();
});
},
[editor, onClose],
);
const insertTableColumnAtSelection = useCallback(
(shouldInsertAfter) => {
editor.update(() => {
for (let i = 0; i < selectionCounts.columns; i++) {
$insertTableColumn__EXPERIMENTAL(shouldInsertAfter);
}
onClose();
});
},
[editor, onClose, selectionCounts.columns],
);
const deleteTableRowAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableRow__EXPERIMENTAL();
onClose();
});
}, [editor, onClose]);
const deleteTableAtSelection = useCallback(() => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
tableNode.remove();
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);
const deleteTableColumnAtSelection = useCallback(() => {
editor.update(() => {
$deleteTableColumn__EXPERIMENTAL();
onClose();
});
}, [editor, onClose]);
const toggleTableRowIsHeader = useCallback(() => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode);
const tableRows = tableNode.getChildren();
if (tableRowIndex >= tableRows.length || tableRowIndex < 0) {
throw new Error('Expected table cell to be inside of table row.');
}
const tableRow = tableRows[tableRowIndex];
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row');
}
tableRow.getChildren().forEach((tableCell) => {
if (!$isTableCellNode(tableCell)) {
throw new Error('Expected table cell');
}
tableCell.toggleHeaderStyle(TableCellHeaderStates.ROW);
});
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);
const toggleTableColumnIsHeader = useCallback(() => {
editor.update(() => {
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableColumnIndex =
$getTableColumnIndexFromTableCellNode(tableCellNode);
const tableRows = tableNode.getChildren();
for (let r = 0; r < tableRows.length; r++) {
const tableRow = tableRows[r];
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row');
}
const tableCells = tableRow.getChildren();
if (tableColumnIndex >= tableCells.length || tableColumnIndex < 0) {
throw new Error('Expected table cell to be inside of table row.');
}
const tableCell = tableCells[tableColumnIndex];
if (!$isTableCellNode(tableCell)) {
throw new Error('Expected table cell');
}
tableCell.toggleHeaderStyle(TableCellHeaderStates.COLUMN);
}
clearTableSelection();
onClose();
});
}, [editor, tableCellNode, clearTableSelection, onClose]);
const handleCellBackgroundColor = useCallback(
(value) => {
editor.update(() => {
const selection = $getSelection();
if (
$isRangeSelection(selection) || $isGridSelection(selection)
) {
const [cell] = DEPRECATED_$getNodeTriplet(selection.anchor);
if ($isTableCellNode(cell)) {
cell.setBackgroundColor(value);
}
}
});
},
[editor],
);
let mergeCellButton = null;
if (cellMerge) {
if (canMergeCells) {
mergeCellButton = (
<button
className="item"
onClick={() => mergeTableCellsAtSelection()}
data-test-id="table-merge-cells">
Merge cells
</button>
);
} else if (canUnmergeCell) {
mergeCellButton = (
<button
className="item"
onClick={() => unmergeTableCellsAtSelection()}
data-test-id="table-unmerge-cells">
Unmerge cells
</button>
);
}
}
return createPortal(
<div
className="dropdown"
ref={dropDownRef}
onClick={(e) => {
e.stopPropagation();
}}>
{mergeCellButton}
<button
className="item"
onClick={() =>
showColorPickerModal('Cell background color', () => (
<ColorPicker
color={backgroundColor}
onChange={handleCellBackgroundColor}
/>
))
}
data-test-id="table-background-color">
<span className="text">设置背景色</span>
</button>
<hr />
<button
className="item"
onClick={() => insertTableRowAtSelection(false)}
data-test-id="table-insert-row-above">
<span className="text">
顶部插入{`${selectionCounts.rows}`}
</span>
</button>
<button
className="item"
onClick={() => insertTableRowAtSelection(true)}
data-test-id="table-insert-row-below">
<span className="text">
底部插入{`${selectionCounts.rows}`}
</span>
</button>
<hr />
<button
className="item"
onClick={() => insertTableColumnAtSelection(false)}
data-test-id="table-insert-column-before">
<span className="text">
左侧插入{`${selectionCounts.columns}`}
</span>
</button>
<button
className="item"
onClick={() => insertTableColumnAtSelection(true)}
data-test-id="table-insert-column-after">
<span className="text">
右侧插入{`${selectionCounts.columns}`}
</span>
</button>
<hr />
<button
className="item"
onClick={() => deleteTableColumnAtSelection()}
data-test-id="table-delete-columns">
<span className="text">删除列</span>
</button>
<button
className="item"
onClick={() => deleteTableRowAtSelection()}
data-test-id="table-delete-rows">
<span className="text">删除行</span>
</button>
<button
className="item"
onClick={() => deleteTableAtSelection()}
data-test-id="table-delete">
<span className="text">删除表格</span>
</button>
<hr />
<button className="item" onClick={() => toggleTableRowIsHeader()}>
<span className="text">
{(tableCellNode.__headerState & TableCellHeaderStates.ROW) ===
TableCellHeaderStates.ROW
? '删除'
: '添加'}{' '}
行表头
</span>
</button>
<button className="item" onClick={() => toggleTableColumnIsHeader()}>
<span className="text">
{(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) ===
TableCellHeaderStates.COLUMN
? '删除'
: '添加'}{' '}
列表头
</span>
</button>
</div>,
document.body,
);
}
function TableCellActionMenuContainer({
anchorElem,
cellMerge,
}) {
const [editor] = useLexicalComposerContext();
const menuButtonRef = useRef(null);
const menuRootRef = useRef(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [tableCellNode, setTableMenuCellNode] = useState(null,);
const [colorPickerModal, showColorPickerModal] = useModal();
const moveMenu = useCallback(() => {
const menu = menuButtonRef.current;
const selection = $getSelection();
const nativeSelection = window.getSelection();
const activeElement = document.activeElement;
if (selection == null || menu == null) {
setTableMenuCellNode(null);
return;
}
const rootElement = editor.getRootElement();
if (
$isRangeSelection(selection) &&
rootElement !== null &&
nativeSelection !== null &&
rootElement.contains(nativeSelection.anchorNode)
) {
const tableCellNodeFromSelection = $getTableCellNodeFromLexicalNode(
selection.anchor.getNode(),
);
if (tableCellNodeFromSelection == null) {
setTableMenuCellNode(null);
return;
}
const tableCellParentNodeDOM = editor.getElementByKey(
tableCellNodeFromSelection.getKey(),
);
if (tableCellParentNodeDOM == null) {
setTableMenuCellNode(null);
return;
}
setTableMenuCellNode(tableCellNodeFromSelection);
} else if (!activeElement) {
setTableMenuCellNode(null);
}
}, [editor]);
useEffect(() => {
return editor.registerUpdateListener(() => {
editor.getEditorState().read(() => {
moveMenu();
});
});
});
useEffect(() => {
const menuButtonDOM = menuButtonRef.current;
if (menuButtonDOM != null && tableCellNode != null) {
const tableCellNodeDOM = editor.getElementByKey(tableCellNode.getKey());
if (tableCellNodeDOM != null) {
const tableCellRect = tableCellNodeDOM.getBoundingClientRect();
const menuRect = menuButtonDOM.getBoundingClientRect();
const anchorRect = anchorElem.getBoundingClientRect();
const top = tableCellRect.top - anchorRect.top + 4;
const left =
tableCellRect.right - menuRect.width - 10 - anchorRect.left;
menuButtonDOM.style.opacity = '1';
menuButtonDOM.style.transform = `translate(${left}px, ${top}px)`;
} else {
menuButtonDOM.style.opacity = '0';
menuButtonDOM.style.transform = 'translate(-10000px, -10000px)';
}
}
}, [menuButtonRef, tableCellNode, editor, anchorElem]);
const prevTableCellDOM = useRef(tableCellNode);
useEffect(() => {
if (prevTableCellDOM.current !== tableCellNode) {
setIsMenuOpen(false);
}
prevTableCellDOM.current = tableCellNode;
}, [prevTableCellDOM, tableCellNode]);
return (
<div className="table-cell-action-button-container" ref={menuButtonRef}>
{tableCellNode != null && (
<>
<button
className="table-cell-action-button chevron-down"
onClick={(e) => {
e.stopPropagation();
setIsMenuOpen(!isMenuOpen);
}}
ref={menuRootRef}>
<i className="chevron-down" />
</button>
{colorPickerModal}
{isMenuOpen && (
<TableActionMenu
contextRef={menuRootRef}
setIsMenuOpen={setIsMenuOpen}
onClose={() => setIsMenuOpen(false)}
tableCellNodeParam={tableCellNode}
cellMerge={cellMerge}
showColorPickerModal={showColorPickerModal}
/>
)}
</>
)}
</div>
);
}
export default function TableActionMenuPlugin({
anchorElem = document.body,
cellMerge = false,
}){
const isEditable = useLexicalEditable();
return createPortal(
isEditable ? (
<TableCellActionMenuContainer
anchorElem={anchorElem}
cellMerge={cellMerge}
/>
) : null,
anchorElem,
);
}

View File

@ -0,0 +1,6 @@
.table-cell-action-button-container {
position: absolute;
top: 0;
left: 0;
will-change: transform;
}

View File

@ -0,0 +1,3 @@
.TableCellResizer__resizer {
position: absolute;
}

View File

@ -0,0 +1,407 @@
import './index.css';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import useLexicalEditable from '@lexical/react/useLexicalEditable';
import {
$getTableColumnIndexFromTableCellNode,
$getTableNodeFromLexicalNodeOrThrow,
$getTableRowIndexFromTableCellNode,
$isTableCellNode,
$isTableRowNode,
getCellFromTarget,
} from '@lexical/table';
import {
$getNearestNodeFromDOMNode,
$getSelection,
COMMAND_PRIORITY_HIGH,
DEPRECATED_$isGridSelection,
SELECTION_CHANGE_COMMAND,
} from 'lexical';
import * as React from 'react';
import {
MouseEventHandler,
ReactPortal,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {createPortal} from 'react-dom';
const MIN_ROW_HEIGHT = 33;
const MIN_COLUMN_WIDTH = 50;
function TableCellResizer({editor}){
const targetRef = useRef(null);
const resizerRef = useRef(null);
const tableRectRef = useRef(null);
const mouseStartPosRef = useRef(null);
const [mouseCurrentPos, updateMouseCurrentPos] =
useState(null);
const [activeCell, updateActiveCell] = useState(null);
const [isSelectingGrid, updateIsSelectingGrid] = useState(false);
const [draggingDirection, updateDraggingDirection] =
useState(null);
useEffect(() => {
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(payload) => {
const selection = $getSelection();
const isGridSelection = DEPRECATED_$isGridSelection(selection);
if (isSelectingGrid !== isGridSelection) {
updateIsSelectingGrid(isGridSelection);
}
return false;
},
COMMAND_PRIORITY_HIGH,
);
});
const resetState = useCallback(() => {
updateActiveCell(null);
targetRef.current = null;
updateDraggingDirection(null);
mouseStartPosRef.current = null;
tableRectRef.current = null;
}, []);
useEffect(() => {
const onMouseMove = (event) => {
setTimeout(() => {
const target = event.target;
if (draggingDirection) {
updateMouseCurrentPos({
x: event.clientX,
y: event.clientY,
});
return;
}
if (resizerRef.current && resizerRef.current.contains(target)) {
return;
}
if (targetRef.current !== target) {
targetRef.current = target;
const cell = getCellFromTarget(target);
if (cell && activeCell !== cell) {
editor.update(() => {
const tableCellNode = $getNearestNodeFromDOMNode(cell.elem);
if (!tableCellNode) {
throw new Error('TableCellResizer: Table cell node not found.');
}
const tableNode =
$getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableElement = editor.getElementByKey(tableNode.getKey());
if (!tableElement) {
throw new Error('TableCellResizer: Table element not found.');
}
targetRef.current = target;
tableRectRef.current = tableElement.getBoundingClientRect();
updateActiveCell(cell);
});
} else if (cell == null) {
resetState();
}
}
}, 0);
};
document.addEventListener('mousemove', onMouseMove);
return () => {
document.removeEventListener('mousemove', onMouseMove);
};
}, [activeCell, draggingDirection, editor, resetState]);
const isHeightChanging = (direction) => {
if (direction === 'bottom') return true;
return false;
};
const updateRowHeight = useCallback(
(newHeight) => {
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
editor.update(() => {
const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem);
if (!$isTableCellNode(tableCellNode)) {
throw new Error('TableCellResizer: Table cell node not found.');
}
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableRowIndex = $getTableRowIndexFromTableCellNode(tableCellNode);
const tableRows = tableNode.getChildren();
if (tableRowIndex >= tableRows.length || tableRowIndex < 0) {
throw new Error('Expected table cell to be inside of table row.');
}
const tableRow = tableRows[tableRowIndex];
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row');
}
tableRow.setHeight(newHeight);
});
},
[activeCell, editor],
);
const updateColumnWidth = useCallback(
(newWidth) => {
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
editor.update(() => {
const tableCellNode = $getNearestNodeFromDOMNode(activeCell.elem);
if (!$isTableCellNode(tableCellNode)) {
throw new Error('TableCellResizer: Table cell node not found.');
}
const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
const tableColumnIndex =
$getTableColumnIndexFromTableCellNode(tableCellNode);
const tableRows = tableNode.getChildren();
for (let r = 0; r < tableRows.length; r++) {
const tableRow = tableRows[r];
if (!$isTableRowNode(tableRow)) {
throw new Error('Expected table row');
}
const rowCells = tableRow.getChildren();
const rowCellsSpan = rowCells.map((cell) => cell.getColSpan());
const aggregatedRowSpans = rowCellsSpan.reduce(
(rowSpans, cellSpan) => {
const previousCell = rowSpans[rowSpans.length - 1] ?? 0;
rowSpans.push(previousCell + cellSpan);
return rowSpans;
},
[],
);
const rowColumnIndexWithSpan = aggregatedRowSpans.findIndex(
(cellSpan) => cellSpan > tableColumnIndex,
);
if (
rowColumnIndexWithSpan >= rowCells.length ||
rowColumnIndexWithSpan < 0
) {
throw new Error('Expected table cell to be inside of table row.');
}
const tableCell = rowCells[rowColumnIndexWithSpan];
if (!$isTableCellNode(tableCell)) {
throw new Error('Expected table cell');
}
tableCell.setWidth(newWidth);
}
});
},
[activeCell, editor],
);
const mouseUpHandler = useCallback(
(direction) => {
const handler = (event) => {
event.preventDefault();
event.stopPropagation();
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
if (mouseStartPosRef.current) {
const {x, y} = mouseStartPosRef.current;
if (activeCell === null) {
return;
}
if (isHeightChanging(direction)) {
const height = activeCell.elem.getBoundingClientRect().height;
const heightChange = Math.abs(event.clientY - y);
const isShrinking = direction === 'bottom' && y > event.clientY;
updateRowHeight(
Math.max(
isShrinking ? height - heightChange : heightChange + height,
MIN_ROW_HEIGHT,
),
);
} else {
const computedStyle = getComputedStyle(activeCell.elem);
let width = activeCell.elem.clientWidth; // width with padding
width -=
parseFloat(computedStyle.paddingLeft) +
parseFloat(computedStyle.paddingRight);
const widthChange = Math.abs(event.clientX - x);
const isShrinking = direction === 'right' && x > event.clientX;
updateColumnWidth(
Math.max(
isShrinking ? width - widthChange : widthChange + width,
MIN_COLUMN_WIDTH,
),
);
}
resetState();
document.removeEventListener('mouseup', handler);
}
};
return handler;
},
[activeCell, resetState, updateColumnWidth, updateRowHeight],
);
const toggleResize = useCallback(
(direction) =>
(event) => {
event.preventDefault();
event.stopPropagation();
if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}
mouseStartPosRef.current = {
x: event.clientX,
y: event.clientY,
};
updateMouseCurrentPos(mouseStartPosRef.current);
updateDraggingDirection(direction);
document.addEventListener('mouseup', mouseUpHandler(direction));
},
[
activeCell,
draggingDirection,
resetState,
updateColumnWidth,
updateRowHeight,
mouseUpHandler,
],
);
const getResizers = useCallback(() => {
if (activeCell) {
const {height, width, top, left} =
activeCell.elem.getBoundingClientRect();
const styles = {
bottom: {
backgroundColor: 'none',
cursor: 'row-resize',
height: '10px',
left: `${window.pageXOffset + left}px`,
top: `${window.pageYOffset + top + height}px`,
width: `${width}px`,
},
right: {
backgroundColor: 'none',
cursor: 'col-resize',
height: `${height}px`,
left: `${window.pageXOffset + left + width}px`,
top: `${window.pageYOffset + top}px`,
width: '10px',
},
};
const tableRect = tableRectRef.current;
if (draggingDirection && mouseCurrentPos && tableRect) {
if (isHeightChanging(draggingDirection)) {
styles[draggingDirection].left = `${
window.pageXOffset + tableRect.left
}px`;
styles[draggingDirection].top = `${
window.pageYOffset + mouseCurrentPos.y
}px`;
styles[draggingDirection].height = '3px';
styles[draggingDirection].width = `${tableRect.width}px`;
} else {
styles[draggingDirection].top = `${
window.pageYOffset + tableRect.top
}px`;
styles[draggingDirection].left = `${
window.pageXOffset + mouseCurrentPos.x
}px`;
styles[draggingDirection].width = '3px';
styles[draggingDirection].height = `${tableRect.height}px`;
}
styles[draggingDirection].backgroundColor = '#adf';
}
return styles;
}
return {
bottom: null,
left: null,
right: null,
top: null,
};
}, [activeCell, draggingDirection, mouseCurrentPos]);
const resizerStyles = getResizers();
return (
<div ref={resizerRef}>
{activeCell != null && !isSelectingGrid && (
<>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.right || undefined}
onMouseDown={toggleResize('right')}
/>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.bottom || undefined}
onMouseDown={toggleResize('bottom')}
/>
</>
)}
</div>
);
}
export default function TableCellResizerPlugin(){
const [editor] = useLexicalComposerContext();
const isEditable = useLexicalEditable();
return useMemo(
() =>
isEditable
? createPortal(<TableCellResizer editor={editor} />, document.body)
: null,
[editor, isEditable],
);
}

View File

@ -0,0 +1,87 @@
.table-of-contents .heading2 {
margin-left: 10px;
}
.table-of-contents .heading3 {
margin-left: 20px;
}
.selected-heading {
color: #3578e5;
position: relative;
}
.selected-heading-wrapper::before {
content: ' ';
position: absolute;
display: inline-block;
left: -30px;
top: 4px;
z-index: 10;
height: 4px;
width: 4px;
background-color: #3578e5;
border: solid 4px white;
border-radius: 50%;
}
.normal-heading {
cursor: pointer;
line-height: 20px;
font-size: 16px;
}
.table-of-contents {
color: #65676b;
position: fixed;
top: 200px;
right: -35px;
padding: 10px;
width: 250px;
display: flex;
flex-direction: row;
justify-content: flex-start;
z-index: 1;
height: 300px;
}
.first-heading {
color: black;
font-weight: bold;
cursor: pointer;
}
.headings {
list-style: none;
margin-top: 0;
margin-left: 10px;
padding: 0;
overflow: scroll;
width: 200px;
height: 220px;
overflow-x: hidden;
overflow-y: auto;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
/* Hide scrollbar for Chrome, Safari and Opera */
.headings::-webkit-scrollbar {
display: none;
}
.headings::before {
content: ' ';
position: absolute;
height: 220px;
width: 4px;
right: 240px;
margin-top: 5px;
background-color: #ccd0d5;
border-radius: 2px;
}
.normal-heading-wrapper {
margin-left: 32px;
position: relative;
}

View File

@ -0,0 +1,197 @@
/**
* 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.
*
*/
import type {TableOfContentsEntry} from '@lexical/react/LexicalTableOfContents';
import type {HeadingTagType} from '@lexical/rich-text';
import type {NodeKey} from 'lexical';
import './index.css';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import LexicalTableOfContents from '@lexical/react/LexicalTableOfContents';
import {useEffect, useRef, useState} from 'react';
import * as React from 'react';
const MARGIN_ABOVE_EDITOR = 624;
const HEADING_WIDTH = 9;
function indent(tagName: HeadingTagType) {
if (tagName === 'h2') {
return 'heading2';
} else if (tagName === 'h3') {
return 'heading3';
}
}
function isHeadingAtTheTopOfThePage(element: HTMLElement): boolean {
const elementYPosition = element?.getClientRects()[0].y;
return (
elementYPosition >= MARGIN_ABOVE_EDITOR &&
elementYPosition <= MARGIN_ABOVE_EDITOR + HEADING_WIDTH
);
}
function isHeadingAboveViewport(element: HTMLElement): boolean {
const elementYPosition = element?.getClientRects()[0].y;
return elementYPosition < MARGIN_ABOVE_EDITOR;
}
function isHeadingBelowTheTopOfThePage(element: HTMLElement): boolean {
const elementYPosition = element?.getClientRects()[0].y;
return elementYPosition >= MARGIN_ABOVE_EDITOR + HEADING_WIDTH;
}
function TableOfContentsList({
tableOfContents,
}: {
tableOfContents: Array<TableOfContentsEntry>;
}): JSX.Element {
const [selectedKey, setSelectedKey] = useState('');
const selectedIndex = useRef(0);
const [editor] = useLexicalComposerContext();
function scrollToNode(key: NodeKey, currIndex: number) {
editor.getEditorState().read(() => {
const domElement = editor.getElementByKey(key);
if (domElement !== null) {
domElement.scrollIntoView();
setSelectedKey(key);
selectedIndex.current = currIndex;
}
});
}
useEffect(() => {
function scrollCallback() {
if (
tableOfContents.length !== 0 &&
selectedIndex.current < tableOfContents.length - 1
) {
let currentHeading = editor.getElementByKey(
tableOfContents[selectedIndex.current][0],
);
if (currentHeading !== null) {
if (isHeadingBelowTheTopOfThePage(currentHeading)) {
//On natural scroll, user is scrolling up
while (
currentHeading !== null &&
isHeadingBelowTheTopOfThePage(currentHeading) &&
selectedIndex.current > 0
) {
const prevHeading = editor.getElementByKey(
tableOfContents[selectedIndex.current - 1][0],
);
if (
prevHeading !== null &&
(isHeadingAboveViewport(prevHeading) ||
isHeadingBelowTheTopOfThePage(prevHeading))
) {
selectedIndex.current--;
}
currentHeading = prevHeading;
}
const prevHeadingKey = tableOfContents[selectedIndex.current][0];
setSelectedKey(prevHeadingKey);
} else if (isHeadingAboveViewport(currentHeading)) {
//On natural scroll, user is scrolling down
while (
currentHeading !== null &&
isHeadingAboveViewport(currentHeading) &&
selectedIndex.current < tableOfContents.length - 1
) {
const nextHeading = editor.getElementByKey(
tableOfContents[selectedIndex.current + 1][0],
);
if (
nextHeading !== null &&
(isHeadingAtTheTopOfThePage(nextHeading) ||
isHeadingAboveViewport(nextHeading))
) {
selectedIndex.current++;
}
currentHeading = nextHeading;
}
const nextHeadingKey = tableOfContents[selectedIndex.current][0];
setSelectedKey(nextHeadingKey);
}
}
} else {
selectedIndex.current = 0;
}
}
let timerId: ReturnType<typeof setTimeout>;
function debounceFunction(func: () => void, delay: number) {
clearTimeout(timerId);
timerId = setTimeout(func, delay);
}
function onScroll(): void {
debounceFunction(scrollCallback, 10);
}
document.addEventListener('scroll', onScroll);
return () => document.removeEventListener('scroll', onScroll);
}, [tableOfContents, editor]);
return (
<div className="table-of-contents">
<ul className="headings">
{tableOfContents.map(([key, text, tag], index) => {
if (index === 0) {
return (
<div className="normal-heading-wrapper" key={key}>
<div
className="first-heading"
onClick={() => scrollToNode(key, index)}
role="button"
tabIndex={0}>
{('' + text).length > 20
? text.substring(0, 20) + '...'
: text}
</div>
<br />
</div>
);
} else {
return (
<div
className={`normal-heading-wrapper ${
selectedKey === key ? 'selected-heading-wrapper' : ''
}`}
key={key}>
<div
onClick={() => scrollToNode(key, index)}
role="button"
className={indent(tag)}
tabIndex={0}>
<li
className={`normal-heading ${
selectedKey === key ? 'selected-heading' : ''
}
`}>
{('' + text).length > 27
? text.substring(0, 27) + '...'
: text}
</li>
</div>
</div>
);
}
})}
</ul>
</div>
);
}
export default function TableOfContentsPlugin() {
return (
<LexicalTableOfContents>
{(tableOfContents) => {
return <TableOfContentsList tableOfContents={tableOfContents} />;
}}
</LexicalTableOfContents>
);
}

View File

@ -165,7 +165,7 @@ export function TablePlugin({
cellContext.set(cellEditorConfig, children);
return editor.registerCommand(
INSERT_TABLE_COMMAND,
INSERT_NEW_TABLE_COMMAND,
({columns, rows, includeHeaders}) => {
const tableNode = $createTableNodeWithDimensions(
Number(rows),

View File

@ -31,6 +31,22 @@ const firstTheme = {
underlineStrikethrough: "editor-text-underlineStrikethrough",
code: "editor-text-code"
},
table: 'editor-table',
tableAddColumns: 'editor-tableAddColumns',
tableAddRows: 'editor-tableAddRows',
tableCell: 'editor-tableCell',
tableCellActionButton: 'editor-tableCellActionButton',
tableCellActionButtonContainer:
'editor-tableCellActionButtonContainer',
tableCellEditing: 'editor-tableCellEditing',
tableCellHeader: 'editor-tableCellHeader',
tableCellPrimarySelected: 'editor-tableCellPrimarySelected',
tableCellResizer: 'editor-tableCellResizer',
tableCellSelected: 'editor-tableCellSelected',
tableCellSortedIndicator: 'editor-tableCellSortedIndicator',
tableResizeRuler: 'editor-tableCellResizeRuler',
tableSelected: 'editor-tableSelected',
tableSelection: 'editor-tableSelection',
code: "editor-code",
codeHighlight: {
atrule: "editor-tokenAttr",
@ -67,4 +83,3 @@ const firstTheme = {
};
export default firstTheme;