Resolve "Add "Link" shortcut/icon in markdown editor to make it easier to add references"

This commit is contained in:
Jan Beckmann 2018-10-02 13:59:42 +00:00 committed by Phil Hughes
parent 57dc233325
commit ceafbbd317
9 changed files with 145 additions and 21 deletions

View file

@ -31,11 +31,17 @@ function blockTagText(text, textArea, blockTag, selected) {
}
}
function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) {
var pos;
if (!textArea.setSelectionRange) {
return;
}
if (select && select.length > 0) {
// calculate the part of the text to be selected
const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
const endPosition = startPosition + select.length;
return textArea.setSelectionRange(startPosition, endPosition);
}
if (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) {
pos = textArea.selectionStart - tag.length;
@ -51,7 +57,7 @@ function moveCursor(textArea, tag, wrapped, removedLastNewLine) {
}
}
export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) {
export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) {
var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
@ -82,11 +88,16 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
const textPlaceholder = '{text}';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') {
textToInsert = blockTagText(text, textArea, blockTag, selected);
} else {
textToInsert = selectedSplit.map(function(val) {
if (tag.indexOf(textPlaceholder) > -1) {
return tag.replace(textPlaceholder, val);
}
if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, ''));
} else {
@ -94,6 +105,8 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
}
}).join('\n');
}
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected);
} else {
textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
@ -107,17 +120,17 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
}
insertText(textArea, textToInsert);
return moveCursor(textArea, tag, wrap, removedLastNewLine);
return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), wrap, removedLastNewLine, select });
}
function updateText(textArea, tag, blockTag, wrap) {
function updateText({ textArea, tag, blockTag, wrap, select }) {
var $textArea, selected, text;
$textArea = $(textArea);
textArea = $textArea.get(0);
text = $textArea.val();
selected = selectedText(text, textArea);
$textArea.focus();
return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap);
return insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select });
}
function replaceRange(s, start, end, substitute) {
@ -127,7 +140,12 @@ function replaceRange(s, start, end, substitute) {
export function addMarkdownListeners(form) {
return $('.js-md', form).off('click').on('click', function() {
const $this = $(this);
return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend'));
return updateText({
textArea: $this.closest('.md-area').find('textarea'),
tag: $this.data('mdTag'),
blockTag: $this.data('mdBlock'),
wrap: !$this.data('mdPrepend'),
select: $this.data('mdSelect') });
});
}

View file

@ -105,6 +105,12 @@
button-title="Insert code"
icon="code"
/>
<toolbar-button
tag="[{text}](url)"
tag-select="url"
button-title="Add a link"
icon="link"
/>
<toolbar-button
:prepend="true"
tag="* "

View file

@ -27,6 +27,11 @@
required: false,
default: '',
},
tagSelect: {
type: String,
required: false,
default: '',
},
prepend: {
type: Boolean,
required: false,
@ -40,6 +45,7 @@
<button
v-tooltip
:data-md-tag="tag"
:data-md-select="tagSelect"
:data-md-block="tagBlock"
:data-md-prepend="prepend"
:title="buttonTitle"

View file

@ -18,14 +18,15 @@
Preview
%li.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
= markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
= markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: "Go full screen", data: { container: "body" } }
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") })
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") })
= markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") })
= markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") })
= markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") })
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } }
= sprite_icon("screen-full")
.md-write-holder

View file

@ -0,0 +1,5 @@
---
title: Add link button to markdown editor toolbar
merge_request: 18579
author: Jan Beckmann
type: added

View file

@ -3651,6 +3651,33 @@ msgstr ""
msgid "Markdown enabled"
msgstr ""
msgid "MarkdownToolbar|Add a bullet list"
msgstr ""
msgid "MarkdownToolbar|Add a link"
msgstr ""
msgid "MarkdownToolbar|Add a numbered list"
msgstr ""
msgid "MarkdownToolbar|Add a task list"
msgstr ""
msgid "MarkdownToolbar|Add bold text"
msgstr ""
msgid "MarkdownToolbar|Add italic text"
msgstr ""
msgid "MarkdownToolbar|Go full screen"
msgstr ""
msgid "MarkdownToolbar|Insert a quote"
msgstr ""
msgid "MarkdownToolbar|Insert code"
msgstr ""
msgid "Max access level"
msgstr ""

