summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2018-10-02 13:59:42 +0000
committerPhil Hughes <me@iamphill.com>2018-10-02 13:59:42 +0000
commit5837f8c4e93495a626824081d94154026ce20d88 (patch)
tree88d41f541dbf2c8d04ef4a1bdb866c6906bbbc26
parent57dc233325727a0baf28492bf59ae4b67562b84d (diff)
parentceafbbd317c9b9e67be6a1a223ab3f893ae047b3 (diff)
downloadgitlab-ce-5837f8c4e93495a626824081d94154026ce20d88.tar.gz
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
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue6
-rw-r--r--app/views/projects/_md_preview.html.haml17
-rw-r--r--changelogs/unreleased/44627-add-link-md-editor.yml5
-rw-r--r--locale/gitlab.pot27
-rw-r--r--spec/javascripts/lib/utils/text_markdown_spec.js69
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js2
9 files changed, 145 insertions, 21 deletions
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index ce0bc4d40e9..f7429601afa 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -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') });
});
}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 8c22f3f6536..afc4196c729 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -106,6 +106,12 @@
icon="code"
/>
<toolbar-button
+ tag="[{text}](url)"
+ tag-select="url"
+ button-title="Add a link"
+ icon="link"
+ />
+ <toolbar-button
:prepend="true"
tag="* "
button-title="Add a bullet list"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 9f1e009efdd..bda33636369 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -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"
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 8fb6aa55436..5436806162d 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -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
diff --git a/changelogs/unreleased/44627-add-link-md-editor.yml b/changelogs/unreleased/44627-add-link-md-editor.yml
new file mode 100644
index 00000000000..65551ce9c14
--- /dev/null
+++ b/changelogs/unreleased/44627-add-link-md-editor.yml
@@ -0,0 +1,5 @@
+---
+title: Add link button to markdown editor toolbar
+merge_request: 18579
+author: Jan Beckmann
+type: added
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 01448223a7d..cc11577b624 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/javascripts/lib/utils/text_markdown_spec.js b/spec/javascripts/lib/utils/text_markdown_spec.js
index ca0e7c395a0..043dd018e0c 100644
--- a/spec/javascripts/lib/utils/text_markdown_spec.js
+++ b/spec/javascripts/lib/utils/text_markdown_spec.js
@@ -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);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 69034975422..0dea9278cc2 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -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(
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
index 488575df401..bc934afe7a4 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -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', () => {