diff options
author | Jacob Schatz <jschatz@gitlab.com> | 2016-06-20 17:18:13 +0000 |
---|---|---|
committer | Jacob Schatz <jschatz@gitlab.com> | 2016-06-20 17:18:13 +0000 |
commit | a0aaa6a0b8427ff1fd6811adb4cf59ab8e64a969 (patch) | |
tree | 28d14b3b5330319af686be0aa2fd30739d382742 | |
parent | d32ac35642aa0d75ecf5fafd7cb3e3665bbe2581 (diff) | |
parent | b543a96e1c71c9233f9425cee2435cd0497adc4c (diff) | |
download | gitlab-ce-a0aaa6a0b8427ff1fd6811adb4cf59ab8e64a969.tar.gz |
Merge branch 'markdowner' into 'master'
POC: Markdown shortcut buttons
## What does this MR do?
Adds markdown shortcut buttons to text area for comments.
## Are there points in the code the reviewer needs to double check?
Because changing `textarea.val('something')` kills the natural browser undo stack, I had to implement a custom undo stack using state. You can't use the "undoable" state undo pattern because you need to go back to a previous state regardless of cursor position. The undo also adds an undo history item once you delete stuff or press enter.
You can also edit multiple textareas at once and it will keep an undo history for each textarea individually, so the undo state should not collide between textareas.
## Why was this MR needed?
It has been requested multiple times and the competition has it. https://gitlab.com/gitlab-org/gitlab-ce/issues/17185#note_12073433
Libraries are available that already implement this functionality but they are enormous and bloaty. I implemented this in very few lines of code and kept it very simple and as minimal as possible. This was also some competitions approach. I believe so as to not include too much JS.
Adding extra buttons with new functionality **should only need new HTML and no new JS**.
Only extra complex thing was adding a overridden undo stack, which was made as simple as possible as well.
## What are the relevant issue numbers?
https://gitlab.com/gitlab-org/gitlab-ce/issues/17185#note_12073433
## Screenshots (if relevant)
**NOTE:** One thing you cannot see in this screenshot is that I am pressing <kbd>Cmd</kbd><kbd>Z</kbd> to undo and <kbd>Cmd</kbd><kbd>Shift</kbd><kbd>Z</kbd> to redo which is the undo/redo stack I implemented. <kbd>Ctrl</kbd><kbd>Y</kbd> also works for redo.
![markdown-editor](/uploads/2517bfb1a7b4269da7fcc4003c88b7f6/markdown-editor.gif)
cc @dzaporozhets for UI
cc @iamphill @alfredo1 for JS review
cc @JobV if you like the idea.
Fixes: #17185
See merge request !4305
-rw-r--r-- | app/assets/javascripts/gl_form.js.coffee | 3 | ||||
-rw-r--r-- | app/assets/javascripts/lib/text_utility.js.coffee | 79 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/markdown_area.scss | 28 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/note_form.scss | 4 | ||||
-rw-r--r-- | app/helpers/gitlab_markdown_helper.rb | 13 | ||||
-rw-r--r-- | app/views/projects/_md_preview.html.haml | 15 | ||||
-rw-r--r-- | app/views/projects/notes/_hints.html.haml | 2 | ||||
-rw-r--r-- | spec/features/issues_spec.rb | 2 | ||||
-rw-r--r-- | spec/javascripts/issue_spec.js.coffee | 13 |
9 files changed, 148 insertions, 11 deletions
diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee index d540cc4dc46..77512d187c9 100644 --- a/app/assets/javascripts/gl_form.js.coffee +++ b/app/assets/javascripts/gl_form.js.coffee @@ -34,6 +34,8 @@ class @GLForm # form and textarea event listeners @addEventListeners() + gl.text.init(@form) + # hide discard button @form.find('.js-note-discard').hide() @@ -42,6 +44,7 @@ class @GLForm clearEventListeners: -> @textarea.off 'focus' @textarea.off 'blur' + gl.text.removeListeners(@form) addEventListeners: -> @textarea.on 'focus', -> diff --git a/app/assets/javascripts/lib/text_utility.js.coffee b/app/assets/javascripts/lib/text_utility.js.coffee new file mode 100644 index 00000000000..bb2772dfed2 --- /dev/null +++ b/app/assets/javascripts/lib/text_utility.js.coffee @@ -0,0 +1,79 @@ +((w) -> + w.gl ?= {} + w.gl.text ?= {} + + gl.text.randomString = -> Math.random().toString(36).substring(7) + + gl.text.replaceRange = (s, start, end, substitute) -> + s.substring(0, start) + substitute + s.substring(end); + + gl.text.selectedText = (text, textarea) -> + text.substring(textarea.selectionStart, textarea.selectionEnd) + + gl.text.insertText = (textArea, text, tag, selected, wrap) -> + selectedSplit = selected.split('\n') + startChar = if not wrap and textArea.selectionStart > 0 then '\n' else '' + + if selectedSplit.length > 1 and not wrap + insertText = selectedSplit.map((val) -> + if val.indexOf(tag) is 0 + "#{val.replace(tag, '')}" + else + "#{tag}#{val}" + ).join('\n') + else + insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}" + + if document.queryCommandSupported('insertText') + document.execCommand 'insertText', false, insertText + else + try + document.execCommand("ms-beginUndoUnit") + + textArea.value = @replaceRange( + text, + textArea.selectionStart, + textArea.selectionEnd, + insertText) + try + document.execCommand("ms-endUndoUnit") + + @moveCursor(textArea, tag, wrap) + + gl.text.moveCursor = (textArea, tag, wrapped) -> + return unless textArea.setSelectionRange + + if textArea.selectionStart is textArea.selectionEnd + if wrapped + pos = textArea.selectionStart - tag.length + else + pos = textArea.selectionStart + + textArea.setSelectionRange pos, pos + + gl.text.updateText = (textArea, tag, wrap) -> + $textArea = $(textArea) + oldVal = $textArea.val() + textArea = $textArea.get(0) + text = $textArea.val() + selected = @selectedText(text, textArea) + $textArea.focus() + + @insertText(textArea, text, tag, selected, wrap) + + gl.text.init = (form) -> + self = @ + $('.js-md', form) + .off 'click' + .on 'click', -> + $this = $(@) + self.updateText( + $this.closest('.md-area').find('textarea'), + $this.data('md-tag'), + not $this.data('md-prepend') + ) + + gl.text.removeListeners = (form) -> + $('.js-md', form).off() + +) window diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index fd885b38680..25f9c50258e 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -65,6 +65,11 @@ a { padding-top: 0; line-height: 1; + border-bottom: 1px solid $border-color; + + &.btn.btn-xs { + padding: 2px 5px; + } } } } @@ -99,3 +104,26 @@ } } } + +.toolbar-group { + float: left; + margin-right: -5px; + margin-left: $gl-padding; + + &:first-child { + margin-left: 0; + } +} + +.toolbar-btn { + float: left; + padding: 0 5px; + color: #959494; + background: transparent; + border: 0; + outline: 0; + + &:hover { + color: $gl-link-color; + } +} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index de5ccbe2b6c..3784010348a 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -179,6 +179,10 @@ border-top: 1px solid $border-color; } +.md-helper { + padding-top: 10px; +} + .toolbar-button { padding: 0; background: none; diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index 067a00660aa..a0dafc52622 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -185,4 +185,17 @@ module GitlabMarkdownHelper '' end end + + def markdown_toolbar_button(options = {}) + data = options[:data].merge({ container: "body" }) + content_tag :button, + type: "button", + class: "toolbar-btn js-md has-tooltip hidden-xs", + tabindex: -1, + data: data, + title: options[:title], + aria: { label: options[:title] } do + icon(options[:icon]) + end + end end diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 28a28282fd3..ca6714ef42b 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -14,8 +14,17 @@ %span This is a confidential issue. Your comment will not be visible to the public. %li.pull-right - %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 } - Go full screen + .toolbar-group + = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" }) + = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" }) + = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) + = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`" }, title: "Insert code" }) + = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) + = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) + = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) + .toolbar-group + %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } + =icon("arrows-alt fw") .md-write-holder = yield @@ -24,7 +33,7 @@ - if defined?(referenced_users) && referenced_users %div.referenced-users.hide %span - = icon('exclamation-triangle') + = icon("exclamation-triangle") You are about to add %strong %span.js-referenced-users-count 0 diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml index 0b002043408..7d1cbc62e86 100644 --- a/app/views/projects/notes/_hints.html.haml +++ b/app/views/projects/notes/_hints.html.haml @@ -5,4 +5,4 @@ is supported %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } = icon('file-image-o', class: 'toolbar-button-icon') - Attach a file + Attach a file
\ No newline at end of file diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index c3cb3379440..5065dfb849c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -22,7 +22,7 @@ describe 'Issues', feature: true do before do visit edit_namespace_project_issue_path(project.namespace, project, issue) - click_button "Go full screen" + find('.js-zen-enter').click end it 'should open new issue popup' do diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index ea27f36e9b5..71f0c1076c5 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -1,3 +1,4 @@ +#= require lib/text_utility #= require issue describe 'Issue', -> @@ -38,7 +39,7 @@ describe 'reopen/close issue', -> expect(typeof $btnClose.prop('disabled')).toBe('undefined') $btnClose.trigger('click') - + expect($btnReopen).toBeVisible() expect($btnClose).toBeHidden() expect($('div.status-box-closed')).toBeVisible() @@ -50,7 +51,7 @@ describe 'reopen/close issue', -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://goesnowhere.nothing/whereami') req.success saved: false - + $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') $btnClose.attr('href','http://goesnowhere.nothing/whereami') @@ -59,7 +60,7 @@ describe 'reopen/close issue', -> expect(typeof $btnClose.prop('disabled')).toBe('undefined') $btnClose.trigger('click') - + expect($btnReopen).toBeHidden() expect($btnClose).toBeVisible() expect($('div.status-box-closed')).toBeHidden() @@ -73,7 +74,7 @@ describe 'reopen/close issue', -> expect(req.type).toBe('PUT') expect(req.url).toBe('http://goesnowhere.nothing/whereami') req.error() - + $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') $btnClose.attr('href','http://goesnowhere.nothing/whereami') @@ -82,7 +83,7 @@ describe 'reopen/close issue', -> expect(typeof $btnClose.prop('disabled')).toBe('undefined') $btnClose.trigger('click') - + expect($btnReopen).toBeHidden() expect($btnClose).toBeVisible() expect($('div.status-box-closed')).toBeHidden() @@ -105,4 +106,4 @@ describe 'reopen/close issue', -> expect($btnReopen).toBeHidden() expect($btnClose).toBeVisible() expect($('div.status-box-open')).toBeVisible() - expect($('div.status-box-closed')).toBeHidden()
\ No newline at end of file + expect($('div.status-box-closed')).toBeHidden() |