Commit 5837f8c4 authored by Phil Hughes's avatar Phil Hughes

Merge branch '44627-add-link-md-editor' into 'master'

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

Closes #44627

See merge request gitlab-org/gitlab-ce!18579
parents 57dc2333 ceafbbd3
...@@ -31,11 +31,17 @@ function blockTagText(text, textArea, blockTag, selected) { ...@@ -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; var pos;
if (!textArea.setSelectionRange) { if (!textArea.setSelectionRange) {
return; 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 (textArea.selectionStart === textArea.selectionEnd) {
if (wrapped) { if (wrapped) {
pos = textArea.selectionStart - tag.length; pos = textArea.selectionStart - tag.length;
...@@ -51,7 +57,7 @@ function moveCursor(textArea, tag, wrapped, removedLastNewLine) { ...@@ -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; var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
removedLastNewLine = false; removedLastNewLine = false;
removedFirstNewLine = false; removedFirstNewLine = false;
...@@ -82,11 +88,16 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap ...@@ -82,11 +88,16 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
const textPlaceholder = '{text}';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
if (blockTag != null && blockTag !== '') { if (blockTag != null && blockTag !== '') {
textToInsert = blockTagText(text, textArea, blockTag, selected); textToInsert = blockTagText(text, textArea, blockTag, selected);
} else { } else {
textToInsert = selectedSplit.map(function(val) { textToInsert = selectedSplit.map(function(val) {
if (tag.indexOf(textPlaceholder) > -1) {
return tag.replace(textPlaceholder, val);
}
if (val.indexOf(tag) === 0) { if (val.indexOf(tag) === 0) {
return "" + (val.replace(tag, '')); return "" + (val.replace(tag, ''));
} else { } else {
...@@ -94,6 +105,8 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap ...@@ -94,6 +105,8 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
} }
}).join('\n'); }).join('\n');
} }
} else if (tag.indexOf(textPlaceholder) > -1) {
textToInsert = tag.replace(textPlaceholder, selected);
} else { } else {
textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' '); textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' ');
} }
...@@ -107,17 +120,17 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap ...@@ -107,17 +120,17 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap
} }
insertText(textArea, textToInsert); 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; var $textArea, selected, text;
$textArea = $(textArea); $textArea = $(textArea);
textArea = $textArea.get(0); textArea = $textArea.get(0);
text = $textArea.val(); text = $textArea.val();
selected = selectedText(text, textArea); selected = selectedText(text, textArea);
$textArea.focus(); $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) { function replaceRange(s, start, end, substitute) {
...@@ -127,7 +140,12 @@ function replaceRange(s, start, end, substitute) { ...@@ -127,7 +140,12 @@ function replaceRange(s, start, end, substitute) {
export function addMarkdownListeners(form) { export function addMarkdownListeners(form) {
return $('.js-md', form).off('click').on('click', function() { return $('.js-md', form).off('click').on('click', function() {
const $this = $(this); 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') });
}); });
} }
......
...@@ -105,6 +105,12 @@ ...@@ -105,6 +105,12 @@
button-title="Insert code" button-title="Insert code"
icon="code" icon="code"
/> />
<toolbar-button
tag="[{text}](url)"
tag-select="url"
button-title="Add a link"
icon="link"
/>
<toolbar-button <toolbar-button
:prepend="true" :prepend="true"
tag="* " tag="* "
......
...@@ -27,6 +27,11 @@ ...@@ -27,6 +27,11 @@
required: false, required: false,
default: '', default: '',
}, },
tagSelect: {
type: String,
required: false,
default: '',
},
prepend: { prepend: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -40,6 +45,7 @@ ...@@ -40,6 +45,7 @@
<button <button
v-tooltip v-tooltip
:data-md-tag="tag" :data-md-tag="tag"
:data-md-select="tagSelect"
:data-md-block="tagBlock" :data-md-block="tagBlock"
:data-md-prepend="prepend" :data-md-prepend="prepend"
:title="buttonTitle" :title="buttonTitle"
......
...@@ -18,14 +18,15 @@ ...@@ -18,14 +18,15 @@
Preview Preview
%li.md-header-toolbar.active %li.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic 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: "Insert a quote" }) = 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: "Insert code" }) = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") })
= markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered 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: "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") = sprite_icon("screen-full")
.md-write-holder .md-write-holder
......
---
title: Add link button to markdown editor toolbar
merge_request: 18579
author: Jan Beckmann
type: added
...@@ -3651,6 +3651,33 @@ msgstr "" ...@@ -3651,6 +3651,33 @@ msgstr ""
msgid "Markdown enabled" msgid "Markdown enabled"
msgstr "" 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" msgid "Max access level"
msgstr "" msgstr ""
......
...@@ -21,7 +21,7 @@ describe('init markdown', () => { ...@@ -21,7 +21,7 @@ describe('init markdown', () => {
textArea.selectionStart = 0; textArea.selectionStart = 0;
textArea.selectionEnd = 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}* `); expect(textArea.value).toEqual(`${initialValue}* `);
}); });
...@@ -32,7 +32,7 @@ describe('init markdown', () => { ...@@ -32,7 +32,7 @@ describe('init markdown', () => {
textArea.value = initialValue; textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length); 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* `); expect(textArea.value).toEqual(`${initialValue}\n* `);
}); });
...@@ -43,7 +43,7 @@ describe('init markdown', () => { ...@@ -43,7 +43,7 @@ describe('init markdown', () => {
textArea.value = initialValue; textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length); 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}* `); expect(textArea.value).toEqual(`${initialValue}* `);
}); });
...@@ -54,9 +54,70 @@ describe('init markdown', () => { ...@@ -54,9 +54,70 @@ describe('init markdown', () => {
textArea.value = initialValue; textArea.value = initialValue;
textArea.setSelectionRange(initialValue.length, initialValue.length); 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}* `); 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);
});
});
});
}); });
...@@ -153,7 +153,7 @@ describe('Markdown field component', () => { ...@@ -153,7 +153,7 @@ describe('Markdown field component', () => {
const textarea = vm.$el.querySelector('textarea'); const textarea = vm.$el.querySelector('textarea');
textarea.setSelectionRange(0, 0); textarea.setSelectionRange(0, 0);
vm.$el.querySelectorAll('.js-md')[4].click(); vm.$el.querySelectorAll('.js-md')[5].click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
...@@ -168,7 +168,7 @@ describe('Markdown field component', () => { ...@@ -168,7 +168,7 @@ describe('Markdown field component', () => {
const textarea = vm.$el.querySelector('textarea'); const textarea = vm.$el.querySelector('textarea');
textarea.setSelectionRange(0, 50); textarea.setSelectionRange(0, 50);
vm.$el.querySelectorAll('.js-md')[4].click(); vm.$el.querySelectorAll('.js-md')[5].click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect( expect(
......
...@@ -18,7 +18,7 @@ describe('Markdown field header component', () => { ...@@ -18,7 +18,7 @@ describe('Markdown field header component', () => {
}); });
it('renders markdown buttons', () => { 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', () => { it('renders `write` link as active when previewMarkdown is false', () => {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment