Allow editing links, remove link on space/enter, ctrl-click to open #702, #704, #639

This commit is contained in:
Ben Gotow 2018-02-26 11:15:59 -08:00
parent ed3f58d872
commit 6a758dc784
3 changed files with 89 additions and 9 deletions

View file

@ -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;
}
}
}
}

View file

@ -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 (
<a href={href} title={href}>
{children}
</a>
);
} else {
const onClick = e => {
if (e.ctrlKey || e.metaKey) {
AppEnv.windowEventHandler.openLink({ href, metaKey: e.metaKey });
}
};
return (
<span className="link" title={href} onClick={onClick}>
{children}
</span>
);
}
}
@ -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: {

View file

@ -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 ? (
<button className="active" onMouseDown={this.onRemove}>
<button className="active" onMouseDown={this.onPrompt}>
<i className={config.iconClassOn} />
</button>
) : (
@ -198,7 +246,7 @@ export function BuildMarkButtonWithValuePicker(config) {
}
}}
/>
<button onMouseDown={this.onConfirm}>Add</button>
<button onMouseDown={this.onConfirm}>{active ? 'Save' : 'Add'}</button>
</div>
)}
</div>