From 02645c833c0cc8c9698b36e2b86366b9ddb85fbd Mon Sep 17 00:00:00 2001 From: StevenIseki Date: Tue, 13 Dec 2016 15:48:39 +1100 Subject: [PATCH] Add links from button to linkify --- .../CustomInlineToolbarEditor/index.js | 40 ++++-- .../src/components/AddLinkButton/index.js | 8 ++ draft-js-buttons/src/index.js | 2 + .../src/utils/createLinkButton.js | 32 +++++ .../src/components/Toolbar/index.js | 1 + draft-js-inline-toolbar-plugin/src/index.js | 10 +- draft-js-linkify-plugin/src/LinkAdd/index.js | 130 ++++++++++++++++++ .../src/LinkAdd/styles.css | 126 +++++++++++++++++ draft-js-linkify-plugin/src/index.js | 7 + draft-js-linkify-plugin/src/linkStrategy.js | 27 ++-- .../src/linkifyStrategy.js | 17 +++ .../src/modifiers/addLink.js | 19 +++ 12 files changed, 394 insertions(+), 25 deletions(-) create mode 100644 draft-js-buttons/src/components/AddLinkButton/index.js create mode 100644 draft-js-buttons/src/utils/createLinkButton.js create mode 100644 draft-js-linkify-plugin/src/LinkAdd/index.js create mode 100644 draft-js-linkify-plugin/src/LinkAdd/styles.css create mode 100644 draft-js-linkify-plugin/src/linkifyStrategy.js create mode 100644 draft-js-linkify-plugin/src/modifiers/addLink.js 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() + ); +};