Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"private": true,
"workspaces": [
"slate-mention-plugin",
"slate-string-deserialize",
"slate-paste-url-plugin",
"playground"
Expand Down
2 changes: 1 addition & 1 deletion playground/.eslintcache
Original file line number Diff line number Diff line change
@@ -1 +1 @@
[{"/Users/temporary/Works/personal/slate-plugin/playground/src/App.tsx":"1","/Users/temporary/Works/personal/slate-plugin/playground/src/Stories/SlateStringDeserialize.tsx":"2","/Users/temporary/Works/personal/slate-plugin/playground/src/Stories/Button.tsx":"3"},{"size":556,"mtime":1609048817928,"results":"4","hashOfConfig":"5"},{"size":598,"mtime":1609049567508,"results":"6","hashOfConfig":"5"},{"size":941,"mtime":1609050074641,"results":"7","hashOfConfig":"5"},{"filePath":"8","messages":"9","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"yzs728",{"filePath":"10","messages":"11","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"12","messages":"13","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/temporary/Works/personal/slate-plugin/playground/src/App.tsx",[],"/Users/temporary/Works/personal/slate-plugin/playground/src/Stories/SlateStringDeserialize.tsx",[],"/Users/temporary/Works/personal/slate-plugin/playground/src/Stories/Button.tsx",[]]
[{"/Users/temporary/Works/personal/slate-plugin/playground/src/SlateMentionPlugin.stories.tsx":"1","/Users/temporary/Works/personal/slate-plugin/playground/src/SlatePasteUrl.stories.tsx":"2","/Users/temporary/Works/personal/slate-plugin/playground/src/SlateStringDeserialize.stories.tsx":"3","/Users/temporary/Works/personal/slate-plugin/playground/src/SlateStringDeserialize.tsx":"4","/Users/temporary/Works/personal/slate-plugin/playground/src/SlatePasteUrl.tsx":"5","/Users/temporary/Works/personal/slate-plugin/playground/src/SlateMentionPlugin.tsx":"6"},{"size":326,"mtime":1609569098831,"results":"7","hashOfConfig":"8"},{"size":444,"mtime":1609567894988,"results":"9","hashOfConfig":"8"},{"size":543,"mtime":1609054016105,"results":"10","hashOfConfig":"8"},{"size":788,"mtime":1609054016105,"results":"11","hashOfConfig":"8"},{"size":2425,"mtime":1609567894989,"results":"12","hashOfConfig":"8"},{"size":1188,"mtime":1609772574601,"results":"13","hashOfConfig":"8"},{"filePath":"14","messages":"15","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"16"},"1wli3a0",{"filePath":"17","messages":"18","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"16"},{"filePath":"19","messages":"20","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"16"},{"filePath":"21","messages":"22","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"16"},{"filePath":"23","messages":"24","errorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"25"},{"filePath":"26","messages":"27","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"16"},"/Users/temporary/Works/personal/slate-plugin/playground/src/SlateMentionPlugin.stories.tsx",[],["28","29"],"/Users/temporary/Works/personal/slate-plugin/playground/src/SlatePasteUrl.stories.tsx",[],"/Users/temporary/Works/personal/slate-plugin/playground/src/SlateStringDeserialize.stories.tsx",[],"/Users/temporary/Works/personal/slate-plugin/playground/src/SlateStringDeserialize.tsx",[],"/Users/temporary/Works/personal/slate-plugin/playground/src/SlatePasteUrl.tsx",["30"],"import {useState, useMemo, FC} from 'react'\nimport {createEditor, Node} from 'slate'\nimport {Slate, Editable, withReact, DefaultElement} from 'slate-react'\nimport {usePasteUrl, Options} from 'slate-paste-url-plugin'\n\nexport type Props = {\n defaultType?: string\n patterns?:\n | {capture: string; type?: string}\n | {capture: string; type?: string}[]\n}\n\nconst Editor: FC<Props> = (props) => {\n const options: Options = {\n defaultType: props.defaultType,\n patterns: props.patterns\n ? Array.isArray(props.patterns)\n ? props.patterns.map((pattern) => ({\n type: pattern.type,\n capture: new RegExp(pattern.capture, 'i'),\n }))\n : {\n type: props.patterns.type,\n capture: new RegExp(props.patterns.capture, 'i'),\n }\n : undefined,\n }\n const withPasteUrl = usePasteUrl(options)\n const editor = useMemo(() => withPasteUrl(withReact(createEditor())), [])\n const [value, setValue] = useState<Node[]>([\n {\n type: 'paragraph',\n children: [\n {\n text: `Slate paste url example, try block some text and paste url or github url to the blocked text.\n `,\n },\n ],\n },\n {\n type: 'paragraph',\n children: [\n {\n text:\n 'To change how the url rendered, edit the renderElement in SlatePasteUrl.tsx',\n },\n ],\n },\n ])\n\n return (\n <Slate\n editor={editor}\n value={value}\n onChange={(newValue) => setValue(newValue)}\n >\n <Editable\n renderElement={(props) => {\n switch (props.element.type) {\n case 'link': {\n return (\n <a\n href={props.element.link as string}\n style={{fontWeight: 'bold'}}\n {...props.attributes}\n >\n {props.children}\n </a>\n )\n }\n case 'github_link': {\n return (\n <a\n href={props.element.link as string}\n style={{fontWeight: 'bold', color: 'blue'}}\n {...props.attributes}\n >\n {props.children}\n </a>\n )\n }\n default: {\n return <DefaultElement {...props} />\n }\n }\n }}\n />\n </Slate>\n )\n}\n\nexport default Editor\n","/Users/temporary/Works/personal/slate-plugin/playground/src/SlateMentionPlugin.tsx",[],{"ruleId":"31","replacedBy":"32"},{"ruleId":"33","replacedBy":"34"},{"ruleId":"35","severity":1,"message":"36","line":29,"column":73,"nodeType":"37","endLine":29,"endColumn":75,"suggestions":"38"},"no-native-reassign",["39"],"no-negated-in-lhs",["40"],"react-hooks/exhaustive-deps","React Hook useMemo has a missing dependency: 'withPasteUrl'. Either include it or remove the dependency array.","ArrayExpression",["41"],"no-global-assign","no-unsafe-negation",{"desc":"42","fix":"43"},"Update the dependencies array to be: [withPasteUrl]",{"range":"44","text":"45"},[939,941],"[withPasteUrl]"]
1 change: 1 addition & 0 deletions playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"slate": "^0.59.0",
"slate-history": "^0.59.0",
"slate-react": "^0.59.0",
"slate-mention-plugin": "0.0.1",
"slate-paste-url-plugin": "0.0.1",
"slate-string-deserialize": "1.0.4",
"typescript": "^4.0.3",
Expand Down
13 changes: 13 additions & 0 deletions playground/src/SlateMentionPlugin.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react'
import {Story, Meta} from '@storybook/react/types-6-0'
import Editor from './SlateMentionPlugin'

export default {
component: Editor,
title: 'slate-mention-plugin',
} as Meta

const Template: Story<any> = (args) => <Editor {...args} />

export const Default = Template.bind({})
Default.args = {}
50 changes: 50 additions & 0 deletions playground/src/SlateMentionPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {useState, useMemo, FC} from 'react'
import {createEditor, Node} from 'slate'
import {Slate, Editable, withReact, DefaultLeaf} from 'slate-react'
import {decorate, renderLeafFN} from 'slate-mention-plugin'

const users = ['Mallory', 'Amanda', 'Adele', 'Moira', 'Cassie']
const fetchSuggestion = async (mention: string) => {
return users
.filter((user) => user.toLowerCase().indexOf(mention) !== -1)
.map((user) => ({
label: user,
value: {
user,
},
}))
}

const Editor: FC<any> = () => {
const editor = useMemo(() => withReact(createEditor()), [])
const [value, setValue] = useState<Node[]>([
{
type: 'paragraph',
children: [
{
text: `coba mention `,
},
],
},
])

return (
<Slate
editor={editor}
value={value}
onChange={(newValue) => setValue(newValue)}
>
<Editable
renderLeaf={(props) => {
const mentionLeaf = renderLeafFN({
fetchSuggestion,
})(props)
return mentionLeaf || <DefaultLeaf {...props} />
}}
decorate={decorate(editor)}
/>
</Slate>
)
}

export default Editor
8 changes: 8 additions & 0 deletions slate-mention-plugin/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"endOfLine": "lf",
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"bracketSpacing": false
}
21 changes: 21 additions & 0 deletions slate-mention-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "slate-mention-plugin",
"version": "0.0.1",
"main": "dist/index.js",
"author": "imdbsd",
"license": "MIT",
"scripts": {
"build": "rm -rf ./dist && tsc -p . && cp ./src/MentionModal/styles.css ./dist/MentionModal/styles.css",
"prepublish": "rm -rf ./dist && yarn build",
"publish:major": "yarn publish --major --message Release",
"publish:patch": "yarn publish --patch --message 'Release patch'"
},
"devDependencies": {
"typescript": "^4.1.3"
},
"peerDependencies": {
"react": "^17.0.1",
"slate": "^0.59.0",
"slate-react": "^0.59.0"
}
}
5 changes: 5 additions & 0 deletions slate-mention-plugin/src/MentionModal/Loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as React from 'react'