View file

@ -21,7 +21,7 @@ describe('init markdown', () => {
textArea.selectionStart = 0;
textArea.selectionEnd = 0;
insertMarkdownText(textArea, textArea.value, '*', null, '', false);
insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}* `);
});
@ -32,7 +32,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
insertMarkdownText(textArea, textArea.value, '*', null, '', false);
insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}\n* `);
});
@ -43,7 +43,7 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
insertMarkdownText(textArea, textArea.value, '*', null, '', false);
insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}* `);
});
@ -54,9 +54,70 @@ describe('init markdown', () => {
textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length);
insertMarkdownText(textArea, textArea.value, '*', null, '', false);
insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected: '', wrap: false });
expect(textArea.value).toEqual(`${initialValue}* `);
});
});
describe('with selection', () => {
const text = 'initial selected value';
const selected = 'selected';
beforeEach(() => {
textArea.value = text;
const selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
});
it('applies the tag to the selected value', () => {
insertMarkdownText({ textArea, text: textArea.value, tag: '*', blockTag: null, selected, wrap: true });
expect(textArea.value).toEqual(text.replace(selected, `*${selected}*`));
});
it('replaces the placeholder in the tag', () => {
insertMarkdownText({ textArea, text: textArea.value, tag: '[{text}](url)', blockTag: null, selected, wrap: false });
expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`));
});
describe('and text to be selected', () => {
const tag = '[{text}](url)';
const select = 'url';
it('selects the text', () => {
insertMarkdownText({ textArea,
text: textArea.value,
tag,
blockTag: null,
selected,
wrap: false,
select });
const expectedText = text.replace(selected, `[${selected}](url)`);
expect(textArea.value).toEqual(expectedText);
expect(textArea.selectionStart).toEqual(expectedText.indexOf(select));
expect(textArea.selectionEnd).toEqual(expectedText.indexOf(select) + select.length);
});
it('selects the right text when multiple tags are present', () => {
const initialValue = `${tag} ${tag} ${selected}`;
textArea.value = initialValue;
const selectedIndex = initialValue.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({ textArea,
text: textArea.value,
tag,
blockTag: null,
selected,
wrap: false,
select });
const expectedText = initialValue.replace(selected, `[${selected}](url)`);
expect(textArea.value).toEqual(expectedText);
expect(textArea.selectionStart).toEqual(expectedText.lastIndexOf(select));
expect(textArea.selectionEnd).toEqual(expectedText.lastIndexOf(select) + select.length);
});
});
});
});

View file

@ -153,7 +153,7 @@ describe('Markdown field component', () => {
const textarea = vm.$el.querySelector('textarea');
textarea.setSelectionRange(0, 0);
vm.$el.querySelectorAll('.js-md')[4].click();
vm.$el.querySelectorAll('.js-md')[5].click();
Vue.nextTick(() => {
expect(
@ -168,7 +168,7 @@ describe('Markdown field component', () => {
const textarea = vm.$el.querySelector('textarea');
textarea.setSelectionRange(0, 50);
vm.$el.querySelectorAll('.js-md')[4].click();
vm.$el.querySelectorAll('.js-md')[5].click();
Vue.nextTick(() => {
expect(

View file

@ -18,7 +18,7 @@ describe('Markdown field header component', () => {
});
it('renders markdown buttons', () => {
expect(vm.$el.querySelectorAll('.js-md').length).toBe(7);
expect(vm.$el.querySelectorAll('.js-md').length).toBe(8);
});
it('renders `write` link as active when previewMarkdown is false', () => {