From 6a758dc784ab165b8e8d4846ad9de7450dd96eed Mon Sep 17 00:00:00 2001 From: Ben Gotow Date: Mon, 26 Feb 2018 11:15:59 -0800 Subject: [PATCH] Allow editing links, remove link on space/enter, ctrl-click to open #702, #704, #639 --- .../composer/styles/composer.less | 8 +++ .../composer-editor/link-plugins.jsx | 34 +++++++++-- .../toolbar-component-factories.jsx | 56 +++++++++++++++++-- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/app/internal_packages/composer/styles/composer.less b/app/internal_packages/composer/styles/composer.less index 8ab125433..c2a993817 100644 --- a/app/internal_packages/composer/styles/composer.less +++ b/app/internal_packages/composer/styles/composer.less @@ -33,6 +33,10 @@ -webkit-margin-end: initial !important; } +.RichEditor-root .link { + color: @text-color-link; + text-decoration: underline; +} .RichEditor-root .custom-block-selected, .RichEditor-root .uneditable.custom-block-selected { border: 1px dashed @text-color; @@ -143,11 +147,15 @@ flex: 1; flex-shrink: 0; max-width: 32px; + text-align: center; .dropdown { padding: 5px; display: flex; flex-direction: row; + button { + max-width: 50px; + } } } } diff --git a/app/src/components/composer-editor/link-plugins.jsx b/app/src/components/composer-editor/link-plugins.jsx index 0d3035333..319e6ff5d 100644 --- a/app/src/components/composer-editor/link-plugins.jsx +++ b/app/src/components/composer-editor/link-plugins.jsx @@ -4,7 +4,7 @@ import { Mark } from 'slate'; import AutoReplace from 'slate-auto-replace'; import { RegExpUtils } from 'mailspring-exports'; -import { BuildMarkButtonWithValuePicker } from './toolbar-component-factories'; +import { BuildMarkButtonWithValuePicker, getMarkOfType } from './toolbar-component-factories'; export const LINK_TYPE = 'link'; @@ -22,14 +22,28 @@ function onPaste(event, change, editor) { } } -function renderMark({ mark, children }) { - if (mark.type === LINK_TYPE) { - const href = mark.data.href || mark.data.get('href'); +function renderMark({ mark, children, targetIsHTML }) { + if (mark.type !== LINK_TYPE) { + return; + } + const href = mark.data.href || mark.data.get('href'); + if (targetIsHTML) { return ( {children} ); + } else { + const onClick = e => { + if (e.ctrlKey || e.metaKey) { + AppEnv.windowEventHandler.openLink({ href, metaKey: e.metaKey }); + } + }; + return ( + + {children} + + ); } } @@ -66,12 +80,22 @@ export default [ BuildMarkButtonWithValuePicker({ type: LINK_TYPE, field: 'href', - iconClassOn: 'fa fa-unlink', + iconClassOn: 'fa fa-link', iconClassOff: 'fa fa-link', placeholder: 'http://', }), ], onPaste, + onKeyDown: function onKeyDown(event, change) { + // ensure space and enter always terminate links + if (!['Space', 'Enter', ' ', 'Return'].includes(event.key)) { + return; + } + const mark = getMarkOfType(change.value, LINK_TYPE); + if (mark) { + change.removeMark(mark); + } + }, renderMark, rules, commands: { diff --git a/app/src/components/composer-editor/toolbar-component-factories.jsx b/app/src/components/composer-editor/toolbar-component-factories.jsx index b17320020..206646267 100644 --- a/app/src/components/composer-editor/toolbar-component-factories.jsx +++ b/app/src/components/composer-editor/toolbar-component-factories.jsx @@ -35,6 +35,33 @@ function removeMarksOfTypeInRange(change, range, type) { return change; } +export function expandSelectionToRangeOfMark(change, type) { + const { selection, document } = change.value; + const node = document.getNode(selection.anchorKey); + let start = selection.anchorOffset; + let end = selection.anchorOffset; + + // expand backwards until the mark disappears + while (start > 0 && node.getMarksAtIndex(start).find(m => m.type === type)) { + start -= 1; + } + // expand forwards until the mark disappears + while (end < node.text.length - 1 && node.getMarksAtIndex(end + 1).find(m => m.type === type)) { + end += 1; + } + + // expand selection + change.select({ + anchorKey: selection.anchorKey, + anchorOffset: start, + focusKey: selection.anchorKey, + focusOffset: end, + isFocused: true, + isBackward: false, + }); + return change; +} + export function hasMark(value, type) { return !!getMarkOfType(value, type); } @@ -107,7 +134,10 @@ export function BuildMarkButtonWithValuePicker(config) { const active = getMarkOfType(this.props.value, config.type); const fieldValue = (active && active.data.get(config.field)) || ''; this.setState({ expanded: true, fieldValue: fieldValue }, () => { - setTimeout(() => this._inputEl.focus(), 0); + setTimeout(() => { + this._inputEl.focus(); + this._inputEl.select(); + }, 0); }); }; @@ -117,6 +147,13 @@ export function BuildMarkButtonWithValuePicker(config) { // attach the URL value to the LINK that was created when we opened the link modal const { value, onChange } = this.props; const { fieldValue } = this.state; + + if (fieldValue.trim() === '') { + this.onRemove(e); + this.setState({ expanded: false, fieldValue: '' }); + return; + } + const newMark = Mark.create({ type: config.type, data: { @@ -124,7 +161,17 @@ export function BuildMarkButtonWithValuePicker(config) { }, }); - if (value.selection.isCollapsed) { + const active = getMarkOfType(this.props.value, config.type); + if (active) { + // update the active mark + const change = value.change(); + expandSelectionToRangeOfMark(change, config.type); + removeMarksOfTypeInRange(change, value.selection, config.type) + .addMark(newMark) + .focus(); + onChange(change); + } else if (value.selection.isCollapsed) { + // apply new mark to new text onChange( value .change() @@ -135,6 +182,7 @@ export function BuildMarkButtonWithValuePicker(config) { .focus() ); } else { + // apply new mark to selected text onChange( removeMarksOfTypeInRange(value.change(), value.selection, config.type) .addMark(newMark) @@ -175,7 +223,7 @@ export function BuildMarkButtonWithValuePicker(config) { onBlur={this.onBlur} > {active ? ( - ) : ( @@ -198,7 +246,7 @@ export function BuildMarkButtonWithValuePicker(config) { } }} /> - + )}