-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from seafileltd/link-plugin
Link plugin
- Loading branch information
Showing
16 changed files
with
730 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
import { Editor, Path, Range, Transforms } from 'slate'; | ||
import slugid from 'slugid'; | ||
import { findPath, getAboveNode, getEditorString, getNodeType, getSelectedElems } from '../../core/queries'; | ||
import { focusEditor } from '../../core/transforms/focus-editor'; | ||
import { ELementTypes, INSERT_POSITION } from '../../constants'; | ||
import { generateDefaultText, generateEmptyElement } from '../../core/utils'; | ||
import { replaceNodeChildren } from '../../core/transforms/replace-node-children'; | ||
|
||
export const isMenuDisabled = (editor, readonly = false) => { | ||
if (readonly) return true; | ||
const { selection } = editor; | ||
if (!selection) return false; | ||
const selectedElems = getSelectedElems(editor); | ||
// Check if the selected element is illegal | ||
const isSelectedIllegalElement = selectedElems.some(elem => { | ||
const { type } = elem; | ||
if (editor.isVoid(elem)) return true; | ||
if ([ELementTypes.CODE_BLOCK, ELementTypes.CODE_LINE].includes(type)) return true; | ||
return false; | ||
}); | ||
if (isSelectedIllegalElement) return true; | ||
return false; | ||
}; | ||
|
||
export const isLinkType = (editor) => { | ||
const [match] = Editor.nodes(editor, { | ||
match: n => getNodeType(n) === ELementTypes.LINK, | ||
universal: true, | ||
}); | ||
return !!match; | ||
}; | ||
|
||
export const generateLinkNode = (url, title) => { | ||
const linkNode = { | ||
type: ELementTypes.LINK, | ||
url: url, | ||
title: title, | ||
id: slugid.nice(), | ||
children: [{ id: slugid.nice(), text: title || '' }], | ||
}; | ||
return linkNode; | ||
}; | ||
|
||
/** | ||
* @param {Object} props | ||
* @param {Object} props.editor | ||
* @param {String} props.url | ||
* @param {String} props.title | ||
* @param {InsertPosition} props.insertPosition | ||
* @param {Object | undefined} props.slateNode | ||
*/ | ||
export const insertLink = (props) => { | ||
const { editor, url, title, insertPosition = INSERT_POSITION.CURRENT, slateNode } = props; | ||
const { selection } = editor; | ||
if (insertPosition === INSERT_POSITION.CURRENT && isMenuDisabled(editor)) return; | ||
// We had validated in modal,here we do it again for safety | ||
if (!title || !url) return; | ||
if (!selection) return; | ||
|
||
const linkNode = generateLinkNode(url, title); | ||
|
||
if (insertPosition === INSERT_POSITION.AFTER) { | ||
let path = Editor.path(editor, selection); | ||
|
||
if (slateNode && slateNode?.type === ELementTypes.LIST_ITEM) { | ||
path = findPath(editor, slateNode, []); | ||
const nextPath = Path.next(path); | ||
Editor.insertNodes(editor, linkNode, { at: nextPath }); | ||
return; | ||
} | ||
|
||
const linkNodeWrapper = generateEmptyElement(ELementTypes.PARAGRAPH); | ||
// LinkNode should be wrapped by p and within text nodes in order to be editable | ||
linkNodeWrapper.children.push(linkNode, generateDefaultText()); | ||
Transforms.insertNodes(editor, linkNodeWrapper, { at: [path[0] + 1] }); | ||
focusEditor(editor); | ||
return; | ||
} | ||
|
||
const isCollapsed = Range.isCollapsed(selection); | ||
if (isCollapsed) { | ||
// If selection is collapsed, we insert a space and then insert link node that help operation easier | ||
editor.insertText(' '); | ||
Editor.insertFragment(editor, [linkNode]); | ||
// Using insertText directly causes the added Spaces to be added to the linked text, as in the issue above, so replaced by insertFragment | ||
Editor.insertFragment(editor, [{ id: slugid.nice(), text: ' ' }]); | ||
|
||
focusEditor(editor); | ||
return; | ||
} else { | ||
const selectedText = Editor.string(editor, selection); // Selected text | ||
if (selectedText !== title) { | ||
// Replace the selected text with the link node if the selected text is different from the entered text | ||
editor.deleteFragment(); | ||
Transforms.insertNodes(editor, linkNode); | ||
} else { | ||
// Wrap the selected text with the link node if the selected text is the same as the entered text | ||
Transforms.wrapNodes(editor, linkNode, { split: true, at: selection }); | ||
Transforms.collapse(editor, { edge: 'end' }); | ||
} | ||
} | ||
focusEditor(editor); | ||
}; | ||
|
||
export const getLinkInfo = (editor) => { | ||
const isLinkNode = isLinkType(editor); | ||
if (!isLinkNode) return null; | ||
const [match] = Editor.nodes(editor, { | ||
match: n => getNodeType(n) === ELementTypes.LINK, | ||
universal: true, | ||
}); | ||
if (!match) return null; | ||
const [node, path] = match; | ||
const showedText = getEditorString(editor, path); | ||
return { | ||
linkUrl: node.url, | ||
linkTitle: showedText || node.title, | ||
path: path, | ||
}; | ||
}; | ||
|
||
export const updateLink = (editor, newUrl, newText) => { | ||
const linkAbove = getAboveNode(editor, { match: { type: ELementTypes.LINK } }); | ||
if (!linkAbove) return; | ||
const { href: oldUrl, title: oldText } = linkAbove[0] || {}; | ||
if (oldUrl !== newUrl || oldText !== newText) { | ||
Transforms.setNodes(editor, { url: newUrl, title: newText }, { at: linkAbove[1] }); | ||
} | ||
upsertLinkText(editor, { text: newText }); | ||
}; | ||
|
||
export const upsertLinkText = (editor, { text }) => { | ||
const newLink = getAboveNode(editor, { match: { type: ELementTypes.LINK } }); | ||
if (!newLink) return; | ||
const [newLInkNode, newLinkPath] = newLink; | ||
if ((text && text.length) && text !== getEditorString(editor, newLinkPath)) { | ||
const firstText = newLInkNode.children[0]; | ||
replaceNodeChildren(editor, { | ||
at: newLinkPath, | ||
nodes: { ...firstText, text }, | ||
insertOptions: { | ||
select: true | ||
} | ||
}); | ||
} | ||
}; | ||
|
||
export const unWrapLinkNode = async (editor) => { | ||
if (editor.selection == null) return; | ||
|
||
const [linkNode] = Editor.nodes(editor, { | ||
match: n => getNodeType(n) === ELementTypes.LINK, | ||
universal: true, | ||
}); | ||
// Check selection is link node | ||
if (!linkNode || !linkNode[0]) return; | ||
|
||
Transforms.unwrapNodes(editor, { | ||
match: n => getNodeType(n) === ELementTypes.LINK, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { LINK } from '../../constants/element-types'; | ||
import LinkMenu from './menu'; | ||
import withLink from './plugin'; | ||
import renderLink from './render-elem'; | ||
|
||
const LinkPlugin = { | ||
type: LINK, | ||
nodeType: 'element', | ||
editorMenus: [LinkMenu], | ||
editorPlugin: withLink, | ||
renderElements: [renderLink], | ||
}; | ||
|
||
export default LinkPlugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||
import { Editor } from 'slate'; | ||
import MenuItem from '../../../commons/menu/menu-item'; | ||
import { MENUS_CONFIG_MAP } from '../../../constants/menus-config'; | ||
import { LINK } from '../../../constants/element-types'; | ||
import { isLinkType, isMenuDisabled, unWrapLinkNode } from '../helper'; | ||
import EventBus from '../../../../utils/event-bus'; | ||
import LinkModal from './link-modal'; | ||
import { INTERNAL_EVENTS } from '../../../../constants/event-types'; | ||
|
||
const menuConfig = MENUS_CONFIG_MAP[LINK]; | ||
|
||
const LinkMenu = ({ isRichEditor, className, readonly, editor }) => { | ||
const [isOpenLinkModal, setIsOpenLinkModal] = useState(false); | ||
const [linkInfo, setLinkInfo] = useState({ linkTitle: '', linkUrl: '' }); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
const isLinkActive = useMemo(() => isLinkType(editor), [editor.selection]); | ||
|
||
useEffect(() => { | ||
const eventBus = EventBus.getInstance(); | ||
const unsubscribe = eventBus.subscribe(INTERNAL_EVENTS.ON_OPEN_LINK_POPOVER, handleOpenLinkModal); | ||
return () => unsubscribe(); | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (isLinkType(editor)) { | ||
const newTitle = editor.selection && Editor.string(editor, editor.selection); | ||
newTitle && setLinkInfo({ ...linkInfo, linkTitle: newTitle }); | ||
} | ||
// eslint-disable-next-line react-hooks/exhaustive-deps | ||
}, [editor.selection]); | ||
|
||
const handleOpenLinkModal = useCallback((linkInfo) => { | ||
Reflect.ownKeys.length && setLinkInfo(linkInfo); | ||
setIsOpenLinkModal(true); | ||
}, [setIsOpenLinkModal, setLinkInfo]); | ||
|
||
const onMouseDown = useCallback((event) => { | ||
event.preventDefault(); | ||
event.stopPropagation(); | ||
if (isLinkActive) { | ||
isLinkActive && unWrapLinkNode(editor); | ||
return; | ||
} | ||
if (editor.selection) { | ||
const selectedText = Editor.string(editor, editor.selection); | ||
setLinkInfo({ ...linkInfo, linkTitle: selectedText }); | ||
} | ||
setIsOpenLinkModal(true); | ||
document.getElementById(`seafile_${LINK}`).blur(); | ||
}, [editor, isLinkActive, linkInfo]); | ||
|
||
const onCloseModal = useCallback(() => { | ||
setIsOpenLinkModal(false); | ||
setLinkInfo({ linkTitle: '', linkUrl: '' }); | ||
}, []); | ||
|
||
return ( | ||
<> | ||
<MenuItem | ||
isRichEditor={isRichEditor} | ||
className={className} | ||
disabled={isMenuDisabled(editor, readonly)} | ||
isActive={isLinkActive} | ||
onMouseDown={onMouseDown} | ||
|
||
{...menuConfig} | ||
/> | ||
{isOpenLinkModal && ( | ||
<LinkModal | ||
onCloseModal={onCloseModal} | ||
editor={editor} | ||
linkTitle={linkInfo.linkTitle} | ||
linkUrl={linkInfo.linkUrl} | ||
/>)} | ||
</> | ||
); | ||
}; | ||
|
||
export default LinkMenu; |
Oops, something went wrong.