diff --git a/blocks/components/editable/format-toolbar.js b/blocks/components/editable/format-toolbar.js new file mode 100644 index 00000000000000..4dbbea38ee10d3 --- /dev/null +++ b/blocks/components/editable/format-toolbar.js @@ -0,0 +1,159 @@ +/** + * Internal dependencies + */ + // TODO: We mustn't import by relative path traversing from blocks to editor + // as we're doing here; instead, we should consider a common components path. +import IconButton from '../../../editor/components/icon-button'; +import Toolbar from '../../../editor/components/toolbar'; + +const FORMATTING_CONTROLS = [ + { + icon: 'editor-bold', + title: wp.i18n.__( 'Bold' ), + format: 'bold' + }, + { + icon: 'editor-italic', + title: wp.i18n.__( 'Italic' ), + format: 'italic' + }, + { + icon: 'editor-strikethrough', + title: wp.i18n.__( 'Strikethrough' ), + format: 'strikethrough' + } +]; + +class FormatToolbar extends wp.element.Component { + constructor( props ) { + super( ...arguments ); + this.state = { + linkValue: props.formats.link ? props.formats.link.value : '', + isEditingLink: false + }; + this.addLink = this.addLink.bind( this ); + this.editLink = this.editLink.bind( this ); + this.dropLink = this.dropLink.bind( this ); + this.submitLink = this.submitLink.bind( this ); + this.updateLinkValue = this.updateLinkValue.bind( this ); + } + + componentWillUnmout() { + if ( this.editTimeout ) { + clearTimeout( this.editTimeout ); + } + } + + componentWillReceiveProps( nextProps ) { + const newState = { + linkValue: nextProps.formats.link ? nextProps.formats.link.value : '' + }; + if ( + ! this.props.formats.link || + ! nextProps.formats.link || + this.props.formats.link.node !== nextProps.formats.link.node + ) { + newState.isEditingLink = false; + } + this.setState( newState ); + } + + toggleFormat( format ) { + return () => { + this.props.onChange( { + [ format ]: ! this.props.formats[ format ] + } ); + }; + } + + addLink() { + if ( ! this.props.formats.link ) { + this.props.onChange( { link: { value: '' } } ); + + // Debounce the call to avoid the reset in willReceiveProps + this.editTimeout = setTimeout( () => this.setState( { isEditingLink: true } ) ); + } + } + + dropLink() { + this.props.onChange( { link: undefined } ); + } + + editLink( event ) { + event.preventDefault(); + this.setState( { + isEditingLink: true + } ); + } + + submitLink( event ) { + event.preventDefault(); + this.props.onChange( { link: { value: this.state.linkValue } } ); + this.setState( { + isEditingLink: false + } ); + } + + updateLinkValue( event ) { + this.setState( { + linkValue: event.target.value + } ); + } + + render() { + const { formats, focusPosition } = this.props; + const linkStyle = focusPosition + ? { position: 'absolute', ...focusPosition } + : null; + + return ( +
+ ( { + ...control, + onClick: this.toggleFormat( control.format ), + isActive: !! formats[ control.format ] + } ) ) + .concat( [ { + icon: 'admin-links', + title: wp.i18n.__( 'Link' ), + onClick: this.addLink, + isActive: !! formats.link + } ] ) + } + /> + + { !! formats.link && this.state.isEditingLink && +
+ + + + } + + { !! formats.link && ! this.state.isEditingLink && +
+ + { decodeURI( this.state.linkValue ) } + + + +
+ } +
+ ); + } +} + +export default FormatToolbar; diff --git a/blocks/components/editable/index.js b/blocks/components/editable/index.js index c8e1b96bf31d90..befb5acf0ac1a9 100644 --- a/blocks/components/editable/index.js +++ b/blocks/components/editable/index.js @@ -2,15 +2,16 @@ * External dependencies */ import classnames from 'classnames'; -import { last, isEqual, capitalize, omitBy } from 'lodash'; +import { last, isEqual, capitalize, omitBy, forEach, merge } from 'lodash'; import { nodeListToReact } from 'dom-react'; import { Fill } from 'react-slot-fill'; +import 'element-closest'; /** * Internal dependencies */ import './style.scss'; - +import FormatToolbar from './format-toolbar'; // TODO: We mustn't import by relative path traversing from blocks to editor // as we're doing here; instead, we should consider a common components path. import Toolbar from '../../../editor/components/toolbar'; @@ -22,24 +23,6 @@ const formatMap = { del: 'strikethrough' }; -const FORMATTING_CONTROLS = [ - { - icon: 'editor-bold', - title: wp.i18n.__( 'Bold' ), - format: 'bold' - }, - { - icon: 'editor-italic', - title: wp.i18n.__( 'Italic' ), - format: 'italic' - }, - { - icon: 'editor-strikethrough', - title: wp.i18n.__( 'Strikethrough' ), - format: 'strikethrough' - } -]; - const ALIGNMENT_CONTROLS = [ { icon: 'editor-alignleft', @@ -86,9 +69,11 @@ export default class Editable extends wp.element.Component { this.onFocus = this.onFocus.bind( this ); this.onNodeChange = this.onNodeChange.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); + this.changeFormats = this.changeFormats.bind( this ); this.state = { formats: {}, - alignment: null + alignment: null, + bookmark: null }; } @@ -104,6 +89,7 @@ export default class Editable extends wp.element.Component { toolbar: false, browser_spellcheck: true, entity_encoding: 'raw', + convert_urls: false, setup: this.onSetup, formats: { strikethrough: { inline: 'del' } @@ -147,6 +133,15 @@ export default class Editable extends wp.element.Component { this.props.onChange( this.savedContent ); } + getRelativePosition( node ) { + const editorPosition = this.editorNode.closest( '.editor-visual-editor__block' ).getBoundingClientRect(); + const position = node.getBoundingClientRect(); + return { + top: position.top - editorPosition.top + 40 + ( position.height ), + left: position.left - editorPosition.left - 157 + }; + } + isStartOfEditor() { const range = this.editor.selection.getRng(); if ( range.startOffset !== 0 || ! range.collapsed ) { @@ -225,15 +220,16 @@ export default class Editable extends wp.element.Component { } ); } - onNodeChange( { parents } ) { + onNodeChange( { element, parents } ) { let alignment = null; const formats = {}; - parents.forEach( ( node ) => { const tag = node.nodeName.toLowerCase(); if ( formatMap.hasOwnProperty( tag ) ) { formats[ formatMap[ tag ] ] = true; + } else if ( tag === 'a' ) { + formats.link = { value: node.getAttribute( 'href' ), node }; } if ( tag === 'p' ) { @@ -241,12 +237,9 @@ export default class Editable extends wp.element.Component { } } ); - if ( - this.state.alignment !== alignment || - ! isEqual( this.state.formats, formats ) - ) { - this.setState( { alignment, formats } ); - } + const focusPosition = this.getRelativePosition( element ); + const bookmark = this.editor.selection.getBookmark( 2, true ); + this.setState( { alignment, bookmark, formats, focusPosition } ); } bindEditorNode( ref ) { @@ -260,6 +253,7 @@ export default class Editable extends wp.element.Component { this.editor.selection.moveToBookmark( bookmark ); // Saving the editor on updates avoid unecessary onChanges calls // These calls can make the focus jump + this.editor.save(); } @@ -326,14 +320,35 @@ export default class Editable extends wp.element.Component { return !! this.state.formats[ format ]; } - toggleFormat( format ) { - this.editor.focus(); - - if ( this.isFormatActive( format ) ) { - this.editor.formatter.remove( format ); - } else { - this.editor.formatter.apply( format ); + changeFormats( formats ) { + if ( this.state.bookmark ) { + this.editor.selection.moveToBookmark( this.state.bookmark ); } + + forEach( formats, ( formatValue, format ) => { + if ( format === 'link' ) { + if ( formatValue !== undefined ) { + const anchor = this.editor.dom.getParent( this.editor.selection.getNode(), 'a' ); + if ( ! anchor ) { + this.editor.formatter.remove( 'link' ); + } + this.editor.formatter.apply( 'link', { href: formatValue.value }, anchor ); + } else { + this.editor.execCommand( 'Unlink' ); + } + } else { + const isActive = this.isFormatActive( format ); + if ( isActive && ! formatValue ) { + this.editor.formatter.remove( format ); + } else if ( ! isActive && formatValue ) { + this.editor.formatter.apply( format ); + } + } + } ); + + this.setState( { + formats: merge( {}, this.state.formats, formats ) + } ); } isAlignmentActive( align ) { @@ -373,13 +388,7 @@ export default class Editable extends wp.element.Component { isActive: this.isAlignmentActive( control.align ) } ) ) } /> } - - ( { - ...control, - onClick: () => this.toggleFormat( control.format ), - isActive: this.isFormatActive( control.format ) - } ) ) } /> + , element ]; diff --git a/blocks/components/editable/style.scss b/blocks/components/editable/style.scss index f13391153f9ef4..27dd810ec6465c 100644 --- a/blocks/components/editable/style.scss +++ b/blocks/components/editable/style.scss @@ -23,3 +23,44 @@ figcaption.blocks-editable { font-size: $default-font-size; } } + + +.editable-format-toolbar { + display: inline-flex; +} + +.editable-format-toolbar__link-modal { + position: absolute; + top: 42px; + box-shadow: 0px 3px 20px rgba( 18, 24, 30, .1 ), 0px 1px 3px rgba( 18, 24, 30, .1 ); + border: 1px solid #e0e5e9; + background: #fff; + width: 250px; + display: inline-flex; + align-items: center; + font-family: $default-font; + font-size: $default-font-size; + line-height: $default-line-height; +} + + +input.editable-format-toolbar__link-input { + padding: 10px; + font-size: 13px; + width: 100%; + border: none; + outline: none; + box-shadow: none; + flex-grow: 1; + + &:focus { + border: none; + box-shadow: none; + outline: none; + } +} + +.editable-format-toolbar__link-value { + padding: 10px; + flex-grow: 1; +} diff --git a/editor/modes/visual-editor/block.js b/editor/modes/visual-editor/block.js index 8a64a8555dfb58..65c8ae65a58643 100644 --- a/editor/modes/visual-editor/block.js +++ b/editor/modes/visual-editor/block.js @@ -140,14 +140,13 @@ class VisualEditorBlock extends wp.element.Component { // Disable reason: Each block can be selected by clicking on it - /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role */ + /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ return (
} - +
+ +
); /* eslint-enable jsx-a11y/no-static-element-interactions */ diff --git a/languages/gutenberg.pot b/languages/gutenberg.pot index 5508d2a88f3ec8..928118c1eee50b 100644 --- a/languages/gutenberg.pot +++ b/languages/gutenberg.pot @@ -3,31 +3,39 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "X-Generator: babel-plugin-wp-i18n\n" -#: blocks/components/editable/index.js:28 +#: blocks/components/editable/format-toolbar.js:12 msgid "Bold" msgstr "" -#: blocks/components/editable/index.js:33 +#: blocks/components/editable/format-toolbar.js:121 +msgid "Link" +msgstr "" + +#: blocks/components/editable/format-toolbar.js:139 +msgid "Paste URL or type" +msgstr "" + +#: blocks/components/editable/format-toolbar.js:17 msgid "Italic" msgstr "" -#: blocks/components/editable/index.js:38 +#: blocks/components/editable/format-toolbar.js:22 msgid "Strikethrough" msgstr "" -#: blocks/components/editable/index.js:47 +#: blocks/components/editable/index.js:29 #: blocks/library/image/index.js:41 #: blocks/library/list/index.js:25 msgid "Align left" msgstr "" -#: blocks/components/editable/index.js:52 +#: blocks/components/editable/index.js:34 #: blocks/library/image/index.js:47 #: blocks/library/list/index.js:33 msgid "Align center" msgstr "" -#: blocks/components/editable/index.js:57 +#: blocks/components/editable/index.js:39 #: blocks/library/image/index.js:53 #: blocks/library/list/index.js:41 msgid "Align right" diff --git a/package.json b/package.json index 189dd973ba591d..0b8af695ef02e4 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "dependencies": { "classnames": "^2.2.5", "dom-react": "^2.0.0", + "element-closest": "^2.0.2", "hpq": "^1.2.0", "jed": "^1.1.1", "lodash": "^4.17.4",