diff options
Diffstat (limited to 'app/assets/javascripts/behaviors')
8 files changed, 158 insertions, 26 deletions
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index add43b81f6d..d61797b7ae4 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,8 +1,13 @@ import Autosize from 'autosize'; +import { waitForCSSLoaded } from '../helpers/startup_css_helper'; document.addEventListener('DOMContentLoaded', () => { - const autosizeEls = document.querySelectorAll('.js-autosize'); + waitForCSSLoaded(() => { + const autosizeEls = document.querySelectorAll('.js-autosize'); - Autosize(autosizeEls); - Autosize.update(autosizeEls); + Autosize(autosizeEls); + Autosize.update(autosizeEls); + + autosizeEls.forEach(el => el.classList.add('js-autosize-initialized')); + }); }); diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 8060938c72a..fd12c282b62 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,7 +1,7 @@ +import $ from 'jquery'; import './autosize'; import './bind_in_out'; import './markdown/render_gfm'; -import initGFMInput from './markdown/gfm_auto_complete'; import initCopyAsGFM from './markdown/copy_as_gfm'; import initCopyToClipboard from './copy_to_clipboard'; import './details_behavior'; @@ -15,9 +15,27 @@ import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resi import initSelect2Dropdowns from './select2'; installGlEmojiElement(); -initGFMInput(); + initCopyAsGFM(); initCopyToClipboard(); + initPageShortcuts(); initCollapseSidebarOnWindowResize(); initSelect2Dropdowns(); + +document.addEventListener('DOMContentLoaded', () => { + window.requestIdleCallback( + () => { + // Check if we have to Load GFM Input + const $gfmInputs = $('.js-gfm-input:not(.js-gfm-input-initialized)'); + if ($gfmInputs.length) { + import(/* webpackChunkName: 'initGFMInput' */ './markdown/gfm_auto_complete') + .then(({ default: initGFMInput }) => { + initGFMInput($gfmInputs); + }) + .catch(() => {}); + } + }, + { timeout: 500 }, + ); +}); diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js index 6bbd2133344..d712c90242c 100644 --- a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js +++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js @@ -2,8 +2,8 @@ import $ from 'jquery'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import { parseBoolean } from '~/lib/utils/common_utils'; -export default function initGFMInput() { - $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { +export default function initGFMInput($els) { + $els.each((i, el) => { const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); const enableGFM = parseBoolean(el.dataset.supportsAutocomplete); @@ -14,6 +14,7 @@ export default function initGFMInput() { milestones: enableGFM, mergeRequests: enableGFM, labels: enableGFM, + vulnerabilities: enableGFM, }); }); } diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 01627b7206d..5e9d80e1529 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -5,7 +5,6 @@ import renderMermaid from './render_mermaid'; import renderMetrics from './render_metrics'; import highlightCurrentUser from './highlight_current_user'; import initUserPopovers from '../../user_popovers'; -import initMRPopovers from '../../mr_popover'; // Render GitLab flavoured Markdown // @@ -17,9 +16,25 @@ $.fn.renderGFM = function renderGFM() { renderMermaid(this.find('.js-render-mermaid')); highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.js-user-link').get()); - initMRPopovers(this.find('.gfm-merge_request').get()); + + const mrPopoverElements = this.find('.gfm-merge_request').get(); + if (mrPopoverElements.length) { + import(/* webpackChunkName: 'MrPopoverBundle' */ '../../mr_popover') + .then(({ default: initMRPopovers }) => { + initMRPopovers(mrPopoverElements); + }) + .catch(() => {}); + } + renderMetrics(this.find('.js-render-metrics').get()); return this; }; -$(() => $('body').renderGFM()); +$(() => { + window.requestIdleCallback( + () => { + $('body').renderGFM(); + }, + { timeout: 500 }, + ); +}); diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 03d9955f8fc..30783562da9 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,5 +1,6 @@ import { deprecatedCreateFlash as flash } from '~/flash'; import { s__, sprintf } from '~/locale'; +import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; // Renders math using KaTeX in any element with the // `js-render-math` class @@ -111,7 +112,7 @@ class SafeMathRenderer { // Give the browser time to reflow the svg waitForReflow(() => { - const deltaTime = Date.now() - this.startTime; + const deltaTime = differenceInMilliseconds(this.startTime); this.totalMS += deltaTime; this.renderElement(); diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js index 7987a533ae5..7352be0dbd5 100644 --- a/app/assets/javascripts/behaviors/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts.js @@ -1,5 +1,3 @@ -import Shortcuts from './shortcuts/shortcuts'; - export default function initPageShortcuts() { const { page } = document.body.dataset; const pagesWithCustomShortcuts = [ @@ -29,7 +27,9 @@ export default function initPageShortcuts() { // the pages above have their own shortcuts sub-classes instantiated elsewhere // TODO: replace this whitelist with something more automated/maintainable if (page && !pagesWithCustomShortcuts.includes(page)) { - return new Shortcuts(); + import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts') + .then(({ default: Shortcuts }) => new Shortcuts()) + .catch(() => {}); } return false; } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 85636f3e5d2..8a8b61a57cd 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -2,6 +2,7 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; import Vue from 'vue'; +import { flatten } from 'lodash'; import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle'; import ShortcutsToggle from './shortcuts_toggle.vue'; import axios from '../../lib/utils/axios_utils'; @@ -9,13 +10,13 @@ import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils'; -const defaultStopCallback = Mousetrap.stopCallback; -Mousetrap.stopCallback = (e, element, combo) => { +const defaultStopCallback = Mousetrap.prototype.stopCallback; +Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) { return false; } - return defaultStopCallback(e, element, combo); + return defaultStopCallback.call(this, e, element, combo); }; function initToggleButton() { @@ -27,6 +28,39 @@ function initToggleButton() { }); } +/** + * The key used to save and fetch the local Mousetrap instance + * attached to a `<textarea>` element using `jQuery.data` + */ +const LOCAL_MOUSETRAP_DATA_KEY = 'local-mousetrap-instance'; + +/** + * Gets a mapping of toolbar button => keyboard shortcuts + * associated to the given markdown editor `<textarea>` element + * + * @param {HTMLTextAreaElement} $textarea The jQuery-wrapped `<textarea>` + * element to extract keyboard shortcuts from + * + * @returns A Map with keys that are jQuery-wrapped toolbar buttons + * (i.e. `$toolbarBtn`) and values that are arrays of string + * keyboard shortcuts (e.g. `['command+k', 'ctrl+k]`). + */ +function getToolbarBtnToShortcutsMap($textarea) { + const $allToolbarBtns = $textarea.closest('.md-area').find('.js-md'); + const map = new Map(); + + $allToolbarBtns.each(function attachToolbarBtnHandler() { + const $toolbarBtn = $(this); + const keyboardShortcuts = $toolbarBtn.data('md-shortcuts'); + + if (keyboardShortcuts?.length) { + map.set($toolbarBtn, keyboardShortcuts); + } + }); + + return map; +} + export default class Shortcuts { constructor() { this.onToggleHelp = this.onToggleHelp.bind(this); @@ -34,6 +68,7 @@ export default class Shortcuts { Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('s', Shortcuts.focusSearch); + Mousetrap.bind('/', Shortcuts.focusSearch); Mousetrap.bind('f', this.focusFilter.bind(this)); Mousetrap.bind('p b', Shortcuts.onTogglePerfBar); @@ -143,4 +178,62 @@ export default class Shortcuts { e.preventDefault(); } } + + /** + * Initializes markdown editor shortcuts on the provided `<textarea>` element + * + * @param {JQuery} $textarea The jQuery-wrapped `<textarea>` element + * where markdown shortcuts should be enabled + * @param {Function} handler The handler to call when a + * keyboard shortcut is pressed inside the markdown `<textarea>` + */ + static initMarkdownEditorShortcuts($textarea, handler) { + const toolbarBtnToShortcutsMap = getToolbarBtnToShortcutsMap($textarea); + + const localMousetrap = new Mousetrap($textarea[0]); + + // Save a reference to the local mousetrap instance on the <textarea> + // so that it can be retrieved when unbinding shortcut handlers + $textarea.data(LOCAL_MOUSETRAP_DATA_KEY, localMousetrap); + + toolbarBtnToShortcutsMap.forEach((keyboardShortcuts, $toolbarBtn) => { + localMousetrap.bind(keyboardShortcuts, e => { + e.preventDefault(); + + handler($toolbarBtn); + }); + }); + + // Get an array of all shortcut strings that have been added above + const allShortcuts = flatten([...toolbarBtnToShortcutsMap.values()]); + + const originalStopCallback = Mousetrap.prototype.stopCallback; + localMousetrap.stopCallback = function newStopCallback(e, element, combo) { + if (allShortcuts.includes(combo)) { + return false; + } + + return originalStopCallback.call(this, e, element, combo); + }; + } + + /** + * Removes markdown editor shortcut handlers originally attached + * with `initMarkdownEditorShortcuts`. + * + * Note: it is safe to call this function even if `initMarkdownEditorShortcuts` + * has _not_ yet been called on the given `<textarea>`. + * + * @param {JQuery} $textarea The jQuery-wrapped `<textarea>` + * to remove shortcut handlers from + */ + static removeMarkdownEditorShortcuts($textarea) { + const localMousetrap = $textarea.data(LOCAL_MOUSETRAP_DATA_KEY); + + if (localMousetrap) { + getToolbarBtnToShortcutsMap($textarea).forEach(keyboardShortcuts => { + localMousetrap.unbind(keyboardShortcuts); + }); + } + } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js index 8658081c6c2..f0d2ecfd210 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js @@ -5,12 +5,11 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { constructor(projectFindFile) { super(); - const oldStopCallback = Mousetrap.stopCallback; - this.projectFindFile = projectFindFile; + const oldStopCallback = Mousetrap.prototype.stopCallback; - Mousetrap.stopCallback = (e, element, combo) => { + Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo) { if ( - element === this.projectFindFile.inputElement[0] && + element === projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter') ) { // when press up/down key in textbox, cursor prevent to move to home/end @@ -18,12 +17,12 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { return false; } - return oldStopCallback(e, element, combo); + return oldStopCallback.call(this, e, element, combo); }; - Mousetrap.bind('up', this.projectFindFile.selectRowUp); - Mousetrap.bind('down', this.projectFindFile.selectRowDown); - Mousetrap.bind('esc', this.projectFindFile.goToTree); - Mousetrap.bind('enter', this.projectFindFile.goToBlob); + Mousetrap.bind('up', projectFindFile.selectRowUp); + Mousetrap.bind('down', projectFindFile.selectRowDown); + Mousetrap.bind('esc', projectFindFile.goToTree); + Mousetrap.bind('enter', projectFindFile.goToBlob); } } |