const Loader = () => <div className="mention-modal__loader" />

export default Loader
65 changes: 65 additions & 0 deletions slate-mention-plugin/src/MentionModal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react'
import {useEditor} from 'slate-react'
import useFetchSuggestions from './useFetchSuggestions'
import Loader from './Loader'
import Suggestion, {SuggestionType} from './Suggestion'
import './styles.css'
import {Editor} from 'slate'

export type FetchFN = (mentionAt: string) => Promise<SuggestionType[]>

export type Props = {
fetchSuggestion: FetchFN
}

const Modal: React.FC<Props & {mentionAt: string}> = (props) => {
const editor = useEditor()
const mentionPathRef = React.useMemo(() => {
if (editor.selection) {
return Editor.rangeRef(
editor,
{
anchor: editor.selection.anchor,
focus: editor.selection.focus,
type: 'mention-ref',
},
{
affinity: 'inward',
}
)
}
}, [editor])
console.log({mentionPathRef})
const [loading, suggestions] = useFetchSuggestions(
props.fetchSuggestion,
props.mentionAt
)

const renderSuggestions = React.useCallback(() => {
if (loading) {
return <Loader />
}
if (!loading && suggestions.length === 0) {
return <span className="mention-modal__not-found">not found...</span>
}
return (
<React.Fragment>
{suggestions.map((suggestion) => (
<Suggestion
key={`suggestion-for-${suggestion.label}`}
mentionAt={props.mentionAt}
{...suggestion}
/>
))}
</React.Fragment>
)
}, [suggestions, loading])

return (
<div contentEditable={false} className="mention-modal">
{renderSuggestions()}
</div>
)
}

