feat:添加表格
This commit is contained in:
parent
b6ea89712c
commit
8dd57ec5b4
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
// },
|
||||
// );
|
||||
// }
|
|
@ -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}/>
|
||||
|
|
|
@ -750,3 +750,163 @@ body {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.table-cell-action-button-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
will-change: transform;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.TableCellResizer__resizer {
|
||||
position: absolute;
|
||||
}
|
|
@ -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],
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
Loading…
Reference in New Issue