diff --git a/docs/client/components/pages/InlineToolbar/CustomInlineToolbarEditor/index.js b/docs/client/components/pages/InlineToolbar/CustomInlineToolbarEditor/index.js
index 75fe778597..54278295e4 100644
--- a/docs/client/components/pages/InlineToolbar/CustomInlineToolbarEditor/index.js
+++ b/docs/client/components/pages/InlineToolbar/CustomInlineToolbarEditor/index.js
@@ -3,6 +3,8 @@ import React, { Component } from 'react';
import Editor, { createEditorStateWithText } from 'draft-js-plugins-editor';
// eslint-disable-next-line import/no-unresolved
import createInlineToolbarPlugin, { Separator } from 'draft-js-inline-toolbar-plugin';
+// eslint-disable-next-line import/no-unresolved
+import createLinkifyPlugin from 'draft-js-linkify-plugin'; // eslint-disable-line import/no-unresolved
import {
ItalicButton,
BoldButton,
@@ -15,9 +17,17 @@ import {
OrderedListButton,
BlockquoteButton,
CodeBlockButton,
-} from 'draft-js-buttons'; // eslint-disable-line import/no-unresolved
+ AddLinkButton,
+} from '../../../../../../draft-js-buttons/src/'; // eslint-disable-line import/no-unresolved
import editorStyles from './editorStyles.css';
+let linkAddElement = null;
+let inlineToolbarElement = null;
+
+const addLink = () => {
+ linkAddElement.openPopover();
+};
+
const inlineToolbarPlugin = createInlineToolbarPlugin({
structure: [
BoldButton,
@@ -32,10 +42,14 @@ const inlineToolbarPlugin = createInlineToolbarPlugin({
OrderedListButton,
BlockquoteButton,
CodeBlockButton,
- ]
+ AddLinkButton,
+ ],
+ addLink,
});
const { InlineToolbar } = inlineToolbarPlugin;
-const plugins = [inlineToolbarPlugin];
+const linkifyPlugin = createLinkifyPlugin();
+const { LinkAdd } = linkifyPlugin;
+const plugins = [inlineToolbarPlugin, linkifyPlugin];
const text = 'In this editor a toolbar shows up once you select part of the text …';
export default class CustomInlineToolbarEditor extends Component {
@@ -56,14 +70,24 @@ export default class CustomInlineToolbarEditor extends Component {
render() {
return (
-
-
+
+ { this.editor = element; }}
+ />
+ { inlineToolbarElement = element; }}
+ />
+
+ { linkAddElement = element; }}
editorState={this.state.editorState}
onChange={this.onChange}
- plugins={plugins}
- ref={(element) => { this.editor = element; }}
+ inlineToolbarElement={inlineToolbarElement}
/>
-
);
}
diff --git a/draft-js-buttons/src/components/AddLinkButton/index.js b/draft-js-buttons/src/components/AddLinkButton/index.js
new file mode 100644
index 0000000000..884396fe6a
--- /dev/null
+++ b/draft-js-buttons/src/components/AddLinkButton/index.js
@@ -0,0 +1,8 @@
+import React from 'react';
+import createLinkButton from '../../utils/createLinkButton';
+
+export default createLinkButton({
+ children: (
+ 🔗
+ ),
+});
diff --git a/draft-js-buttons/src/index.js b/draft-js-buttons/src/index.js
index 68cebcaeee..c5d87e2750 100644
--- a/draft-js-buttons/src/index.js
+++ b/draft-js-buttons/src/index.js
@@ -16,6 +16,7 @@ import AlignBlockCenterButton from './components/AlignBlockCenterButton';
import AlignBlockLeftButton from './components/AlignBlockLeftButton';
import AlignBlockRightButton from './components/AlignBlockRightButton';
import AddImageButton from './components/AddImageButton';
+import AddLinkButton from './components/AddLinkButton';
export {
createBlockStyleButton,
@@ -36,4 +37,5 @@ export {
AlignBlockLeftButton,
AlignBlockRightButton,
AddImageButton,
+ AddLinkButton,
};
diff --git a/draft-js-buttons/src/utils/createLinkButton.js b/draft-js-buttons/src/utils/createLinkButton.js
new file mode 100644
index 0000000000..915bda0a18
--- /dev/null
+++ b/draft-js-buttons/src/utils/createLinkButton.js
@@ -0,0 +1,32 @@
+import React, { Component } from 'react';
+
+export default ({ children }) => (
+ class linkButton extends Component {
+
+ activate = (event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ this.props.addLink();
+ }
+
+ preventBubblingUp = (event) => { event.preventDefault(); }
+
+ render() {
+ const { theme } = this.props;
+ return (
+
+
+
+
+ );
+ }
+ }
+);
diff --git a/draft-js-inline-toolbar-plugin/src/components/Toolbar/index.js b/draft-js-inline-toolbar-plugin/src/components/Toolbar/index.js
index 6d58cd6aba..77b2c49a67 100644
--- a/draft-js-inline-toolbar-plugin/src/components/Toolbar/index.js
+++ b/draft-js-inline-toolbar-plugin/src/components/Toolbar/index.js
@@ -50,6 +50,7 @@ export default class Toolbar extends React.Component {
theme={theme.buttonStyles}
getEditorState={store.getItem('getEditorState')}
setEditorState={store.getItem('setEditorState')}
+ addLink={store.getItem('addLink')}
/>
))}
diff --git a/draft-js-inline-toolbar-plugin/src/index.js b/draft-js-inline-toolbar-plugin/src/index.js
index b4dec09c03..fdead276f4 100644
--- a/draft-js-inline-toolbar-plugin/src/index.js
+++ b/draft-js-inline-toolbar-plugin/src/index.js
@@ -14,12 +14,11 @@ import toolbarStyles from './toolbarStyles.css';
const createInlineToolbarPlugin = (config = {}) => {
const defaultTheme = { buttonStyles, toolbarStyles };
- const store = createStore({
- isVisible: false,
- });
+ const defaultaddLink = undefined;
const {
theme = defaultTheme,
+ addLink = defaultaddLink,
structure = [
BoldButton,
ItalicButton,
@@ -28,6 +27,11 @@ const createInlineToolbarPlugin = (config = {}) => {
]
} = config;
+ const store = createStore({
+ isVisible: false,
+ addLink
+ });
+
const toolbarProps = {
store,
structure,
diff --git a/draft-js-linkify-plugin/src/LinkAdd/index.js b/draft-js-linkify-plugin/src/LinkAdd/index.js
new file mode 100644
index 0000000000..95ea10aa7a
--- /dev/null
+++ b/draft-js-linkify-plugin/src/LinkAdd/index.js
@@ -0,0 +1,130 @@
+import React, { Component } from 'react';
+import ReactDOM from 'react-dom';
+import linkifyIt from 'linkify-it';
+import modifier from '../modifiers/addLink';
+import styles from './styles.css';
+
+const linkify = linkifyIt();
+
+export default class LinkAdd extends Component {
+ // Start the popover closed
+ state = {
+ url: '',
+ open: false,
+ linkError: false
+ };
+
+ // When the popover is open and users click anywhere on the page,
+ // the popover should close
+ componentDidMount() {
+ document.addEventListener('click', this.closePopover);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('click', this.closePopover);
+ }
+
+ onPopoverClick = () => {
+ this.preventNextClose = true;
+ }
+
+ onKeyDown(e) {
+ if (e.keyCode === 13) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.addLink();
+ }
+ }
+
+ setPosition = (toolbarElement) => {
+ const position = {
+ top: toolbarElement.offsetTop,
+ left: toolbarElement.offsetLeft,
+ width: toolbarElement.offsetWidth,
+ transform: 'translate(-50%) scale(1)',
+ transition: 'transform 0.15s cubic-bezier(.3,1.2,.2,1)',
+ };
+ this.setState({ position });
+ }
+
+ openPopover = () => {
+ if (!this.state.open) {
+ this.preventNextClose = true;
+ // eslint-disable-next-line react/no-find-dom-node
+ const toolbarElement = ReactDOM.findDOMNode(this.props.inlineToolbarElement);
+ this.setPosition(toolbarElement);
+ setTimeout(() => {
+ setTimeout(() => this.inputElement.focus(), 0);
+ this.setState({ open: true, });
+ }, 0);
+ }
+ };
+
+ closePopover = () => {
+ if (!this.preventNextClose && this.state.open) {
+ this.setState({ open: false });
+ }
+
+ this.preventNextClose = false;
+ };
+
+ addLink = () => {
+ const { editorState, onChange } = this.props;
+ const { url } = this.state;
+ if (linkify.test(url)) {
+ this.setState({ linkError: false });
+ onChange(modifier(editorState, url));
+ this.closePopover();
+ } else {
+ this.setState({ linkError: true });
+ }
+ };
+
+ changeUrl = (evt) => {
+ this.setState({ url: evt.target.value });
+ }
+
+ render() {
+ const popoverClassName = this.state.open ?
+ styles.addLinkPopover :
+ styles.addLinkClosedPopover;
+
+ const inputClassName = this.state.linkError ?
+ `${styles.addLinkInput} ${styles.addLinkInputError}` :
+ styles.addLinkInput;
+
+ return (
+
+
+ { this.inputElement = element; }}
+ type="text"
+ placeholder="Paste the link url …"
+ className={inputClassName}
+ onChange={this.changeUrl}
+ onKeyDown={(e) => this.onKeyDown(e)}
+ value={this.state.url}
+ />
+
+
+
+
+ );
+ }
+}
diff --git a/draft-js-linkify-plugin/src/LinkAdd/styles.css b/draft-js-linkify-plugin/src/LinkAdd/styles.css
new file mode 100644
index 0000000000..0e60a0aaf2
--- /dev/null
+++ b/draft-js-linkify-plugin/src/LinkAdd/styles.css
@@ -0,0 +1,126 @@
+.addLink {
+ background: #FFF;
+ display: inline-block;
+}
+
+.addLinkPopover {
+ background: #FFF;
+ position: absolute;
+ height: 54px;
+ width: 300px;
+ border-radius: 2px;
+ padding: 10px;
+ box-shadow: 0px 4px 30px 0px rgba(220,220,220,1);
+ z-index: 1000;
+ transition: 0.5s linear height;
+}
+
+.addLinkClosedPopover {
+ display: none;
+}
+
+.addLinkButton {
+ box-sizing: border-box;
+ background: #fff;
+ border: 1px solid #ddd;
+ padding: 0;
+ color: #888;
+ margin: 0;
+ border-radius: 1.5em;
+ cursor: pointer;
+ height: 1.5em;
+ width: 2.5em;
+ font-size: 1.5em;
+ line-height: 1.2em;
+ margin: 0;
+}
+
+.addLinkButton:focus {
+ outline: 0; /* reset for :focus */
+}
+
+.addLinkButton:hover {
+ background: #f3f3f3;
+}
+
+.addLinkButton:active {
+ background: #e6e6e6;
+}
+
+.addLinkPressedButton {
+ composes: addLinkButton;
+ background: #ededed;
+}
+
+.addLinkBottomGradient {
+ width: 100%;
+ height: 1em;
+ position: absolute;
+ bottom: 0px;
+ left: 0px;
+ right: 0px;
+ background-color: white;
+ pointer-events: none;
+ background: linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%);
+}
+
+.addLinkInput {
+ box-sizing: border-box;
+ border: 1px solid #ddd;
+ cursor: text;
+ padding: 4px;
+ width: 78%;
+ border-radius: 2px;
+ margin-bottom: 1em;
+ box-shadow: inset 0px 1px 8px -3px #ABABAB;
+ background: #fefefe;
+}
+
+.addLinkInput:focus {
+ outline: none !important;
+}
+
+.addLinkInputError {
+ color: darkred;
+ border: 1px solid darkred;
+ box-shadow: none;
+}
+
+.addLinkConfirmButton {
+ box-sizing: border-box;
+ background: #fff;
+ border: 1px solid #ddd;
+ padding: 0;
+ color: #888;
+ margin: 0;
+ border-radius: 0.4em;
+ cursor: pointer;
+ height: 2.1em;
+ width: 8%;
+ font-size: 1em;
+ line-height: 2.1em;
+ margin: 0;
+ margin-left: 1%;
+}
+
+.addLinkConfirmButton:focus {
+ outline: 0;
+}
+
+.addLinkConfirmButton:hover {
+ background: #f3f3f3;
+}
+
+.addLinkConfirmButton:active {
+ background: #e6e6e6;
+}
+
+.addLinkError {
+ opacity: 0;
+ color: red;
+}
+
+.addLinkErrorDisabled {
+ opacity: 1;
+ color: red;
+}
diff --git a/draft-js-linkify-plugin/src/index.js b/draft-js-linkify-plugin/src/index.js
index 28ae8462e5..bbd0f3f7cc 100644
--- a/draft-js-linkify-plugin/src/index.js
+++ b/draft-js-linkify-plugin/src/index.js
@@ -1,6 +1,8 @@
import decorateComponentWithProps from 'decorate-component-with-props';
import Link from './Link';
+import LinkAdd from './LinkAdd';
import linkStrategy from './linkStrategy';
+import linkifyStrategy from './linkifyStrategy';
import styles from './styles.css';
const defaultTheme = {
@@ -23,11 +25,16 @@ const linkPlugin = (config = {}) => {
return {
decorators: [
+ {
+ strategy: linkifyStrategy,
+ component: decorateComponentWithProps(Link, { theme, target, component }),
+ },
{
strategy: linkStrategy,
component: decorateComponentWithProps(Link, { theme, target, component }),
},
],
+ LinkAdd,
};
};
diff --git a/draft-js-linkify-plugin/src/linkStrategy.js b/draft-js-linkify-plugin/src/linkStrategy.js
index 4f37d8045e..9bdb43fcf6 100644
--- a/draft-js-linkify-plugin/src/linkStrategy.js
+++ b/draft-js-linkify-plugin/src/linkStrategy.js
@@ -1,17 +1,16 @@
-import linkifyIt from 'linkify-it';
-import tlds from 'tlds';
+import { Entity } from 'draft-js';
-const linkify = linkifyIt();
-linkify.tlds(tlds);
-
-// Gets all the links in the text, and returns them via the callback
-const linkStrategy = (contentBlock: Object, callback: Function) => {
- const links = linkify.match(contentBlock.get('text'));
- if (typeof links !== 'undefined' && links !== null) {
- for (let i = 0; i < links.length; i += 1) {
- callback(links[i].index, links[i].lastIndex);
- }
- }
-};
+function linkStrategy(contentBlock, cb) {
+ contentBlock.findEntityRanges(
+ (character) => {
+ const entityKey = character.getEntity();
+ return (
+ entityKey !== null &&
+ Entity.get(entityKey).getType() === 'LINK'
+ );
+ },
+ cb
+ );
+}
export default linkStrategy;
diff --git a/draft-js-linkify-plugin/src/linkifyStrategy.js b/draft-js-linkify-plugin/src/linkifyStrategy.js
new file mode 100644
index 0000000000..bac8a9f46a
--- /dev/null
+++ b/draft-js-linkify-plugin/src/linkifyStrategy.js
@@ -0,0 +1,17 @@
+import linkifyIt from 'linkify-it';
+import tlds from 'tlds';
+
+const linkify = linkifyIt();
+linkify.tlds(tlds);
+
+// Gets all the links in the text, and returns them via the callback
+const linkifyStrategy = (contentBlock: Object, callback: Function) => {
+ const links = linkify.match(contentBlock.get('text'));
+ if (typeof links !== 'undefined' && links !== null) {
+ for (let i = 0; i < links.length; i += 1) {
+ callback(links[i].index, links[i].lastIndex);
+ }
+ }
+};
+
+export default linkifyStrategy;
diff --git a/draft-js-linkify-plugin/src/modifiers/addLink.js b/draft-js-linkify-plugin/src/modifiers/addLink.js
new file mode 100644
index 0000000000..8da4a6e31f
--- /dev/null
+++ b/draft-js-linkify-plugin/src/modifiers/addLink.js
@@ -0,0 +1,19 @@
+import {
+ RichUtils,
+ Entity,
+ EditorState,
+} from 'draft-js';
+
+export default (editorState, url) => {
+ const entityKey = Entity.create('LINK', 'MUTABLE', { url });
+
+ const newEditorState = RichUtils.toggleLink(
+ editorState,
+ editorState.getSelection(),
+ entityKey,
+ );
+ return EditorState.forceSelection(
+ newEditorState,
+ editorState.getCurrentContent().getSelectionAfter()
+ );
+};