export default Modal
35 changes: 35 additions & 0 deletions slate-mention-plugin/src/MentionModal/Suggestion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react'
import {RangeRef} from 'slate'
import {useEditor, useSlate} from 'slate-react'
import {insertMention} from '../commands'

export type SuggestionType = {
label: string
value: any
}

type Props = SuggestionType & {
mentionAt: string
}

const Suggestion: React.FC<Props> = (props) => {
const {label, value, mentionAt} = props
const editor = useEditor()
console.log('editor: ', editor.selection)
const handleOnClick = React.useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.preventDefault()
console.log({label, value})
console.log('editor on click', editor.selection)
insertMention(editor, mentionAt, label, value)
},
[label, value, editor]
)
return (
<div className="mention-modal__suggestion" onClick={handleOnClick}>
<span>{label}</span>
</div>
)
}

export default Suggestion
61 changes: 61 additions & 0 deletions slate-mention-plugin/src/MentionModal/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.mention-modal {
min-width: 100px;
position: absolute;
left: 0;
border: 1px solid #cfcfcf;
background-color: white;
border-radius: 5px;
padding: 10px;
display: flex;
justify-content: center;
margin-top: 5px;
flex-direction: column;
}

.mention-modal__not-found {
color: gray;
font-family: inherit;
}

.mention-modal__loader {
border: 5px solid #f3f3f3; /* Light grey */
border-top: 5px solid #3498db; /* Blue */
border-radius: 50%;
width: 15px;
height: 15px;
animation: spin 2s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

.mention-modal__suggestion {
color: black;
font-family: inherit;
margin-left: -10px;
margin-right: -10px;
padding: 10px;
}

.mention-modal__suggestion:first-child {
margin-top: -10px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
}

.mention-modal__suggestion:last-child {
margin-bottom: -10px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}

.mention-modal__suggestion:hover {
background-color: #f3f3f3;
cursor: pointer;
}
33 changes: 33 additions & 0 deletions slate-mention-plugin/src/MentionModal/useFetchSuggestions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react'
import {FetchFN} from './Modal'
import {SuggestionType} from './Suggestion'

const useFetchSuggestions = (
fetchSuggestion: FetchFN,
mentionAt: string
): [loading: boolean, suggestions: SuggestionType[]] => {
const [loading, setLoading] = React.useState<boolean>(false)
const [suggestions, setSuggestions] = React.useState<SuggestionType[]>([])
const fetchTimeout = React.useRef<NodeJS.Timeout | undefined>()

const handleFetchSuggestions = React.useCallback(() => {
setLoading(true)
fetchSuggestion(mentionAt).then((result) => {
setSuggestions(result)
})
setLoading(false)
}, [fetchSuggestion, mentionAt])

React.useEffect(() => {
if (fetchTimeout.current) {
clearTimeout(fetchTimeout.current)
}
fetchTimeout.current = setTimeout(() => {
handleFetchSuggestions()
}, 500)
}, [handleFetchSuggestions])

return [loading, suggestions]
}

export default useFetchSuggestions
18 changes: 18 additions & 0 deletions slate-mention-plugin/src/RenderLeaf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as React from 'react'
import {RenderLeafProps} from 'slate-react'
import MentionModal, {Props} from './MentionModal/Modal'

const RenderLeaf: React.FC<RenderLeafProps & Props> = (props) => {
const mentionAt = props.leaf.text.substr(1) // removing '@'
return (
<span style={{color: 'blue', position: 'relative'}} {...props.attributes}>
{props.children}
<MentionModal
mentionAt={mentionAt}
fetchSuggestion={props.fetchSuggestion}
/>
</span>
)
}

export default RenderLeaf
31 changes: 31 additions & 0 deletions slate-mention-plugin/src/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Transforms, Editor} from 'slate'
import {MENTION_ELEMENT} from './index'

export const insertMention = (
editor: Editor,
mentionAt: string,
text: string,
mentionValue: any
) => {
console.log(editor.selection)
if (editor.selection) {
Transforms.delete(editor, {
at: {
anchor: {
path: editor.selection.anchor.path,
offset: editor.selection.anchor.offset - (mentionAt.length + 1),
},
focus: editor.selection.focus,
},
})
Transforms.insertNodes(editor, {
type: MENTION_ELEMENT,
mentionValue,
children: [
{
text,
},
],
})
}
}
Loading