Skip to content

Commit a6685d0

Browse files
committed
Implement link
1 parent 88f190e commit a6685d0

File tree

8 files changed

+177
-63
lines changed

8 files changed

+177
-63
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from 'react';
2+
import { Entity } from 'draft-js';
3+
4+
const Link = (props) => {
5+
const { href, title } = Entity.get(props.entityKey).getData();
6+
return (
7+
<a href={href} title={title}>
8+
{props.children}
9+
</a>
10+
);
11+
};
12+
13+
export default Link;

src/decorators/link/index.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import createLinkStrategy from './linkStrategy';
2+
import Link from './components/Link';
3+
4+
const createLinkDecorator = (config, store) => ({
5+
strategy: createLinkStrategy(config, store),
6+
component: Link,
7+
});
8+
9+
export default createLinkDecorator;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { Entity } from 'draft-js';
2+
3+
const createLinkStrategy = () => {
4+
const findLinkEntities = (contentBlock, callback) => {
5+
contentBlock.findEntityRanges((character) => {
6+
const entityKey = character.getEntity();
7+
return (
8+
entityKey !== null &&
9+
Entity.get(entityKey).getType() === 'LINK'
10+
);
11+
}, callback);
12+
};
13+
return findLinkEntities;
14+
};
15+
16+
export default createLinkStrategy;

src/index.js

Lines changed: 70 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,65 +7,78 @@ import handleBlockType from './modifiers/handleBlockType';
77
import handleInlineStyle from './modifiers/handleInlineStyle';
88
import handleNewCodeBlock from './modifiers/handleNewCodeBlock';
99
import insertEmptyBlock from './modifiers/insertEmptyBlock';
10+
import handleLink from './modifiers/handleLink';
11+
import createLinkDecorator from './decorators/link';
1012

