From 5fbd75c9c145c2bd64abf2fdc2c53b2f0f1e10be Mon Sep 17 00:00:00 2001 From: Marius BALTEANU Date: Sun, 3 Jan 2021 01:20:04 +0200 Subject: [PATCH] Add keyboard shortcuts for bold, italic and underline buttons --- lib/redmine/platform.rb | 4 ++ public/javascripts/jstoolbar/jstoolbar.js | 50 +++++++++++++++--- public/javascripts/jstoolbar/markdown.js | 3 ++ public/javascripts/jstoolbar/textile.js | 3 ++ test/system/keyboard_shortcuts_test.rb | 62 +++++++++++++++++++++++ 5 files changed, 115 insertions(+), 7 deletions(-) diff --git a/lib/redmine/platform.rb b/lib/redmine/platform.rb index 9fcc5f9c6..039766f21 100644 --- a/lib/redmine/platform.rb +++ b/lib/redmine/platform.rb @@ -24,6 +24,10 @@ module Redmine (/(:?mswin|mingw)/.match?(RUBY_PLATFORM)) || (RUBY_PLATFORM == 'java' && /windows/i.match?(ENV['OS'] || ENV['os'])) end + + def osx? + (/(:?darwin)/.match?(RUBY_PLATFORM)) + end end end end diff --git a/public/javascripts/jstoolbar/jstoolbar.js b/public/javascripts/jstoolbar/jstoolbar.js index de4eb997a..ed6605965 100644 --- a/public/javascripts/jstoolbar/jstoolbar.js +++ b/public/javascripts/jstoolbar/jstoolbar.js @@ -22,6 +22,7 @@ /* Modified by JP LANG for textile formatting */ let lastJstPreviewed = null; +const isMac = Boolean(navigator.platform.toLowerCase().match(/mac/)); function jsToolBar(textarea) { if (!document.createElement) { return; } @@ -208,6 +209,7 @@ jsToolBar.prototype = { mode: 'wiki', elements: {}, help_link: '', + shortcuts: {}, getMode: function() { return this.mode; @@ -233,10 +235,27 @@ jsToolBar.prototype = { button: function(toolName) { var tool = this.elements[toolName]; if (typeof tool.fn[this.mode] != 'function') return null; - var b = new jsButton(tool.title, tool.fn[this.mode], this, 'jstb_'+toolName); + + const className = 'jstb_' + toolName; + let title = tool.title + + if (tool.hasOwnProperty('shortcut')) { + this.shortcuts[tool.shortcut] = className; + title = this.buttonTitleWithShortcut(tool.title, tool.shortcut) + } + + var b = new jsButton(title, tool.fn[this.mode], this, className); if (tool.icon != undefined) b.icon = tool.icon; + return b; }, + buttonTitleWithShortcut: function(title, shortcutKey) { + if (isMac) { + return title + " (⌘" + shortcutKey.toUpperCase() + ")"; + } else { + return title + " (Ctrl+" + shortcutKey.toUpperCase() + ")"; + } + }, space: function(toolName) { var tool = new jsSpace(toolName) if (this.elements[toolName].width !== undefined) @@ -409,7 +428,7 @@ jsToolBar.prototype = { this.toolbar.classList.add('hidden'); this.textarea.classList.add('hidden'); this.preview.classList.remove('hidden'); - this.tabsBlock.getElementsByClassName('tab-edit')[0].classList.remove('selected'); + this.tabsBlock.querySelector('.tab-edit').classList.remove('selected'); event.target.classList.add('selected'); }, hidePreview: function(event) { @@ -418,18 +437,26 @@ jsToolBar.prototype = { this.textarea.classList.remove('hidden'); this.textarea.focus(); this.preview.classList.add('hidden'); - this.tabsBlock.getElementsByClassName('tab-preview')[0].classList.remove('selected'); + this.tabsBlock.querySelector('.tab-preview').classList.remove('selected'); event.target.classList.add('selected'); }, keyboardShortcuts: function(e) { + let stop = false; if (isToogleEditPreviewShortcut(e)) { - // Switch to preview only if tab edit is selected when the event triggered. + // Switch to preview only if Edit tab is selected when the event triggers. if (this.tabsBlock.querySelector('.tab-edit.selected')) { - e.stopPropagation(); - e.preventDefault(); - this.tabsBlock.getElementsByClassName('tab-preview')[0].click(); + stop = true + this.tabsBlock.querySelector('.tab-preview').click(); } } + if (isModifierKey(e) && this.shortcuts.hasOwnProperty(e.key.toLowerCase())) { + stop = true + this.toolbar.querySelector("." + this.shortcuts[e.key.toLowerCase()]).click(); + } + if (stop) { + e.stopPropagation(); + e.preventDefault(); + } }, stripBaseURL: function(url) { if (this.base_url != '') { @@ -539,4 +566,13 @@ function isToogleEditPreviewShortcut(e) { } else { return false; } +} +function isModifierKey(e) { + if (isMac && e.metaKey) { + return true; + } else if (!isMac && e.ctrlKey) { + return true; + } else { + return false; + } } \ No newline at end of file diff --git a/public/javascripts/jstoolbar/markdown.js b/public/javascripts/jstoolbar/markdown.js index c30de3096..ae2725269 100644 --- a/public/javascripts/jstoolbar/markdown.js +++ b/public/javascripts/jstoolbar/markdown.js @@ -26,6 +26,7 @@ jsToolBar.prototype.elements.strong = { type: 'button', title: 'Strong', + shortcut: 'b', fn: { wiki: function() { this.singleTag('**') } } @@ -35,6 +36,7 @@ jsToolBar.prototype.elements.strong = { jsToolBar.prototype.elements.em = { type: 'button', title: 'Italic', + shortcut: 'i', fn: { wiki: function() { this.singleTag("*") } } @@ -44,6 +46,7 @@ jsToolBar.prototype.elements.em = { jsToolBar.prototype.elements.ins = { type: 'button', title: 'Underline', + shortcut: 'u', fn: { wiki: function() { this.singleTag('_') } } diff --git a/public/javascripts/jstoolbar/textile.js b/public/javascripts/jstoolbar/textile.js index 9adc77add..76d2170cc 100644 --- a/public/javascripts/jstoolbar/textile.js +++ b/public/javascripts/jstoolbar/textile.js @@ -26,6 +26,7 @@ jsToolBar.prototype.elements.strong = { type: 'button', title: 'Strong', + shortcut: 'b', fn: { wiki: function() { this.singleTag('*') } } @@ -35,6 +36,7 @@ jsToolBar.prototype.elements.strong = { jsToolBar.prototype.elements.em = { type: 'button', title: 'Italic', + shortcut: 'i', fn: { wiki: function() { this.singleTag("_") } } @@ -44,6 +46,7 @@ jsToolBar.prototype.elements.em = { jsToolBar.prototype.elements.ins = { type: 'button', title: 'Underline', + shortcut: 'u', fn: { wiki: function() { this.singleTag('+') } } diff --git a/test/system/keyboard_shortcuts_test.rb b/test/system/keyboard_shortcuts_test.rb index 115ee6d74..c71e3e802 100644 --- a/test/system/keyboard_shortcuts_test.rb +++ b/test/system/keyboard_shortcuts_test.rb @@ -68,4 +68,66 @@ class InlineAutocompleteSystemTest < ApplicationSystemTestCase find 'textarea#issue_notes', :visible => true find 'div#preview_issue_notes', :visible => false end + + def test_keyboard_shortcuts_for_wiki_toolbar_buttons_using_textile + with_settings :text_formatting => 'textile' do + log_user('jsmith', 'jsmith') + visit 'issues/new' + + find('#issue_description').click.send_keys([modifier_key, 'b']) + assert_equal '**', find('#issue_description').value + + # Clear textarea value + fill_in 'Description', :with => '' + find('#issue_description').send_keys([modifier_key, 'u']) + assert_equal '++', find('#issue_description').value + + # Clear textarea value + fill_in 'Description', :with => '' + find('#issue_description').send_keys([modifier_key, 'i']) + assert_equal '__', find('#issue_description').value + end + end + + def test_keyboard_shortcuts_for_wiki_toolbar_buttons_using_markdown + with_settings :text_formatting => 'markdown' do + log_user('jsmith', 'jsmith') + visit 'issues/new' + + find('#issue_description').click.send_keys([modifier_key, 'b']) + assert_equal '****', find('#issue_description').value + + # Clear textarea value + fill_in 'Description', :with => '' + find('#issue_description').send_keys([modifier_key, 'u']) + assert_equal '__', find('#issue_description').value + + # Clear textarea value + fill_in 'Description', :with => '' + find('#issue_description').send_keys([modifier_key, 'i']) + assert_equal '**', find('#issue_description').value + end + end + + def test_keyboard_shortcuts_keys_should_be_shown_in_button_title + log_user('jsmith', 'jsmith') + visit 'issues/new' + + within('.jstBlock .jstElements') do + assert_equal "Strong (#{modifier_key_title}B)", find('button.jstb_strong')['title'] + assert_equal "Italic (#{modifier_key_title}I)", find('button.jstb_em')['title'] + assert_equal "Underline (#{modifier_key_title}U)", find('button.jstb_ins')['title'] + end + end + + private + + def modifier_key + modifier = osx? ? "command" : "control" + modifier.to_sym + end + + def modifier_key_title + osx? ? "⌘" : "Ctrl+" + end end -- 2.22.0