diff options
author | Sam Bigelow <sbigelow@gitlab.com> | 2018-12-14 12:56:25 -0500 |
---|---|---|
committer | Sam Bigelow <sbigelow@gitlab.com> | 2019-01-02 08:24:32 -0800 |
commit | 066a99b6e9d0849b477858c9aac274c2f81bb367 (patch) | |
tree | b98cd6b7ea66d64c4e7aa1dbb86055b48bed59cc /app/assets | |
parent | 28cffb9f41836a84d3323e640fe31f92f37bccd9 (diff) | |
download | gitlab-ce-066a99b6e9d0849b477858c9aac274c2f81bb367.tar.gz |
Add markdown buttons to file editor27861-add-markdown-editing-buttons-to-the-file-editor
Currently, we have markdown files in many places (e.g. comments, new
issues, etc.). This Merge Request detects if the file being edited is a
markdown file and adds markdown buttons and their functionality to the
single file editor (Not the web IDE)
Diffstat (limited to 'app/assets')
-rw-r--r-- | app/assets/javascripts/blob_edit/blob_bundle.js | 9 | ||||
-rw-r--r-- | app/assets/javascripts/blob_edit/edit_blob.js | 20 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 162 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/markdown_area.scss | 2 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/editor.scss | 4 |
5 files changed, 159 insertions, 38 deletions
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index b07f951346e..5f64175362d 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -16,6 +16,7 @@ export default () => { const filePath = editBlobForm.data('blobFilename'); const currentAction = $('.js-file-title').data('currentAction'); const projectId = editBlobForm.data('project-id'); + const isMarkdown = editBlobForm.data('is-markdown'); const commitButton = $('.js-commit-button'); const cancelLink = $('.btn.btn-cancel'); @@ -27,7 +28,13 @@ export default () => { window.onbeforeunload = null; }); - new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId); + new EditBlob({ + assetsPath: `${urlRoot}${assetsPath}`, + filePath, + currentAction, + projectId, + isMarkdown, + }); new NewCommitForm(editBlobForm); // returning here blocks page navigation diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 6e19548eed2..011898a5e7a 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -6,22 +6,31 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; import getModeByFileExtension from '~/lib/utils/ace_utils'; +import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; export default class EditBlob { - constructor(assetsPath, aceMode, currentAction, projectId) { - this.configureAceEditor(aceMode, assetsPath); + // The options object has: + // assetsPath, filePath, currentAction, projectId, isMarkdown + constructor(options) { + this.options = options; + this.configureAceEditor(); this.initModePanesAndLinks(); 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.loadModule('ace/ext/searchbox'); ace.config.loadModule('ace/ext/modelist'); this.editor = ace.edit('editor'); + if (isMarkdown) { + addEditorMarkdownListeners(this.editor); + } + // This prevents warnings re: automatic scrolling being logged this.editor.$blockScrolling = Infinity; @@ -32,7 +41,8 @@ export default class EditBlob { } } - initFileSelectors(currentAction, projectId) { + initFileSelectors() { + const { currentAction, projectId } = this.options; this.fileTemplateMediator = new TemplateSelectorMediator({ currentAction, editor: this.editor, diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 1254ec798a6..84a617acb42 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -8,6 +8,10 @@ function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } +function addBlockTags(blockTag, selected) { + return `${blockTag}\n${selected}\n${blockTag}`; +} + function lineBefore(text, textarea) { var split; split = text @@ -24,19 +28,45 @@ function lineAfter(text, textarea) { .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) { - const before = lineBefore(text, textArea); - const after = lineAfter(text, textArea); - if (before === blockTag && after === blockTag) { + const shouldRemoveBlock = + lineBefore(text, textArea) === blockTag && lineAfter(text, textArea) === blockTag; + + if (shouldRemoveBlock) { // To remove the block tag we have to select the line before & after if (blockTag != null) { textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1); textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1); } return selected; - } else { - return blockTag + '\n' + selected + '\n' + blockTag; } + return addBlockTags(blockTag, selected); } function moveCursor({ @@ -46,33 +76,48 @@ function moveCursor({ positionBetweenTags, removedLastNewLine, select, + editor, + editorSelectionStart, + editorSelectionEnd, }) { var pos; - if (!textArea.setSelectionRange) { + if (textArea && !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 (positionBetweenTags) { - pos = textArea.selectionStart - tag.length; - } else { - pos = textArea.selectionStart; + if (textArea) { + // 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); + } else if (editor) { + editor.navigateLeft(tag.length - tag.indexOf(select)); + editor.getSelection().selectAWord(); + return; } + } + if (textArea) { + if (textArea.selectionStart === textArea.selectionEnd) { + if (positionBetweenTags) { + pos = textArea.selectionStart - tag.length; + } else { + pos = textArea.selectionStart; + } - if (removedLastNewLine) { - pos -= 1; - } + if (removedLastNewLine) { + pos -= 1; + } - if (cursorOffset) { - pos -= cursorOffset; - } + if (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({ selected = '', wrap, select, + editor, }) { var textToInsert, selectedSplit, @@ -92,11 +138,20 @@ export function insertMarkdownText({ removedLastNewLine, removedFirstNewLine, currentLineEmpty, - lastNewLine; + lastNewLine, + editorSelectionStart, + editorSelectionEnd; removedLastNewLine = false; removedFirstNewLine = 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 // if so fill in the url part instead of the text part of the pattern. if (tag === LINK_TAG_PATTERN) { @@ -119,14 +174,27 @@ export function insertMarkdownText({ } // Remove the last newline - if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { - removedLastNewLine = true; - selected = selected.replace(/\n$/, ''); + if (textArea) { + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } + } else if (editor) { + if (editorSelectionStart.row !== editorSelectionEnd.row) { + removedLastNewLine = true; + selected = selected.replace(/\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'); // Check whether the current line is empty or consists only of spaces(=handle as empty) @@ -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}'; if (selectedSplit.length > 1 && (!wrap || (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 { textToInsert = selectedSplit .map(function(val) { @@ -170,7 +244,11 @@ export function insertMarkdownText({ textToInsert += '\n'; } - insertText(textArea, textToInsert); + if (editor) { + editor.insert(textToInsert); + } else { + insertText(textArea, textToInsert); + } return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), @@ -178,6 +256,9 @@ export function insertMarkdownText({ positionBetweenTags: wrap && selected.length === 0, removedLastNewLine, select, + editor, + editorSelectionStart, + editorSelectionEnd, }); } @@ -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) { return $('.js-md', form).off('click'); } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 5609a2086e6..eb191cf4d39 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -173,7 +173,7 @@ svg { width: 14px; height: 14px; - margin-top: 3px; + vertical-align: middle; fill: $gl-text-color-secondary; } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index f46ff360496..5a988b184b6 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -128,6 +128,10 @@ width: 100%; } } + + @media(max-width: map-get($grid-breakpoints, md)-1) { + clear: both; + } } .editor-ref { |