11-
const createMarkdownShortcutsPlugin = () => ({
12-
blockRenderMap,
13-
handleReturn(ev, { setEditorState, getEditorState }) {
14-
const editorState = getEditorState();
15-
let newEditorState = handleNewCodeBlock(editorState);
16-
if (editorState === newEditorState && (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey)) {
17-
newEditorState = insertEmptyBlock(editorState);
18-
}
19-
if (editorState !== newEditorState) {
20-
setEditorState(newEditorState);
21-
return 'handled';
22-
}
23-
return 'not-handled';
24-
},
25-
blockStyleFn(block) {
26-
if (block.getType() === CHECKABLE_LIST_ITEM) {
27-
return CHECKABLE_LIST_ITEM;
28-
}
29-
return null;
30-
},
31-
blockRendererFn(block, { setEditorState, getEditorState }) {
32-
if (block.getType() === CHECKABLE_LIST_ITEM) {
33-
return {
34-
component: CheckableListItem,
35-
props: {
36-
onChangeChecked: () => setEditorState(
37-
CheckableListItemUtils.toggleChecked(getEditorState(), block)
38-
),
39-
checked: !!block.getData().get('checked'),
40-
},
41-
};
42-
}
43-
return null;
44-
},
45-
onTab(ev, { getEditorState, setEditorState }) {
46-
const editorState = getEditorState();
47-
const newEditorState = adjustBlockDepth(editorState, ev);
48-
if (newEditorState !== editorState) {
49-
setEditorState(newEditorState);
50-
return 'handled';
51-
}
52-
return 'not-handled';
53-
},
54-
handleBeforeInput(character, { getEditorState, setEditorState }) {
55-
if (character !== ' ') {
13+
const createMarkdownShortcutsPlugin = (config = {}) => {
14+
const store = {};
15+
return {
16+
blockRenderMap,
17+
decorators: [createLinkDecorator(config, store)],
18+
initialize({ setEditorState, getEditorState }) {
19+
store.setEditorState = setEditorState;
20+
store.getEditorState = getEditorState;
21+
},
22+
handleReturn(ev, { setEditorState, getEditorState }) {
23+
const editorState = getEditorState();
24+
let newEditorState = handleNewCodeBlock(editorState);
25+
if (editorState === newEditorState && (ev.ctrlKey || ev.shiftKey || ev.metaKey || ev.altKey)) {
26+
newEditorState = insertEmptyBlock(editorState);
27+
}
28+
if (editorState !== newEditorState) {
29+
setEditorState(newEditorState);
30+
return 'handled';
31+
}
32+
return 'not-handled';
33+
},
34+
blockStyleFn(block) {
35+
if (block.getType() === CHECKABLE_LIST_ITEM) {
36+
return CHECKABLE_LIST_ITEM;
37+
}
38+
return null;
39+
},
40+
blockRendererFn(block, { setEditorState, getEditorState }) {
41+
if (block.getType() === CHECKABLE_LIST_ITEM) {
42+
return {
43+
component: CheckableListItem,
44+
props: {
45+
onChangeChecked: () => setEditorState(
46+
CheckableListItemUtils.toggleChecked(getEditorState(), block)
47+
),
48+
checked: !!block.getData().get('checked'),
49+
},
50+
};
51+
}
52+
return null;
53+
},
54+
onTab(ev, { getEditorState, setEditorState }) {
55+
const editorState = getEditorState();
56+
const newEditorState = adjustBlockDepth(editorState, ev);
57+
if (newEditorState !== editorState) {
58+
setEditorState(newEditorState);
59+
return 'handled';
60+
}
61+
return 'not-handled';
62+
},
63+
handleBeforeInput(character, { getEditorState, setEditorState }) {
64+
if (character !== ' ') {
65+
return 'not-handled';
66+
}
67+
const editorState = getEditorState();
68+
let newEditorState = handleBlockType(editorState, character);
69+
if (editorState === newEditorState) {
70+
newEditorState = handleInlineStyle(editorState, character);
71+
}
72+
if (editorState === newEditorState) {
73+
newEditorState = handleLink(editorState, character);
74+
}
75+
if (editorState !== newEditorState) {
76+
setEditorState(newEditorState);
77+
return 'handled';
78+
}
5679
return 'not-handled';
5780
}
58-
const editorState = getEditorState();
59-
let newEditorState = handleBlockType(editorState, character);
60-
if (editorState === newEditorState) {
61-
newEditorState = handleInlineStyle(editorState, character);
62-
}
63-
if (editorState !== newEditorState) {
64-
setEditorState(newEditorState);
65-
return 'handled';
66-
}
67-
return 'not-handled';
68-
}
69-
});
81+
}
82+
};
7083

7184
export default createMarkdownShortcutsPlugin;

src/modifiers/changeCurrentInlineStyle.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ const changeCurrentInlineStyle = (editorState, matchArr, style) => {
55
const selection = editorState.getSelection();
66
const key = selection.getStartKey();
77
const { index } = matchArr;
8-
const focusOffset = index + matchArr[0].length;
98
const blockMap = currentContent.getBlockMap();
109
const block = blockMap.get(key);
1110
const currentInlineStyle = block.getInlineStyleAt(index).merge();
1211
const newStyle = currentInlineStyle.merge([style]);
12+
const focusOffset = index + matchArr[0].length;
1313
const wordSelection = SelectionState.createEmpty(key).merge({
1414
anchorOffset: index,
1515
focusOffset

src/modifiers/handleInlineStyle.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,19 @@ const handleInlineStyle = (editorState, character) => {
1818
const key = editorState.getSelection().getStartKey();
1919
const text = editorState.getCurrentContent().getBlockForKey(key).getText();
2020
const line = `${text}${character}`;
21-
let newEditorState = null;
21+
let newEditorState = editorState;
2222
Object.keys(inlineMatchers).some((k) => {
2323
inlineMatchers[k].some((re) => {
2424
let matchArr;
2525
do {
2626
matchArr = re.exec(line);
2727
if (matchArr) {
28-
newEditorState = changeCurrentInlineStyle(editorState, matchArr, k);
29-
return true;
28+
newEditorState = changeCurrentInlineStyle(newEditorState, matchArr, k);
3029
}
3130
} while (matchArr);
32-
return false;
31+
return newEditorState !== editorState;
3332
});
34-
return !!newEditorState;
33+
return newEditorState !== editorState;
3534
});
3635
return newEditorState || editorState;
3736
};

src/modifiers/handleLink.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import insertLink from './insertLink';
2+
3+
const handleLink = (editorState, character) => {
4+
const re = /\[([^\]]+)]\(([^)"]+)(?: "([^"]+)")?\)/g;
5+
const key = editorState.getSelection().getStartKey();
6+
const text = editorState.getCurrentContent().getBlockForKey(key).getText();
7+
const line = `${text}${character}`;
8+
let newEditorState = editorState;
9+
let matchArr;
10+
do {
11+
matchArr = re.exec(line);
12+
if (matchArr) {
13+
newEditorState = insertLink(newEditorState, matchArr);
14+
}
15+
} while (matchArr);
16+
return newEditorState;
17+
};
18+
19+
export default handleLink;

src/modifiers/insertLink.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { EditorState, RichUtils, SelectionState, Entity, Modifier } from 'draft-js';
2+
3+
4+
const insertLink = (editorState, matchArr) => {
5+
const currentContent = editorState.getCurrentContent();
6+
const selection = editorState.getSelection();
7+
const key = selection.getStartKey();
8+
const [
9+
matchText,
10+
text,
11+
href,
12+
title
13+
] = matchArr;
14+
const { index } = matchArr;
15+
const focusOffset = index + matchText.length;
16+
const wordSelection = SelectionState.createEmpty(key).merge({
17+
anchorOffset: index,
18+
focusOffset
19+
});
20+
const entityKey = Entity.create(
21+
'LINK',
22+
'MUTABLE',
23+
{ href, title }
24+
);
25+
let newContentState = Modifier.replaceText(
26+
currentContent,
27+
wordSelection,
28+
text,
29+
null,
30+
entityKey
31+
);
32+
newContentState = Modifier.insertText(
33+
newContentState,
34+
newContentState.getSelectionAfter(),
35+
' '
36+
);
37+
const newWordSelection = wordSelection.merge({
38+
focusOffset: index + text.length
39+
});
40+
let newEditorState = EditorState.push(editorState, newContentState, 'insert-link');
41+
newEditorState = RichUtils.toggleLink(newEditorState, newWordSelection, entityKey);
42+
return EditorState.forceSelection(newEditorState, newContentState.getSelectionAfter());
43+
};
44+
45+
export default insertLink;

0 commit comments

Comments
 (0)