Commit fb4b0e6c authored by Fatih Acet's avatar Fatih Acet

Merge branch '27861-add-markdown-editing-buttons-to-the-file-editor' into 'master'

Resolve "Add markdown editing buttons to the file editor"

Closes #27861

See merge request gitlab-org/gitlab-ce!23480
parents 5a980865 066a99b6
...@@ -16,6 +16,7 @@ export default () => { ...@@ -16,6 +16,7 @@ export default () => {
const filePath = editBlobForm.data('blobFilename'); const filePath = editBlobForm.data('blobFilename');
const currentAction = $('.js-file-title').data('currentAction'); const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id'); const projectId = editBlobForm.data('project-id');
const isMarkdown = editBlobForm.data('is-markdown');
const commitButton = $('.js-commit-button'); const commitButton = $('.js-commit-button');
const cancelLink = $('.btn.btn-cancel'); const cancelLink = $('.btn.btn-cancel');
...@@ -27,7 +28,13 @@ export default () => { ...@@ -27,7 +28,13 @@ export default () => {
window.onbeforeunload = null; window.onbeforeunload = null;
}); });
new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId); new EditBlob({
assetsPath: `${urlRoot}${assetsPath}`,
filePath,
currentAction,
projectId,
isMarkdown,
});
new NewCommitForm(editBlobForm); new NewCommitForm(editBlobForm);
// returning here blocks page navigation // returning here blocks page navigation
......
...@@ -6,22 +6,31 @@ import createFlash from '~/flash'; ...@@ -6,22 +6,31 @@ import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator'; import TemplateSelectorMediator from '../blob/file_template_mediator';
import getModeByFileExtension from '~/lib/utils/ace_utils'; import getModeByFileExtension from '~/lib/utils/ace_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
export default class EditBlob { export default class EditBlob {
constructor(assetsPath, aceMode, currentAction, projectId) { // The options object has:
this.configureAceEditor(aceMode, assetsPath); // assetsPath, filePath, currentAction, projectId, isMarkdown
constructor(options) {
this.options = options;
this.configureAceEditor();
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap(); this.initSoftWrap();
this.initFileSelectors(currentAction, projectId); this.initFileSelectors();
} }
configureAceEditor(filePath, assetsPath) { configureAceEditor() {
const { filePath, assetsPath, isMarkdown } = this.options;
ace.config.set('modePath', `${assetsPath}/ace`); ace.config.set('modePath', `${assetsPath}/ace`);
ace.config.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/searchbox');
ace.config.loadModule('ace/ext/modelist'); ace.config.loadModule('ace/ext/modelist');
this.editor = ace.edit('editor'); this.editor = ace.edit('editor');
if (isMarkdown) {
addEditorMarkdownListeners(this.editor);
}
// This prevents warnings re: automatic scrolling being logged // This prevents warnings re: automatic scrolling being logged
this.editor.$blockScrolling = Infinity; this.editor.$blockScrolling = Infinity;
...@@ -32,7 +41,8 @@ export default class EditBlob { ...@@ -32,7 +41,8 @@ export default class EditBlob {
} }
} }
initFileSelectors(currentAction, projectId) { initFileSelectors() {
const { currentAction, projectId } = this.options;
this.fileTemplateMediator = new TemplateSelectorMediator({ this.fileTemplateMediator = new TemplateSelectorMediator({
currentAction, currentAction,
editor: this.editor, editor: this.editor,
......
...@@ -8,6 +8,10 @@ function selectedText(text, textarea) { ...@@ -8,6 +8,10 @@ function selectedText(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd); return text.substring(textarea.selectionStart, textarea.selectionEnd);
} }
function addBlockTags(blockTag, selected) {
return `${blockTag}\n${selected}\n${blockTag}`;
}
function lineBefore(text, textarea) { function lineBefore(text, textarea) {
var split; var split;
split = text split = text
...@@ -24,19 +28,45 @@ function lineAfter(text, textarea) { ...@@ -24,19 +28,45 @@ function lineAfter(text, textarea) {
.split('\n')[0]; .split('\n')[0];
} }
function editorBlockTagText(text, blockTag, selected, editor) {
const lines = text.split('\n');
const selectionRange = editor.getSelectionRange();
const shouldRemoveBlock =
lines[selectionRange.start.row - 1] === blockTag &&
lines[selectionRange.end.row + 1] === blockTag;
if (shouldRemoveBlock) {
if (blockTag !== null) {
// ace is globally defined
// eslint-disable-next-line no-undef
const { Range } = ace.require('ace/range');
const lastLine = lines[selectionRange.end.row + 1];
const rangeWithBlockTags = new Range(
lines[selectionRange.start.row - 1],
0,
selectionRange.end.row + 1,
lastLine.length,
);
editor.getSelection().setSelectionRange(rangeWithBlockTags);
}
return selected;
}
return addBlockTags(blockTag, selected);
}
function blockTagText(text, textArea, blockTag, selected) { function blockTagText(text, textArea, blockTag, selected) {
const before = lineBefore(text, textArea); const shouldRemoveBlock =
const after = lineAfter(text, textArea); lineBefore(text, textArea) === blockTag && lineAfter(text, textArea) === blockTag;
if (before === blockTag && after === blockTag) {
if (shouldRemoveBlock) {
// To remove the block tag we have to select the line before & after // To remove the block tag we have to select the line before & after
if (blockTag != null) { if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
} }
return selected; return selected;
} else {
return blockTag + '\n' + selected + '\n' + blockTag;
} }
return addBlockTags(blockTag, selected);
} }
function moveCursor({ function moveCursor({
...@@ -46,33 +76,48 @@ function moveCursor({ ...@@ -46,33 +76,48 @@ function moveCursor({
positionBetweenTags, positionBetweenTags,
removedLastNewLine, removedLastNewLine,
select, select,
editor,
editorSelectionStart,
editorSelectionEnd,
}) { }) {
var pos; var pos;
if (!textArea.setSelectionRange) { if (textArea && !textArea.setSelectionRange) {
return; return;
} }
if (select && select.length > 0) { if (select && select.length > 0) {
// calculate the part of the text to be selected if (textArea) {
const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select)); // calculate the part of the text to be selected
const endPosition = startPosition + select.length; const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select));
return textArea.setSelectionRange(startPosition, endPosition); const endPosition = startPosition + select.length;
} return textArea.setSelectionRange(startPosition, endPosition);
if (textArea.selectionStart === textArea.selectionEnd) { } else if (editor) {
if (positionBetweenTags) { editor.navigateLeft(tag.length - tag.indexOf(select));
pos = textArea.selectionStart - tag.length; editor.getSelection().selectAWord();
} else { return;
pos = textArea.selectionStart;
} }
}
if (textArea) {
if (textArea.selectionStart === textArea.selectionEnd) {
if (positionBetweenTags) {
pos = textArea.selectionStart - tag.length;
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) { if (removedLastNewLine) {
pos -= 1; pos -= 1;
} }
if (cursorOffset) { if (cursorOffset) {
pos -= cursorOffset; pos -= cursorOffset;
} }
return textArea.setSelectionRange(pos, pos); return textArea.setSelectionRange(pos, pos);
}
} else if (editor && editorSelectionStart.row === editorSelectionEnd.row) {
if (positionBetweenTags) {
editor.navigateLeft(tag.length);
}
} }
} }
...@@ -85,6 +130,7 @@ export function insertMarkdownText({ ...@@ -85,6 +130,7 @@ export function insertMarkdownText({
selected = '', selected = '',
wrap, wrap,
select, select,
editor,
}) { }) {
var textToInsert, var textToInsert,
selectedSplit, selectedSplit,
...@@ -92,11 +138,20 @@ export function insertMarkdownText({ ...@@ -92,11 +138,20 @@ export function insertMarkdownText({
removedLastNewLine, removedLastNewLine,
removedFirstNewLine, removedFirstNewLine,
currentLineEmpty, currentLineEmpty,
lastNewLine; lastNewLine,
editorSelectionStart,
editorSelectionEnd;
removedLastNewLine = false; removedLastNewLine = false;
removedFirstNewLine = false; removedFirstNewLine = false;
currentLineEmpty = false; currentLineEmpty = false;
if (editor) {
const selectionRange = editor.getSelectionRange();
editorSelectionStart = selectionRange.start;
editorSelectionEnd = selectionRange.end;
}
// check for link pattern and selected text is an URL // check for link pattern and selected text is an URL
// if so fill in the url part instead of the text part of the pattern. // if so fill in the url part instead of the text part of the pattern.
if (tag === LINK_TAG_PATTERN) { if (tag === LINK_TAG_PATTERN) {
...@@ -119,14 +174,27 @@ export function insertMarkdownText({ ...@@ -119,14 +174,27 @@ export function insertMarkdownText({
} }
// Remove the last newline // Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { if (textArea) {
removedLastNewLine = true; if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
selected = selected.replace(/\n$/, ''); removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
} else if (editor) {
if (editorSelectionStart.row !== editorSelectionEnd.row) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
} }
selectedSplit = selected.split('\n'); selectedSplit = selected.split('\n');
if (!wrap) { if (editor && !wrap) {
lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row];
if (/^\s*$/.test(lastNewLine)) {
currentLineEmpty = true;
}
} else if (textArea && !wrap) {
lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n'); lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
// Check whether the current line is empty or consists only of spaces(=handle as empty) // Check whether the current line is empty or consists only of spaces(=handle as empty)
...@@ -135,13 +203,19 @@ export function insertMarkdownText({ ...@@ -135,13 +203,19 @@ export function insertMarkdownText({
} }
} }
startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; const isBeginning =
(textArea && textArea.selectionStart === 0) ||
(editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0);
startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
const textPlaceholder = '{text}'; 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 = editor
? editorBlockTagText(text, blockTag, selected, editor)
: blockTagText(text, textArea, blockTag, selected);
} else { } else {
textToInsert = selectedSplit textToInsert = selectedSplit
.map(function(val) { .map(function(val) {
...@@ -170,7 +244,11 @@ export function insertMarkdownText({ ...@@ -170,7 +244,11 @@ export function insertMarkdownText({
textToInsert += '\n'; textToInsert += '\n';
} }
insertText(textArea, textToInsert); if (editor) {
editor.insert(textToInsert);
} else {
insertText(textArea, textToInsert);
}
return moveCursor({ return moveCursor({
textArea, textArea,
tag: tag.replace(textPlaceholder, selected), tag: tag.replace(textPlaceholder, selected),
...@@ -178,6 +256,9 @@ export function insertMarkdownText({ ...@@ -178,6 +256,9 @@ export function insertMarkdownText({
positionBetweenTags: wrap && selected.length === 0, positionBetweenTags: wrap && selected.length === 0,
removedLastNewLine, removedLastNewLine,
select, select,
editor,
editorSelectionStart,
editorSelectionEnd,
}); });
} }
...@@ -217,6 +298,25 @@ export function addMarkdownListeners(form) { ...@@ -217,6 +298,25 @@ export function addMarkdownListeners(form) {
}); });
} }
export function addEditorMarkdownListeners(editor) {
$('.js-md')
.off('click')
.on('click', function(e) {
const { mdTag, mdBlock, mdPrepend, mdSelect } = $(e.currentTarget).data();
insertMarkdownText({
tag: mdTag,
blockTag: mdBlock,
wrap: !mdPrepend,
select: mdSelect,
selected: editor.getSelectedText(),
text: editor.getValue(),
editor,
});
editor.focus();
});
}
export function removeMarkdownListeners(form) { export function removeMarkdownListeners(form) {
return $('.js-md', form).off('click'); return $('.js-md', form).off('click');
} }
...@@ -173,7 +173,7 @@ ...@@ -173,7 +173,7 @@
svg { svg {
width: 14px; width: 14px;
height: 14px; height: 14px;
margin-top: 3px; vertical-align: middle;
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
} }
......
...@@ -128,6 +128,10 @@ ...@@ -128,6 +128,10 @@
width: 100%; width: 100%;
} }
} }
@media(max-width: map-get($grid-breakpoints, md)-1) {
clear: both;
}
} }
.editor-ref { .editor-ref {
......
...@@ -177,7 +177,8 @@ module BlobHelper ...@@ -177,7 +177,8 @@ module BlobHelper
'relative-url-root' => Rails.application.config.relative_url_root, 'relative-url-root' => Rails.application.config.relative_url_root,
'assets-prefix' => Gitlab::Application.config.assets.prefix, 'assets-prefix' => Gitlab::Application.config.assets.prefix,
'blob-filename' => @blob && @blob.path, 'blob-filename' => @blob && @blob.path,
'project-id' => project.id 'project-id' => project.id,
'is-markdown' => @blob && @blob.path && Gitlab::MarkupHelper.gitlab_markdown?(@blob.path)
} }
end end
......
...@@ -18,17 +18,7 @@ ...@@ -18,17 +18,7 @@
Preview Preview
%li.md-header-toolbar.active %li.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") }) = render 'projects/blob/markdown_buttons', show_fullscreen_button: true
= 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") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") })
%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 .md-write-holder
= yield = yield
......
- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create' - action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
- file_name = params[:id].split("/").last ||= ""
- is_markdown = Gitlab::MarkupHelper.gitlab_markdown?(file_name)
.file-holder-bottom-radius.file-holder.file.append-bottom-default .file-holder-bottom-radius.file-holder.file.append-bottom-default
.js-file-title.file-title.clearfix{ data: { current_action: action } } .js-file-title.file-title.clearfix{ data: { current_action: action } }
...@@ -17,6 +19,8 @@ ...@@ -17,6 +19,8 @@
required: true, class: 'form-control new-file-name js-file-path-name-input' required: true, class: 'form-control new-file-name js-file-path-name-input'
.file-buttons .file-buttons
- if is_markdown
= render 'projects/blob/markdown_buttons', show_fullscreen_button: false
= button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap %span.no-wrap
= custom_icon('icon_no_wrap') = custom_icon('icon_no_wrap')
......
.md-header-toolbar.active
= 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") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") })
- if show_fullscreen_button
%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")
---
title: Add markdown helper buttons to file editor
merge_request: 23480
author:
type: added
import blobBundle from '~/blob_edit/blob_bundle'; import blobBundle from '~/blob_edit/blob_bundle';
import $ from 'jquery'; import $ from 'jquery';
window.ace = { describe('BlobBundle', () => {
config: {
set: () => {},
loadModule: () => {},
},
edit: () => ({ focus: () => {} }),
};
describe('EditBlob', () => {
beforeEach(() => { beforeEach(() => {
spyOnDependency(blobBundle, 'EditBlob').and.stub();
setFixtures(` setFixtures(`
<div class="js-edit-blob-form"> <div class="js-edit-blob-form" data-blob-filename="blah">
<button class="js-commit-button"></button> <button class="js-commit-button"></button>
<a class="btn btn-cancel" href="#"></a> <a class="btn btn-cancel" href="#"></a>
</div>`); </div>`);
......
...@@ -13,215 +13,296 @@ describe('init markdown', () => { ...@@ -13,215 +13,296 @@ describe('init markdown', () => {
textArea.parentNode.removeChild(textArea); textArea.parentNode.removeChild(textArea);
}); });
describe('without selection', () => { describe('textArea', () => {
it('inserts the tag on an empty line', () => { describe('without selection', () => {
const initialValue = ''; it('inserts the tag on an empty line', () => {
const initialValue = '';
textArea.value = initialValue; textArea.value = initialValue;
textArea.selectionStart = 0; textArea.selectionStart = 0;
textArea.selectionEnd = 0; textArea.selectionEnd = 0;
insertMarkdownText({
textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
});
expect(textArea.value).toEqual(`${initialValue}* `);
});
it('inserts the tag on a new line if the current one is not empty', () => {
const initialValue = 'some text';
textArea.value = initialValue; insertMarkdownText({
textArea.setSelectionRange(initialValue.length, initialValue.length); textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
});
insertMarkdownText({ expect(textArea.value).toEqual(`${initialValue}* `);
textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
}); });
expect(textArea.value).toEqual(`${initialValue}\n* `); it('inserts the tag on a new line if the current one is not empty', () => {
}); const initialValue = 'some text';
it('inserts the tag on the same line if the current line only contains spaces', () => { textArea.value = initialValue;
const initialValue = ' '; textArea.setSelectionRange(initialValue.length, initialValue.length);
textArea.value = initialValue; insertMarkdownText({
textArea.setSelectionRange(initialValue.length, initialValue.length); textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
});
insertMarkdownText({ expect(textArea.value).toEqual(`${initialValue}\n* `);
textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
}); });
expect(textArea.value).toEqual(`${initialValue}* `); it('inserts the tag on the same line if the current line only contains spaces', () => {
}); const initialValue = ' ';
it('inserts the tag on the same line if the current line only contains tabs', () => { textArea.value = initialValue;
const initialValue = '\t\t\t'; textArea.setSelectionRange(initialValue.length, initialValue.length);
textArea.value = initialValue; insertMarkdownText({
textArea.setSelectionRange(initialValue.length, initialValue.length); textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
});
insertMarkdownText({ expect(textArea.value).toEqual(`${initialValue}* `);
textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
}); });
expect(textArea.value).toEqual(`${initialValue}* `); it('inserts the tag on the same line if the current line only contains tabs', () => {
}); const initialValue = '\t\t\t';
it('places the cursor inside the tags', () => { textArea.value = initialValue;
const start = 'lorem '; textArea.setSelectionRange(initialValue.length, initialValue.length);
const end = ' ipsum';
const tag = '*';
textArea.value = `${start}${end}`; insertMarkdownText({
textArea.setSelectionRange(start.length, start.length); textArea,
text: textArea.value,
tag: '*',
blockTag: null,
selected: '',
wrap: false,
});
insertMarkdownText({ expect(textArea.value).toEqual(`${initialValue}* `);
textArea,
text: textArea.value,
tag,
blockTag: null,
selected: '',
wrap: true,
}); });
expect(textArea.value).toEqual(`${start}**${end}`); it('places the cursor inside the tags', () => {
const start = 'lorem ';
const end = ' ipsum';
const tag = '*';
// cursor placement should be between tags textArea.value = `${start}${end}`;
expect(textArea.selectionStart).toBe(start.length + tag.length); textArea.setSelectionRange(start.length, start.length);
});
});
describe('with selection', () => { insertMarkdownText({
const text = 'initial selected value'; textArea,
const selected = 'selected'; text: textArea.value,
beforeEach(() => { tag,
textArea.value = text; blockTag: null,
const selectedIndex = text.indexOf(selected); selected: '',
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); wrap: true,
}); });
it('applies the tag to the selected value', () => { expect(textArea.value).toEqual(`${start}**${end}`);
const selectedIndex = text.indexOf(selected);
const tag = '*';
insertMarkdownText({ // cursor placement should be between tags
textArea, expect(textArea.selectionStart).toBe(start.length + tag.length);
text: textArea.value,
tag,
blockTag: null,
selected,
wrap: true,
}); });
expect(textArea.value).toEqual(text.replace(selected, `*${selected}*`));
// cursor placement should be after selection + 2 tag lengths
expect(textArea.selectionStart).toBe(selectedIndex + selected.length + 2 * tag.length);
}); });
it('replaces the placeholder in the tag', () => { describe('with selection', () => {
insertMarkdownText({ const text = 'initial selected value';
textArea, const selected = 'selected';
text: textArea.value, beforeEach(() => {
tag: '[{text}](url)', textArea.value = text;
blockTag: null, const selectedIndex = text.indexOf(selected);
selected, textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
wrap: false,
}); });
expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`)); it('applies the tag to the selected value', () => {
}); const selectedIndex = text.indexOf(selected);
const tag = '*';
describe('and text to be selected', () => {
const tag = '[{text}](url)';
const select = 'url';
it('selects the text', () => {
insertMarkdownText({ insertMarkdownText({
textArea, textArea,
text: textArea.value, text: textArea.value,
tag, tag,
blockTag: null, blockTag: null,
selected, selected,
wrap: false, wrap: true,
select,
}); });
const expectedText = text.replace(selected, `[${selected}](url)`); expect(textArea.value).toEqual(text.replace(selected, `*${selected}*`));
expect(textArea.value).toEqual(expectedText); // cursor placement should be after selection + 2 tag lengths
expect(textArea.selectionStart).toEqual(expectedText.indexOf(select)); expect(textArea.selectionStart).toBe(selectedIndex + selected.length + 2 * tag.length);
expect(textArea.selectionEnd).toEqual(expectedText.indexOf(select) + select.length);
}); });
it('selects the right text when multiple tags are present', () => { it('replaces the placeholder in the tag', () => {
const initialValue = `${tag} ${tag} ${selected}`;
textArea.value = initialValue;
const selectedIndex = initialValue.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({ insertMarkdownText({
textArea, textArea,
text: textArea.value, text: textArea.value,
tag, tag: '[{text}](url)',
blockTag: null, blockTag: null,
selected, selected,
wrap: false, wrap: false,
select,
}); });
const expectedText = initialValue.replace(selected, `[${selected}](url)`); expect(textArea.value).toEqual(text.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);
}); });
it('should support selected urls', () => { describe('and text to be selected', () => {
const expectedUrl = 'http://www.gitlab.com'; const tag = '[{text}](url)';
const expectedSelectionText = 'text'; const select = 'url';
const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`;
const initialValue = `text ${expectedUrl} text`; 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);
});
textArea.value = initialValue; it('selects the right text when multiple tags are present', () => {
const selectedIndex = initialValue.indexOf(expectedUrl); const initialValue = `${tag} ${tag} ${selected}`;
textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); 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);
});
insertMarkdownText({ it('should support selected urls', () => {
textArea, const expectedUrl = 'http://www.gitlab.com';
text: textArea.value, const expectedSelectionText = 'text';
tag, const expectedText = `text [${expectedSelectionText}](${expectedUrl}) text`;
blockTag: null, const initialValue = `text ${expectedUrl} text`;
selected: expectedUrl,
wrap: false, textArea.value = initialValue;
select, const selectedIndex = initialValue.indexOf(expectedUrl);
textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length);
insertMarkdownText({
textArea,
text: textArea.value,
tag,
blockTag: null,
selected: expectedUrl,
wrap: false,
select,
});
expect(textArea.value).toEqual(expectedText);
expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1));
expect(textArea.selectionEnd).toEqual(
expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length,
);
}); });
});
});
});
describe('Ace Editor', () => {
let editor;
beforeEach(() => {
editor = {
getSelectionRange: () => ({
start: 0,
end: 0,
}),
getValue: () => 'this is text \n in two lines',
insert: () => {},
navigateLeft: () => {},
};
});
it('uses ace editor insert text when editor is passed in', () => {
spyOn(editor, 'insert');
expect(textArea.value).toEqual(expectedText); insertMarkdownText({
expect(textArea.selectionStart).toEqual(expectedText.indexOf(expectedSelectionText, 1)); text: editor.getValue,
expect(textArea.selectionEnd).toEqual( tag: '*',
expectedText.indexOf(expectedSelectionText, 1) + expectedSelectionText.length, blockTag: null,
); selected: '',
wrap: false,
editor,
});
expect(editor.insert).toHaveBeenCalled();
});
it('adds block tags on line above and below selection', () => {
spyOn(editor, 'insert');
const selected = 'this text \n is multiple \n lines';
const text = `before \n ${selected} \n after`;
insertMarkdownText({
text,
tag: '',
blockTag: '***',
selected,
wrap: true,
editor,
});
expect(editor.insert).toHaveBeenCalledWith(`***\n${selected}\n***`);
});
it('uses ace editor to navigate back tag length when nothing is selected', () => {
spyOn(editor, 'navigateLeft');
insertMarkdownText({
text: editor.getValue,
tag: '*',
blockTag: null,
selected: '',
wrap: true,
editor,
}); });
expect(editor.navigateLeft).toHaveBeenCalledWith(1);
});
it('ace editor does not navigate back when there is selected text', () => {
spyOn(editor, 'navigateLeft');
insertMarkdownText({
text: editor.getValue,
tag: '*',
blockTag: null,
selected: 'foobar',
wrap: true,
editor,
});
expect(editor.navigateLeft).not.toHaveBeenCalled();
}); });
}); });
}); });
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