refactor
This commit is contained in:
parent
a78eae899a
commit
8084981a29
|
|
@ -20,6 +20,7 @@
|
|||
"@codemirror/language": "^0.20.0",
|
||||
"@codemirror/state": "^0.20.0",
|
||||
"@codemirror/view": "^0.20.3",
|
||||
"@floating-ui/react-dom-interactions": "^0.6.3",
|
||||
"@headlessui/react": "^1.6.0",
|
||||
"@prisma/client": "^3.14.0",
|
||||
"@proselog/jwt": "^0.1.1",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ specifiers:
|
|||
'@codemirror/language': ^0.20.0
|
||||
'@codemirror/state': ^0.20.0
|
||||
'@codemirror/view': ^0.20.3
|
||||
'@floating-ui/react-dom-interactions': ^0.6.3
|
||||
'@headlessui/react': ^1.6.0
|
||||
'@iconify-json/bi': ^1.1.3
|
||||
'@iconify-json/bxs': ^1.1.2
|
||||
|
|
@ -93,6 +94,7 @@ dependencies:
|
|||
'@codemirror/language': 0.20.0
|
||||
'@codemirror/state': 0.20.0
|
||||
'@codemirror/view': 0.20.4
|
||||
'@floating-ui/react-dom-interactions': 0.6.3_ohobp6rpsmerwlq5ipwfh5yigy
|
||||
'@headlessui/react': 1.6.1_ef5jwxihqo6n7gxfmzogljlgcm
|
||||
'@prisma/client': 3.14.0_prisma@3.14.0
|
||||
'@proselog/jwt': 0.1.1
|
||||
|
|
@ -480,6 +482,45 @@ packages:
|
|||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@floating-ui/core/0.7.2:
|
||||
resolution: {integrity: sha512-FRVAkSNU/vGXLIsgbggcs70GkXKEOXgBBbNpYPNHSaKsCAMMd00NrjbtKTesxkdv9xm9N3+XiDlcFGY6WnatBg==}
|
||||
dev: false
|
||||
|
||||
/@floating-ui/dom/0.5.2:
|
||||
resolution: {integrity: sha512-z1DnEa7F3d8Fm/eXSbii8UEGpcjZGkQaYYUI0WpEVgD3vBfebDW8j/3ysusxonuMexoigA+A3b/fYH7sEqiwyg==}
|
||||
dependencies:
|
||||
'@floating-ui/core': 0.7.2
|
||||
dev: false
|
||||
|
||||
/@floating-ui/react-dom-interactions/0.6.3_ohobp6rpsmerwlq5ipwfh5yigy:
|
||||
resolution: {integrity: sha512-xvbGEtBtA7JaEngnHQjROArv2onRp3oJIpb4+bEN5EGJf0hBYDY0vD8vFGPz/5TQwN++hb6icOB1QwdOnffMzw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
dependencies:
|
||||
'@floating-ui/react-dom': 0.7.1_ohobp6rpsmerwlq5ipwfh5yigy
|
||||
aria-hidden: 1.1.3
|
||||
react: 18.1.0
|
||||
react-dom: 18.1.0_react@18.1.0
|
||||
use-isomorphic-layout-effect: 1.1.2_7cpxmzzodpxnolj5zcc5cr63ji
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@floating-ui/react-dom/0.7.1_ohobp6rpsmerwlq5ipwfh5yigy:
|
||||
resolution: {integrity: sha512-Dd7e8AupUjzcjeGf1g3EItf/QRtEWKF5GGyEs5WA5n3zlHvEgZ4XrZM6ANhUnzgE3pUQAaXkcXLnibgFp1YBRw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
dependencies:
|
||||
'@floating-ui/dom': 0.5.2
|
||||
react: 18.1.0
|
||||
react-dom: 18.1.0_react@18.1.0
|
||||
use-isomorphic-layout-effect: 1.1.2_7cpxmzzodpxnolj5zcc5cr63ji
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@headlessui/react/1.6.1_ef5jwxihqo6n7gxfmzogljlgcm:
|
||||
resolution: {integrity: sha512-gMd6uIs1U4Oz718Z5gFoV0o/vD43/4zvbyiJN9Dt7PK9Ubxn+TmJwTmYwyNJc5KxxU1t0CmgTNgwZX9+4NjCnQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -1266,6 +1307,13 @@ packages:
|
|||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
dev: true
|
||||
|
||||
/aria-hidden/1.1.3:
|
||||
resolution: {integrity: sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==}
|
||||
engines: {node: '>=8.5.0'}
|
||||
dependencies:
|
||||
tslib: 1.14.1
|
||||
dev: false
|
||||
|
||||
/aria-query/4.2.2:
|
||||
resolution: {integrity: sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
|
@ -4676,7 +4724,6 @@ packages:
|
|||
|
||||
/tslib/1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
dev: true
|
||||
|
||||
/tsutils/3.21.0_typescript@4.6.4:
|
||||
resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Bold: ICommand = {
|
||||
name: "bold",
|
||||
icon: (
|
||||
<svg
|
||||
height="16"
|
||||
width="16"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4 2a1 1 0 00-1 1v10a1 1 0 001 1h5.5a3.5 3.5 0 001.852-6.47A3.5 3.5 0 008.5 2H4zm4.5 5a1.5 1.5 0 100-3H5v3h3.5zM5 9v3h4.5a1.5 1.5 0 000-3H5z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "**", append: "**" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Code: ICommand = {
|
||||
name: "code",
|
||||
icon: (
|
||||
<svg height="16" viewBox="0 0 16 16" width="16">
|
||||
<path d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 10-1.06 1.06L13.94 8l-3.72 3.72a.75.75 0 101.06 1.06l4.25-4.25a.75.75 0 000-1.06l-4.25-4.25z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "`", append: "`" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { ICommand, prependExecute } from "."
|
||||
|
||||
export const Heading: ICommand = {
|
||||
name: "heading",
|
||||
icon: (
|
||||
<svg height="16" width="16" viewBox="0 0 16 16">
|
||||
<path d="M3.75 2a.75.75 0 01.75.75V7h7V2.75a.75.75 0 011.5 0v10.5a.75.75 0 01-1.5 0V8.5h-7v4.75a.75.75 0 01-1.5 0V2.75A.75.75 0 013.75 2z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
prependExecute({ view, prepend: "### " })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, prependExecute, wrapExecute } from "."
|
||||
|
||||
export const Image: ICommand = {
|
||||
name: "image",
|
||||
icon: (
|
||||
<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||
<path d="M204.8 204.8a51.2 51.2 0 0 0-51.2 51.2v563.2a51.2 51.2 0 0 0 51.2 51.2h614.4a51.2 51.2 0 0 0 51.2-51.2V256a51.2 51.2 0 0 0-51.2-51.2H204.8z m0-51.2h614.4a102.4 102.4 0 0 1 102.4 102.4v563.2a102.4 102.4 0 0 1-102.4 102.4H204.8a102.4 102.4 0 0 1-102.4-102.4V256a102.4 102.4 0 0 1 102.4-102.4z"></path>
|
||||
<path d="M142.6432 738.816l-36.352-36.0448 205.1584-206.8992 144.3328 146.2784-36.4544 35.9424-107.9808-109.4144zM358.4 435.2a76.8 76.8 0 1 1 0-153.6 76.8 76.8 0 0 1 0 153.6z m0-51.2a25.6 25.6 0 1 0 0-51.2 25.6 25.6 0 0 0 0 51.2z"></path>
|
||||
<path d="M673.3312 466.7392l-433.664 453.9392-37.0688-35.3792 469.504-491.4176 236.544 231.5776-35.7888 36.608z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "![", append: "]()" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Italic: ICommand = {
|
||||
name: "italic",
|
||||
icon: (
|
||||
<svg height="16" viewBox="0 0 16 16" width="16">
|
||||
<path d="M6 2.75A.75.75 0 016.75 2h6.5a.75.75 0 010 1.5h-2.505l-3.858 9H9.25a.75.75 0 010 1.5h-6.5a.75.75 0 010-1.5h2.505l3.858-9H6.75A.75.75 0 016 2.75z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "_", append: "_" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, prependExecute, wrapExecute } from "."
|
||||
|
||||
export const Link: ICommand = {
|
||||
name: "link",
|
||||
icon: (
|
||||
<svg height="16" viewBox="0 0 16 16" width="16">
|
||||
<path d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "[", append: "]()" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const ListOrdered: ICommand = {
|
||||
name: "list-ordered",
|
||||
icon: (
|
||||
<svg height="16" viewBox="0 0 16 16" width="16">
|
||||
<path d="M2.003 2.5a.5.5 0 00-.723-.447l-1.003.5a.5.5 0 00.446.895l.28-.14V6H.5a.5.5 0 000 1h2.006a.5.5 0 100-1h-.503V2.5zM5 3.25a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 015 3.25zm0 5a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5A.75.75 0 015 8.25zm0 5a.75.75 0 01.75-.75h8.5a.75.75 0 010 1.5h-8.5a.75.75 0 01-.75-.75zM.924 10.32l.003-.004a.851.851 0 01.144-.153A.66.66 0 011.5 10c.195 0 .306.068.374.146a.57.57 0 01.128.376c0 .453-.269.682-.8 1.078l-.035.025C.692 11.98 0 12.495 0 13.5a.5.5 0 00.5.5h2.003a.5.5 0 000-1H1.146c.132-.197.351-.372.654-.597l.047-.035c.47-.35 1.156-.858 1.156-1.845 0-.365-.118-.744-.377-1.038-.268-.303-.658-.484-1.126-.484-.48 0-.84.202-1.068.392a1.858 1.858 0 00-.348.384l-.007.011-.002.004-.001.002-.001.001a.5.5 0 00.851.525zM.5 10.055l-.427-.26.427.26z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "1. ", append: "\n" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const ListUnordered: ICommand = {
|
||||
name: "list-unordered",
|
||||
icon: (
|
||||
<svg height="16" viewBox="0 0 16 16" width="16">
|
||||
<path d="M2 4a1 1 0 100-2 1 1 0 000 2zm3.75-1.5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zM3 8a1 1 0 11-2 0 1 1 0 012 0zm-1 6a1 1 0 100-2 1 1 0 000 2z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "- ", append: "\n" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { ICommand } from "."
|
||||
|
||||
export const Preview: ICommand = {
|
||||
name: "preview",
|
||||
icon: (
|
||||
<svg width="16" height="16" viewBox="0 0 32 32">
|
||||
<path d="M0 16c3.037-5.864 9.058-9.802 16-9.802s12.963 3.938 15.953 9.703l0.047 0.1c-3.037 5.864-9.058 9.802-16 9.802s-12.963-3.938-15.953-9.703l-0.047-0.1zM16 22.531c3.607 0 6.531-2.924 6.531-6.531s-2.924-6.531-6.531-6.531v0c-3.607 0-6.531 2.924-6.531 6.531s2.924 6.531 6.531 6.531v0zM16 19.265c-1.804 0-3.265-1.461-3.265-3.265s1.461-3.265 3.265-3.265v0c1.804 0 3.265 1.461 3.265 3.265s-1.461 3.265-3.265 3.265v0z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (_, options) => {
|
||||
options?.setPreviewVisible?.((visible) => !visible)
|
||||
},
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Quote: ICommand = {
|
||||
name: "quote",
|
||||
icon: (
|
||||
<svg height="16" viewBox="0 0 16 16" width="16">
|
||||
<path d="M1.75 2.5a.75.75 0 000 1.5h10.5a.75.75 0 000-1.5H1.75zm4 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zm0 5a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5zM2.5 7.75a.75.75 0 00-1.5 0v6a.75.75 0 001.5 0v-6z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "> ", append: "\n" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Strikethrough: ICommand = {
|
||||
name: "strikethrough",
|
||||
icon: (
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path
|
||||
d="M665.088 512H921.6v51.2h-199.6288c17.0496 24.832 25.6512 53.9136 25.6512 87.3472 0 56.2176-23.9616 101.376-70.9632 134.5536-45.1584 31.3344-105.0624 47.0016-179.712 47.0016-75.5712 0-135.4752-17.5104-178.7904-51.6096-49.7664-39.6288-77.4144-99.5328-82.944-178.7904h55.296c6.4512 61.7472 27.648 107.8272 64.512 138.24 33.1776 26.7264 80.1792 40.5504 141.0048 40.5504 58.0608 0 105.0624-11.9808 141.0048-34.0992 35.0208-23.04 53.4528-54.3744 53.4528-93.0816 0-36.7616-15.2576-66.7648-44.8-90.112H102.4v-51.2h426.3936c-15.1552-4.608-31.8976-9.472-50.2784-14.4384-78.336-22.1184-129.024-39.6288-151.1424-52.5312-50.688-28.5696-75.5712-70.0416-75.5712-123.4944 0-57.1392 23.04-100.4544 70.9632-130.8672 42.3936-28.5696 97.6896-42.3936 164.0448-42.3936 71.8848 0 129.024 17.5104 171.4176 52.5312 43.3152 35.0208 68.1984 86.6304 73.728 154.8288h-56.2176c-7.3728-51.6096-26.7264-90.3168-58.0608-117.0432-31.3344-26.7264-75.5712-39.6288-132.7104-39.6288-53.4528 0-94.9248 9.216-125.3376 27.648-35.0208 20.2752-51.6096 51.6096-51.6096 93.0816 0 36.864 20.2752 65.4336 61.7472 87.552 19.3536 9.216 61.7472 23.9616 126.2592 42.3936 83.4048 23.808 139.4688 44.032 169.0624 62.3616z"
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "~~", append: "~~" })
|
||||
},
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Underline: ICommand = {
|
||||
name: "underline",
|
||||
icon: (
|
||||
<svg viewBox="0 0 1024 1024" width="16" height="16">
|
||||
<path d="M204.8 819.2h614.4v51.2H204.8zM268.3392 146.5344h53.0944v381.2352c0 66.1504 13.9264 116.6336 43.52 150.5792 31.3344 35.6864 79.2064 53.9648 145.3568 53.9648s114.0224-18.2784 145.3568-53.9648c28.7232-33.9456 43.52-84.4288 43.52-150.5792V146.5344h53.0944v381.2352c0 80.9472-20.0192 142.7456-58.3168 185.3952-40.9088 44.3904-101.8368 67.0208-183.6544 67.0208s-142.7456-22.6304-182.784-66.1504c-40.0384-43.52-59.1872-105.3184-59.1872-186.2656V146.5344z"></path>
|
||||
</svg>
|
||||
),
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "<u>", append: "</u>" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import clsx from "clsx"
|
||||
import { useCodeCopy } from "~/hooks/useCodeCopy"
|
||||
|
||||
export const PageContent: React.FC<{
|
||||
contentHTML: string
|
||||
className?: string
|
||||
}> = ({ contentHTML, className }) => {
|
||||
useCodeCopy()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(`prose`, className)}
|
||||
dangerouslySetInnerHTML={{ __html: contentHTML }}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import type { PageType } from "~/lib/db.server"
|
||||
import { Rendered } from "~/markdown"
|
||||
import { PageContent } from "../common/PageContent"
|
||||
import { PostMeta } from "./PostMeta"
|
||||
import { useCodeCopy } from "~/hooks/useCodeCopy"
|
||||
|
||||
export const SitePage: React.FC<{
|
||||
page: {
|
||||
|
|
@ -12,8 +12,6 @@ export const SitePage: React.FC<{
|
|||
authors?: { id: string; name: string; avatar: string | null }[]
|
||||
}
|
||||
}> = ({ page }) => {
|
||||
useCodeCopy()
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
|
|
@ -29,10 +27,10 @@ export const SitePage: React.FC<{
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="my-8 prose"
|
||||
dangerouslySetInnerHTML={{ __html: page.rendered?.contentHTML || "" }}
|
||||
></div>
|
||||
<PageContent
|
||||
contentHTML={page.rendered?.contentHTML || ""}
|
||||
className="my-8"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from "react"
|
||||
import { useCallback, useEffect, useRef, useState } from "react"
|
||||
import { EditorState } from "@codemirror/state"
|
||||
import {
|
||||
EditorView,
|
||||
|
|
@ -49,56 +49,67 @@ export const useEditor = ({
|
|||
placeholder?: string
|
||||
onDropFile?: (file: File, view: EditorView) => void
|
||||
}) => {
|
||||
const editorRef = useRef<HTMLDivElement | null>(null)
|
||||
const [node, setNode] = useState<HTMLDivElement | null>(null)
|
||||
const [view, setView] = useState<EditorView | null>(null)
|
||||
|
||||
const initEditor = useCallback(
|
||||
(node: HTMLDivElement) => {
|
||||
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
|
||||
if (vu.docChanged && typeof onChange === "function") {
|
||||
const doc = vu.state.doc
|
||||
const value = doc.toString()
|
||||
onChange(value)
|
||||
}
|
||||
})
|
||||
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: "",
|
||||
extensions: [
|
||||
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||
history(),
|
||||
dropCursor(),
|
||||
drawSelection(),
|
||||
indentOnInput(),
|
||||
crosshairCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
updateListener,
|
||||
EditorView.lineWrapping,
|
||||
markdown(),
|
||||
placeholderExtension(placeholder || ""),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
theme,
|
||||
EditorView.domEventHandlers({
|
||||
drop(e) {
|
||||
const file = e.dataTransfer?.items[0]?.getAsFile()
|
||||
if (!file) return
|
||||
|
||||
onDropFile?.(file, view)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
parent: node,
|
||||
})
|
||||
|
||||
view.focus()
|
||||
|
||||
return view
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[onChange, placeholder],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const updateListener = EditorView.updateListener.of((vu: ViewUpdate) => {
|
||||
if (vu.docChanged && typeof onChange === "function") {
|
||||
const doc = vu.state.doc
|
||||
const value = doc.toString()
|
||||
onChange(value)
|
||||
}
|
||||
})
|
||||
|
||||
const view = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: "",
|
||||
extensions: [
|
||||
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
|
||||
history(),
|
||||
dropCursor(),
|
||||
drawSelection(),
|
||||
indentOnInput(),
|
||||
crosshairCursor(),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
updateListener,
|
||||
EditorView.lineWrapping,
|
||||
markdown(),
|
||||
placeholderExtension(placeholder || ""),
|
||||
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
||||
theme,
|
||||
EditorView.domEventHandlers({
|
||||
drop(e) {
|
||||
const file = e.dataTransfer?.items[0]?.getAsFile()
|
||||
if (!file) return
|
||||
|
||||
onDropFile?.(file, view)
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
parent: editorRef.current!,
|
||||
})
|
||||
|
||||
setView(view)
|
||||
|
||||
const view = node && initEditor(node)
|
||||
if (view) {
|
||||
setView(view)
|
||||
}
|
||||
return () => {
|
||||
view.destroy()
|
||||
setView(null)
|
||||
view?.destroy()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [placeholder, onDropFile])
|
||||
}, [node])
|
||||
|
||||
// Update view state when `value` changed
|
||||
useEffect(() => {
|
||||
|
|
@ -115,6 +126,8 @@ export const useEditor = ({
|
|||
|
||||
return {
|
||||
view,
|
||||
editorRef,
|
||||
editorRef(node: HTMLDivElement) {
|
||||
setNode(node)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,15 @@
|
|||
import clsx from "clsx"
|
||||
import { FC, useCallback, useEffect, useState } from "react"
|
||||
import { renderPageContent } from "~/markdown"
|
||||
import { PageContent } from "../common/PageContent"
|
||||
|
||||
export interface EditorPreviewProps {
|
||||
className?: string
|
||||
content: string
|
||||
previewVisible: boolean
|
||||
}
|
||||
|
||||
export const EditorPreview: FC<EditorPreviewProps> = ({
|
||||
className,
|
||||
content,
|
||||
previewVisible,
|
||||
}) => {
|
||||
const [html, setHtml] = useState("")
|
||||
const renderMarkdown = useCallback(async (doc: string) => {
|
||||
|
|
@ -21,14 +19,5 @@ export const EditorPreview: FC<EditorPreviewProps> = ({
|
|||
useEffect(() => {
|
||||
renderMarkdown(content)
|
||||
}, [content, renderMarkdown])
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
"prose border-l border-gray-100 overflow-auto",
|
||||
className,
|
||||
{ hidden: !previewVisible },
|
||||
)}
|
||||
dangerouslySetInnerHTML={{ __html: html ?? "" }}
|
||||
></div>
|
||||
)
|
||||
return <PageContent className={className} contentHTML={html ?? ""} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { EditorView } from "@codemirror/view"
|
||||
import clsx from "clsx"
|
||||
import { Dispatch, FC, SetStateAction } from "react"
|
||||
import { ICommand } from "../command"
|
||||
import { ICommand } from "~/editor"
|
||||
import { Tooltip } from "./Tooltip"
|
||||
|
||||
export interface EditorToolbarProps {
|
||||
view: EditorView | null
|
||||
|
|
@ -26,30 +27,42 @@ export const EditorToolbar: FC<EditorToolbarProps> = ({
|
|||
const renderToolbar =
|
||||
(mode: ToolbarMode) =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
({ name, icon, execute }: ICommand) =>
|
||||
(
|
||||
<button
|
||||
key={name}
|
||||
className={clsx(
|
||||
"px-1 py-1 mr-1 fill-gray-700 transition-colors hover:fill-blue-500",
|
||||
{
|
||||
"fill-blue-500": mode === ToolbarMode.Preview && previewVisible,
|
||||
},
|
||||
)}
|
||||
onClick={() => {
|
||||
view && execute(view, { setPreviewVisible, container: view.dom })
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
({ name, icon, label, execute }: ICommand) => {
|
||||
const isPreviewModeCommand = mode === ToolbarMode.Preview
|
||||
const active = isPreviewModeCommand && previewVisible
|
||||
const disabled = !isPreviewModeCommand && previewVisible
|
||||
return (
|
||||
<Tooltip label={label} placement="bottom">
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={clsx(
|
||||
"w-7 h-7 transition-colors text-lg border border-transparent rounded flex items-center justify-center",
|
||||
active
|
||||
? `bg-indigo-400 text-white border-indigo-400`
|
||||
: disabled
|
||||
? `text-zinc-400 cursor-not-allowed`
|
||||
: `text-zinc-400 group-hover:text-zinc-600 hover:text-zinc-500 hover:border-zinc-300 hover:bg-zinc-100`,
|
||||
)}
|
||||
onClick={() => {
|
||||
view && execute(view, { setPreviewVisible, container: view.dom })
|
||||
}}
|
||||
>
|
||||
<span className={icon}></span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex px-1 py-1 bg-gray-100 border rounded border-gray-200">
|
||||
<div className="flex-1">
|
||||
<div className="flex group">
|
||||
<div className="flex-1 flex space-x-1">
|
||||
{toolbars?.map(renderToolbar(ToolbarMode.Normal))}
|
||||
</div>
|
||||
<div>{modeToolbars?.map(renderToolbar(ToolbarMode.Preview))}</div>
|
||||
<div className="ml-1">
|
||||
{modeToolbars?.map(renderToolbar(ToolbarMode.Preview))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { cloneElement, useState } from "react"
|
||||
import {
|
||||
Placement,
|
||||
offset,
|
||||
flip,
|
||||
shift,
|
||||
autoUpdate,
|
||||
useFloating,
|
||||
useInteractions,
|
||||
useHover,
|
||||
useFocus,
|
||||
useRole,
|
||||
useDismiss,
|
||||
} from "@floating-ui/react-dom-interactions"
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
placement?: Placement
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
export const Tooltip = ({ children, label, placement = "top" }: Props) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const { x, y, reference, floating, strategy, context } = useFloating({
|
||||
placement,
|
||||
open,
|
||||
onOpenChange: setOpen,
|
||||
middleware: [offset(5), flip(), shift({ padding: 8 })],
|
||||
whileElementsMounted: autoUpdate,
|
||||
})
|
||||
|
||||
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||
useHover(context),
|
||||
useFocus(context),
|
||||
useRole(context, { role: "tooltip" }),
|
||||
useDismiss(context),
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
{cloneElement(
|
||||
children,
|
||||
getReferenceProps({ ref: reference, ...children.props }),
|
||||
)}
|
||||
{open && (
|
||||
<div
|
||||
{...getFloatingProps({
|
||||
ref: floating,
|
||||
className: "bg-black/75 text-white rounded-lg shadow-lg px-3 py-2",
|
||||
style: {
|
||||
position: strategy,
|
||||
top: y ?? "",
|
||||
left: x ?? "",
|
||||
},
|
||||
})}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Bold: ICommand = {
|
||||
name: "bold",
|
||||
label: "Bold",
|
||||
icon: "i-bi:type-bold",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "**", append: "**" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Code: ICommand = {
|
||||
name: "code",
|
||||
label: "Inline Code",
|
||||
icon: "i-bi:code",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "`", append: "`" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const CodeBlock: ICommand = {
|
||||
name: "codeblock",
|
||||
label: "Code Block",
|
||||
icon: "i-bi:body-text",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "```\n", append: "\n```" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand, prependExecute } from "."
|
||||
|
||||
export const Heading: ICommand = {
|
||||
name: "heading",
|
||||
label: "Heading",
|
||||
icon: "i-bi:type-h3",
|
||||
execute: (view) => {
|
||||
prependExecute({ view, prepend: "### " })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, prependExecute, wrapExecute } from "."
|
||||
|
||||
export const Image: ICommand = {
|
||||
name: "image",
|
||||
label: "Image",
|
||||
icon: "i-bi:image",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "![", append: "]()" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Italic: ICommand = {
|
||||
name: "italic",
|
||||
label: "Italic",
|
||||
icon: "i-bi:type-italic",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "_", append: "_" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, prependExecute, wrapExecute } from "."
|
||||
|
||||
export const Link: ICommand = {
|
||||
name: "link",
|
||||
label: "Link",
|
||||
icon: "i-bi:link",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "[", append: "]()" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { EditorSelection } from "@codemirror/state"
|
||||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const ListOrdered: ICommand = {
|
||||
name: "list-ordered",
|
||||
label: "Ordered List",
|
||||
icon: "i-bi:list-ol",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "1. ", append: "\n" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const ListUnordered: ICommand = {
|
||||
name: "list-unordered",
|
||||
label: "Unordered List",
|
||||
icon: "i-bi:list-ul",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "- ", append: "\n" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand } from "."
|
||||
|
||||
export const Preview: ICommand = {
|
||||
name: "preview",
|
||||
label: "Toggle Preview",
|
||||
icon: "i-bi:eye",
|
||||
execute: (_, options) => {
|
||||
options?.setPreviewVisible?.((visible) => !visible)
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Quote: ICommand = {
|
||||
name: "quote",
|
||||
label: "Quote",
|
||||
icon: "i-bi:quote",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "> ", append: "\n" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Strikethrough: ICommand = {
|
||||
name: "strikethrough",
|
||||
label: "Strikethrough",
|
||||
icon: "i-bi:type-strikethrough",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "~~", append: "~~" })
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { ICommand, wrapExecute } from "."
|
||||
|
||||
export const Underline: ICommand = {
|
||||
name: "underline",
|
||||
label: "Underline",
|
||||
icon: "i-bi:type-underline",
|
||||
execute: (view) => {
|
||||
wrapExecute({ view, prepend: "<u>", append: "</u>" })
|
||||
},
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { EditorView } from "@codemirror/view"
|
|||
import { Dispatch, SetStateAction } from "react"
|
||||
import { Bold } from "./Bold"
|
||||
import { Code } from "./Code"
|
||||
import { CodeBlock } from "./CodeBlock"
|
||||
import { Heading } from "./Heading"
|
||||
import { Image } from "./Image"
|
||||
import { Italic } from "./Italic"
|
||||
|
|
@ -12,10 +13,12 @@ import { ListUnordered } from "./ListUnordered"
|
|||
import { Preview } from "./Preview"
|
||||
import { Quote } from "./Quote"
|
||||
import { Strikethrough } from "./Strikethrough"
|
||||
import { Underline } from "./Underline"
|
||||
|
||||
export type ICommand = {
|
||||
icon: React.ReactElement
|
||||
icon: string
|
||||
name: string
|
||||
label: string
|
||||
execute: (
|
||||
view: EditorView,
|
||||
options?: {
|
||||
|
|
@ -117,8 +120,10 @@ export const toolbars: ICommand[] = [
|
|||
Bold,
|
||||
Italic,
|
||||
Strikethrough,
|
||||
Underline,
|
||||
Quote,
|
||||
Code,
|
||||
CodeBlock,
|
||||
ListUnordered,
|
||||
ListOrdered,
|
||||
Link,
|
||||
|
|
@ -4,7 +4,7 @@ import dayjs from "dayjs"
|
|||
import { useRouter } from "next/router"
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
import { modeToolbars, toolbars } from "~/components/command"
|
||||
import { modeToolbars, toolbars } from "~/editor"
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { PublishButton } from "~/components/dashboard/PublishButton"
|
||||
|
|
@ -124,6 +124,7 @@ export default function SubdomainEditor() {
|
|||
value: content,
|
||||
onChange: handleEditorChange,
|
||||
onDropFile: handleDropFile,
|
||||
placeholder: `Start writing...`,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -205,7 +206,7 @@ export default function SubdomainEditor() {
|
|||
</header>
|
||||
<div className="h-screen pt-14 flex w-full">
|
||||
<div className="h-full overflow-auto w-full">
|
||||
<div className="mx-auto py-5 px-5">
|
||||
<div className="max-w-screen-md mx-auto py-5 px-5">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
|
|
@ -222,16 +223,11 @@ export default function SubdomainEditor() {
|
|||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<div className="border border-gray-200 rounded">
|
||||
<div className="flex">
|
||||
<div className="flex-1" ref={editorRef}></div>
|
||||
<EditorPreview
|
||||
className="flex-1"
|
||||
previewVisible={previewVisible}
|
||||
content={content}
|
||||
></EditorPreview>
|
||||
</div>
|
||||
</div>
|
||||
{previewVisible ? (
|
||||
<EditorPreview content={content}></EditorPreview>
|
||||
) : (
|
||||
<div ref={editorRef}></div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue