diff --git a/packages/editor-toolbar/package.json b/packages/editor-toolbar/package.json index 9e4fce7863..ab5cd8dc55 100644 --- a/packages/editor-toolbar/package.json +++ b/packages/editor-toolbar/package.json @@ -16,7 +16,8 @@ "fp-ts": "^2.16.0", "jotai-optics": "^0.3.0", "optics-ts": "^2.4.1", - "image-plugin": "*" + "image-plugin": "*", + "ts-pattern": "^4.0" }, "devDependencies": { "@testing-library/react": "^12.x", diff --git a/packages/editor-toolbar/src/components/EditingLinkPopper.tsx b/packages/editor-toolbar/src/components/EditingLinkPopper.tsx new file mode 100644 index 0000000000..595cccfcad --- /dev/null +++ b/packages/editor-toolbar/src/components/EditingLinkPopper.tsx @@ -0,0 +1,79 @@ +import { useState, useEffect } from "react" +import { Popper, TextField, Paper } from "@material-ui/core" +import { $getSelection } from "lexical" +import { useAtomValue } from "jotai" +import { $isLinkNode, LinkNode } from "@lexical/link" +import { + TextNode, + $isRangeSelection, + $isTextNode, + LexicalEditor, +} from "lexical" +import { + head as Ahead, + reduce as Areduce, + map as Amap, + filter as Afilter, +} from "fp-ts/Array" +import { + fromNullable as OfromNullable, + toNullable as OtoNullable, + getOrElse as OgetOrElse, + bindTo as ObindTo, + bind as Obind, + let as Olet, + Do as ODo, + map as Omap, + none, +} from "fp-ts/Option" +import { MonoidAll } from "fp-ts/boolean" +import { pipe } from "fp-ts/function" +import { isLinkAtom } from "../context/atomConfigs" +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" + +const getAnchorElement = (editor: LexicalEditor) => { + const selection = $getSelection() + if (!$isRangeSelection(selection)) return none + return pipe( + ODo, + Obind("textNode", () => + pipe(selection.getNodes(), Afilter($isTextNode), Ahead), + ), + Obind("linkNode", ({ textNode }) => + pipe(textNode.getParents(), Afilter($isLinkNode), Ahead), + ), + Obind("anchorElement", ({ linkNode }) => + OfromNullable(linkNode.exportDOM(editor).element), + ), + Omap(({ anchorElement }) => anchorElement), + ) +} + +type EditingLinkPopper = { + anchorElement: HTMLElement | null +} + +const EditingLinkPopper = ({ anchorElement }: EditingLinkPopper) => { + const [editor] = useLexicalComposerContext() + // const [anchorElement, setAnchorElement] = useState(null) + const isLink = useAtomValue(isLinkAtom) + const isOpen = MonoidAll.concat(isLink as boolean, editor.isEditable()) + console.log(anchorElement) +// anchorElement needs to be set before open + // useEffect(() => { + // editor.registerUpdateListener(({ editorState }) => { + // editorState.read(() => { + // setAnchorElement(pipe(editor, getAnchorElement, OtoNullable)) + // }) + // }) + // }, [editor, setAnchorElement]) + return ( + + + + + + ) +} + +export { EditingLinkPopper } diff --git a/packages/editor-toolbar/src/components/InsertLinkButton.tsx b/packages/editor-toolbar/src/components/InsertLinkButton.tsx new file mode 100644 index 0000000000..a8f0318644 --- /dev/null +++ b/packages/editor-toolbar/src/components/InsertLinkButton.tsx @@ -0,0 +1,41 @@ +import { useRef } from "react" +import { IconButton } from "@material-ui/core" +import InsertLinkIcon from "@material-ui/icons/InsertLink" +import { TOGGLE_LINK_COMMAND } from "@lexical/link" +import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext" +import { useAtom } from "jotai" +import { match } from "ts-pattern" +import { EditingLinkPopper } from "./EditingLinkPopper" +import { isLinkAtom } from "../context/atomConfigs" +import { useActiveClass } from "../hooks/useActiveClass" + +const InsertLinkButton = () => { + const [editor] = useLexicalComposerContext() + const [isLink, setIsLink] = useAtom(isLinkAtom) + const isActive = useActiveClass(isLinkAtom) + const ref = useRef(null) + + const onClick = () => { + editor.update(() => { + match(isLink) + .with(true, () => { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null) + }) + .with(false, () => { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, "") + }) + .exhaustive() + }) + } + + return ( + <> + + + + + + ) +} + +export { InsertLinkButton } diff --git a/packages/editor-toolbar/src/context/atomConfigs.ts b/packages/editor-toolbar/src/context/atomConfigs.ts index 51a186d5d2..ed16577e65 100644 --- a/packages/editor-toolbar/src/context/atomConfigs.ts +++ b/packages/editor-toolbar/src/context/atomConfigs.ts @@ -45,6 +45,7 @@ export const formatAtom = atom({ fontColor: "hsl(0, 0%, 0%)", fontFamily: FontFamily.ARIAL, blockType: BlockTypes.PARAGRAPH, + isLink: false, }) export const isBoldAtom = focusAtom(formatAtom, (optic) => optic.prop("isBold")) export const isItalicAtom = focusAtom(formatAtom, (optic) => @@ -67,6 +68,8 @@ export const blockTypeAtom = focusAtom(formatAtom, (optic) => optic.prop("blockType"), ) +export const isLinkAtom = focusAtom(formatAtom, (optic) => optic.prop("isLink")) + const historyAtom = atom({ canUndo: false, canRedo: false, diff --git a/packages/editor-toolbar/src/hooks/useLinkProperties.tsx b/packages/editor-toolbar/src/hooks/useLinkProperties.tsx new file mode 100644 index 0000000000..67c1ef8ae1 --- /dev/null +++ b/packages/editor-toolbar/src/hooks/useLinkProperties.tsx @@ -0,0 +1,35 @@ +import { useSetAtom } from "jotai" +import { useCallback } from "react" +import { $isLinkNode } from "@lexical/link" +import { $getSelection, $isRangeSelection, $isTextNode, LexicalNode } from "lexical" +import { pipe } from "fp-ts/function" +import { map as Amap, reduce as Areduce, filter as Afilter } from "fp-ts/Array" +import { MonoidAny } from "fp-ts/boolean" +import { + isLinkAtom, +} from "../context/atomConfigs" + +const hasLinkNodeParent = (node: LexicalNode) => { + return pipe( + node.getParents(), + Amap((parent) => $isLinkNode(parent)), + Areduce(false, MonoidAny.concat) + ) +} + +const useLinkProperties = () => { + const setIsLink = useSetAtom(isLinkAtom) + return useCallback(() => { + const selection = $getSelection() + if (!$isRangeSelection(selection)) return + const isLink = pipe( + selection.getNodes(), + Afilter($isTextNode), + Amap((node) => hasLinkNodeParent(node)), + Areduce(false, MonoidAny.concat) + ) + setIsLink(isLink) + }, [setIsLink]) +} + +export { useLinkProperties } diff --git a/packages/editor-toolbar/src/hooks/useUpdateToolbar.ts b/packages/editor-toolbar/src/hooks/useUpdateToolbar.ts index 1b4e4d1ee1..49fd529162 100644 --- a/packages/editor-toolbar/src/hooks/useUpdateToolbar.ts +++ b/packages/editor-toolbar/src/hooks/useUpdateToolbar.ts @@ -2,20 +2,24 @@ import { useCallback } from "react" import { useFontProperties } from "./useFontProperties" import { useFontColorProperties } from "./useFontColorProperties" import { useBlockTypeProperties } from "./useBlockTypeProperties" +import { useLinkProperties } from "./useLinkProperties" const useUpdateToolbar = () => { const updateFontProperties = useFontProperties() const updateTextColorProperties = useFontColorProperties() const updateBlockTypeProperties = useBlockTypeProperties() + const updateLinkProperties = useLinkProperties() return useCallback(() => { updateFontProperties() updateTextColorProperties() updateBlockTypeProperties() + updateLinkProperties() }, [ updateBlockTypeProperties, updateFontProperties, updateTextColorProperties, + updateLinkProperties, ]) } diff --git a/packages/editor-toolbar/src/index.tsx b/packages/editor-toolbar/src/index.tsx index 0812533861..3454a8f97a 100644 --- a/packages/editor-toolbar/src/index.tsx +++ b/packages/editor-toolbar/src/index.tsx @@ -10,6 +10,7 @@ import { FormatUnderlineButton } from "./components/FormatUnderlineButton" import { ColorPickerButton } from "./components/ColorPickerButton" import { InsertTableButton } from "./components/InsertTableButton" import { InsertImageButton } from "./components/InsertImageButton" +import { InsertLinkButton } from "./components/InsertLinkButton" import { useCleanup } from "./hooks/useCleanup" import { useToolbarStyles } from "./hooks/useToolbarStyles" @@ -27,6 +28,7 @@ const DictybaseToolbar = () => { + diff --git a/packages/editor/src/Editor.tsx b/packages/editor/src/Editor.tsx index afdd502b81..0e2f5b8478 100644 --- a/packages/editor/src/Editor.tsx +++ b/packages/editor/src/Editor.tsx @@ -6,6 +6,7 @@ import { import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin" import { ContentEditable } from "@lexical/react/LexicalContentEditable" import { ListPlugin } from "@lexical/react/LexicalListPlugin" +import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin" import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin" import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary" import { Grid, Container, Button, makeStyles, Theme } from "@material-ui/core" @@ -82,6 +83,7 @@ const Editor = ({ }}> <>{plugins} + diff --git a/packages/editor/src/editorConfig.ts b/packages/editor/src/editorConfig.ts index 08d602fcca..a2c309f359 100644 --- a/packages/editor/src/editorConfig.ts +++ b/packages/editor/src/editorConfig.ts @@ -1,7 +1,7 @@ import { ListItemNode, ListNode } from "@lexical/list" import { HeadingNode, QuoteNode } from "@lexical/rich-text" import { TableCellNode, TableRowNode } from "@lexical/table" -import { LinkNode } from "@lexical/link" +import { LinkNode, AutoLinkNode } from "@lexical/link" import { ImageNode } from "image-plugin" import { WidthTableNode } from "width-table-plugin" import { FlexLayoutNode } from "flex-layout-plugin" @@ -32,6 +32,7 @@ const dictyEditorConfig = { ListItemNode, ListNode, LinkNode, + AutoLinkNode, ImageNode, TableCellNode, TableRowNode, diff --git a/yarn.lock b/yarn.lock index 24d62f86d8..2acf353cdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18553,7 +18553,7 @@ ts-jest@^27.1.3: semver "7.x" yargs-parser "20.x" -ts-pattern@4.x: +ts-pattern@4.x, ts-pattern@^4.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ts-pattern/-/ts-pattern-4.3.0.tgz#7a995b39342f1b00d1507c2d2f3b90ea16e178a6" integrity sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==