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 &&
+
+ }
+
+ );
+ }
+}
+
+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",