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 ? (
-