diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-19 14:01:45 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-12-19 14:01:45 +0300 |
commit | 9297025d0b7ddf095eb618dfaaab2ff8f2018d8b (patch) | |
tree | 865198c01d1824a9b098127baa3ab980c9cd2c06 /app/assets/javascripts/behaviors | |
parent | 6372471f43ee03c05a7c1f8b0c6ac6b8a7431dbe (diff) |
Add latest changes from gitlab-org/gitlab@16-7-stable-eev16.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/behaviors')
12 files changed, 114 insertions, 90 deletions
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 84ff8fa7f33..fe3868fdd04 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -6,9 +6,9 @@ import installGlEmojiElement from './gl_emoji'; import initCopyAsGFM from './markdown/copy_as_gfm'; import './quick_submit'; import './requires_input'; -import initPageShortcuts from './shortcuts'; import { initToastMessages } from './toasts'; import { initGlobalAlerts } from './global_alerts'; +import './shortcuts'; import './toggler_behavior'; import './preview_markdown'; @@ -17,7 +17,6 @@ installGlEmojiElement(); initCopyAsGFM(); initCopyToClipboard(); -initPageShortcuts(); initCollapseSidebarOnWindowResize(); initToastMessages(); diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 36317444af9..72aae254584 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -152,7 +152,9 @@ export class CopyAsGFM { if (lineElements.length > 0) { for (let i = 0; i < lineElements.length; i += 1) { const lineElement = lineElements[i]; - codeElement.appendChild(lineElement); + const line = document.createElement('span'); + line.append(...lineElement.childNodes); + codeElement.appendChild(line); codeElement.appendChild(document.createTextNode('\n')); } } else { diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js deleted file mode 100644 index 22a8be92e52..00000000000 --- a/app/assets/javascripts/behaviors/shortcuts.js +++ /dev/null @@ -1,36 +0,0 @@ -export default function initPageShortcuts() { - const { page } = document.body.dataset; - const pagesWithCustomShortcuts = [ - 'projects:activity', - 'projects:artifacts:browse', - 'projects:artifacts:file', - 'projects:blame:show', - 'projects:blob:show', - 'projects:commit:show', - 'projects:commits:show', - 'projects:find_file:show', - 'projects:issues:edit', - 'projects:issues:index', - 'projects:issues:new', - 'projects:issues:show', - 'projects:merge_requests:creations:diffs', - 'projects:merge_requests:creations:new', - 'projects:merge_requests:edit', - 'projects:merge_requests:index', - 'projects:merge_requests:show', - 'projects:network:show', - 'projects:show', - 'projects:tree:show', - 'groups:show', - ]; - - // the pages above have their own shortcuts sub-classes instantiated elsewhere - // TODO: replace this whitelist with something more automated/maintainable - // https://gitlab.com/gitlab-org/gitlab/-/issues/392845 - if (page && !pagesWithCustomShortcuts.includes(page)) { - import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts') - .then(({ default: Shortcuts }) => new Shortcuts()) - .catch(() => {}); - } - return false; -} diff --git a/app/assets/javascripts/behaviors/shortcuts/index.js b/app/assets/javascripts/behaviors/shortcuts/index.js new file mode 100644 index 00000000000..cc6d8a23f68 --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts/index.js @@ -0,0 +1,16 @@ +const shortcutsPromise = import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts') + .then(({ default: Shortcuts }) => new Shortcuts()) + .catch(() => {}); + +export const addShortcutsExtension = (ShortcutExtension, ...args) => + shortcutsPromise.then((shortcuts) => shortcuts.addExtension(ShortcutExtension, args)); + +export const resetShortcutsForTests = async () => { + if (process.env.NODE_ENV === 'test') { + const { Mousetrap, clearStopCallbacksForTests } = await import('~/lib/mousetrap'); + clearStopCallbacksForTests(); + Mousetrap.reset(); + const shortcuts = await shortcutsPromise; + shortcuts.extensions.clear(); + } +}; diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index 941662635ea..15229689306 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -538,13 +538,10 @@ const GLOBAL_SHORTCUTS_GROUP = { GO_TO_YOUR_TODO_LIST, TOGGLE_PERFORMANCE_BAR, HIDE_APPEARING_CONTENT, + TOGGLE_SUPER_SIDEBAR, ], }; -if (gon.use_new_navigation) { - GLOBAL_SHORTCUTS_GROUP.keybindings.push(TOGGLE_SUPER_SIDEBAR); -} - export const EDITING_SHORTCUTS_GROUP = { id: 'editing', name: __('Editing'), diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 9514ad853b0..e05694c0907 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -63,11 +63,17 @@ function getToolbarBtnToShortcutsMap($textarea) { export default class Shortcuts { constructor() { + if (process.env.NODE_ENV !== 'production' && this.constructor !== Shortcuts) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Shortcuts cannot be subclassed.'); + } + + this.extensions = new Map(); this.onToggleHelp = this.onToggleHelp.bind(this); this.helpModalElement = null; this.helpModalVueInstance = null; - this.bindCommands([ + this.addAll([ [TOGGLE_KEYBOARD_SHORTCUTS_DIALOG, this.onToggleHelp], [START_SEARCH, Shortcuts.focusSearch], [FOCUS_FILTER_BAR, this.focusFilter.bind(this)], @@ -94,16 +100,12 @@ export default class Shortcuts { const findFileURL = document.body.dataset.findFile; if (typeof findFileURL !== 'undefined' && findFileURL !== null) { - this.bindCommand(GO_TO_PROJECT_FIND_FILE, () => { + this.add(GO_TO_PROJECT_FIND_FILE, () => { visitUrl(findFileURL); }); } - const shortcutsModalTriggerEvent = 'click.shortcutsModalTrigger'; - // eslint-disable-next-line @gitlab/no-global-event-off - $(document) - .off(shortcutsModalTriggerEvent) - .on(shortcutsModalTriggerEvent, '.js-shortcuts-modal-trigger', this.onToggleHelp); + $(document).on('click', '.js-shortcuts-modal-trigger', this.onToggleHelp); if (shouldDisableShortcuts()) { disableShortcuts(); @@ -111,6 +113,62 @@ export default class Shortcuts { } /** + * Instantiate a legacy shortcut extension class. + * + * NOTE: The preferred approach for adding shortcuts is described in + * https://docs.gitlab.com/ee/development/fe_guide/keyboard_shortcuts.html. + * This method is only for existing legacy shortcut classes. + * + * A shortcut extension class packages up several shortcuts and behaviors for + * a page or set of pages. They are considered legacy because they usually do + * not follow modern best practices. For instance, they may hook into the UI + * in brittle ways, e.g.. querySelectors. + * + * Extension classes can declare dependencies on other shortcut extension + * classes by listing them in a static `dependencies` property. This is + * essentially a reimplementation of the previous subclassing approach, but + * with idempotency: a shortcut extension class can now only be added at most + * one time. + * + * Extension classes are instantiated and given the Shortcuts singleton + * instance as their first argument. If the class constructor needs + * additional arguments, pass them via the second argument as an array. + * + * See https://gitlab.com/gitlab-org/gitlab/-/issues/392845 for more context. + * + * @param {Function} Extension The extension class to add/instantiate. + * @param {Array} [args] A list of additional args to pass to the extension + * class constructor. + * @param {Set} [extensionsCurrentlyLoading] For internal use only. Do not + * use. + * @returns The instantiated shortcut extension class. + */ + addExtension(Extension, args = [], extensionsCurrentlyLoading = new Set()) { + extensionsCurrentlyLoading.add(Extension); + + let instance = this.extensions.get(Extension); + if (!instance) { + for (const Dep of Extension.dependencies ?? []) { + if (extensionsCurrentlyLoading.has(Dep) || Dep === Shortcuts) { + // We've encountered a circular dependency, so stop recursing. + // eslint-disable-next-line no-continue + continue; + } + + extensionsCurrentlyLoading.add(Dep); + + this.addExtension(Dep, [], extensionsCurrentlyLoading); + } + + instance = new Extension(this, ...args); + this.extensions.set(Extension, instance); + } + + extensionsCurrentlyLoading.delete(Extension); + return instance; + } + + /** * Bind the keyboard shortcut(s) defined by the given command to the given * callback. * @@ -120,7 +178,7 @@ export default class Shortcuts { * @returns {void} */ // eslint-disable-next-line class-methods-use-this - bindCommand(command, callback) { + add(command, callback) { Mousetrap.bind(keysFor(command), callback); } @@ -132,8 +190,8 @@ export default class Shortcuts { * command/callback pairs. * @returns {void} */ - bindCommands(commandsAndCallbacks) { - commandsAndCallbacks.forEach((commandAndCallback) => this.bindCommand(...commandAndCallback)); + addAll(commandsAndCallbacks) { + commandsAndCallbacks.forEach((commandAndCallback) => this.add(...commandAndCallback)); } onToggleHelp(e) { @@ -198,11 +256,7 @@ export default class Shortcuts { } static focusSearch(e) { - if (gon.use_new_navigation) { - document.querySelector('#super-sidebar-search')?.click(); - } else { - document.querySelector('#search')?.focus(); - } + document.querySelector('#super-sidebar-search')?.click(); if (e.preventDefault) { e.preventDefault(); diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index 65ae67d156f..a0bfd337d10 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -7,7 +7,6 @@ import { getShaFromUrl, } from '~/lib/utils/url_utility'; import { updateRefPortionOfTitle } from '~/repository/utils/title'; -import Shortcuts from './shortcuts'; const defaults = { fileBlobPermalinkUrl: null, @@ -19,15 +18,14 @@ function eventHasModifierKeys(event) { return event.ctrlKey || event.metaKey || event.shiftKey; } -export default class ShortcutsBlob extends Shortcuts { - constructor(opts) { +export default class ShortcutsBlob { + constructor(shortcuts, opts) { const options = { ...defaults, ...opts }; - super(); this.options = options; this.shortcircuitPermalinkButton(); - this.bindCommand(PROJECT_FILES_GO_TO_PERMALINK, this.moveToFilePermalink.bind(this)); + shortcuts.add(PROJECT_FILES_GO_TO_PERMALINK, this.moveToFilePermalink.bind(this)); } moveToFilePermalink() { diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js index f26878cf161..393d0165a07 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js @@ -8,10 +8,8 @@ import { import { addStopCallback } from '~/lib/mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; -export default class ShortcutsFindFile extends ShortcutsNavigation { - constructor(projectFindFile) { - super(); - +export default class ShortcutsFindFile { + constructor(shortcuts, projectFindFile) { addStopCallback((e, element, combo) => { if ( element === projectFindFile.inputElement[0] && @@ -28,11 +26,13 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { return undefined; }); - this.bindCommands([ + shortcuts.addAll([ [PROJECT_FILES_MOVE_SELECTION_UP, projectFindFile.selectRowUp], [PROJECT_FILES_MOVE_SELECTION_DOWN, projectFindFile.selectRowDown], [PROJECT_FILES_GO_BACK, projectFindFile.goToTree], [PROJECT_FILES_OPEN_SELECTION, projectFindFile.goToBlob], ]); } + + static dependencies = [ShortcutsNavigation]; } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index b0e515ac19d..cde6d59b210 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -16,12 +16,9 @@ import { MR_COPY_SOURCE_BRANCH_NAME, ISSUABLE_COPY_REF, } from './keybindings'; -import Shortcuts from './shortcuts'; - -export default class ShortcutsIssuable extends Shortcuts { - constructor() { - super(); +export default class ShortcutsIssuable { + constructor(shortcuts) { this.branchInMemoryButton = document.createElement('button'); this.branchClipboardInstance = new ClipboardJS(this.branchInMemoryButton); this.branchClipboardInstance.on('success', () => { @@ -40,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts { toast(s__('GlobalShortcuts|Unable to copy the reference at this time.')); }); - this.bindCommands([ + shortcuts.addAll([ [ISSUE_MR_CHANGE_ASSIGNEE, () => ShortcutsIssuable.openSidebarDropdown('assignee')], [ISSUE_MR_CHANGE_MILESTONE, () => ShortcutsIssuable.openSidebarDropdown('milestone')], [ISSUABLE_CHANGE_LABEL, () => ShortcutsIssuable.openSidebarDropdown('labels')], diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js index 4691a4228e6..bae50c02599 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js @@ -21,13 +21,10 @@ import { PROJECT_FILES_GO_TO_COMPARE, NEW_ISSUE, } from './keybindings'; -import Shortcuts from './shortcuts'; -export default class ShortcutsNavigation extends Shortcuts { - constructor() { - super(); - - this.bindCommands([ +export default class ShortcutsNavigation { + constructor(shortcuts) { + shortcuts.addAll([ [GO_TO_PROJECT_OVERVIEW, () => findAndFollowLink('.shortcuts-project')], [GO_TO_PROJECT_ACTIVITY_FEED, () => findAndFollowLink('.shortcuts-project-activity')], [GO_TO_PROJECT_RELEASES, () => findAndFollowLink('.shortcuts-deployments-releases')], diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js index 02c6af53fc2..eee8c1acf1a 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js @@ -8,11 +8,9 @@ import { } from './keybindings'; import ShortcutsNavigation from './shortcuts_navigation'; -export default class ShortcutsNetwork extends ShortcutsNavigation { - constructor(graph) { - super(); - - this.bindCommands([ +export default class ShortcutsNetwork { + constructor(shortcuts, graph) { + shortcuts.addAll([ [REPO_GRAPH_SCROLL_LEFT, graph.scrollLeft], [REPO_GRAPH_SCROLL_RIGHT, graph.scrollRight], [REPO_GRAPH_SCROLL_UP, graph.scrollUp], @@ -21,4 +19,6 @@ export default class ShortcutsNetwork extends ShortcutsNavigation { [REPO_GRAPH_SCROLL_BOTTOM, graph.scrollBottom], ]); } + + static dependencies = [ShortcutsNavigation]; } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js index 62d612cfa6d..5f45331bf76 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js @@ -2,13 +2,13 @@ import findAndFollowLink from '~/lib/utils/navigation_utility'; import { EDIT_WIKI_PAGE } from './keybindings'; import ShortcutsNavigation from './shortcuts_navigation'; -export default class ShortcutsWiki extends ShortcutsNavigation { - constructor() { - super(); - - this.bindCommand(EDIT_WIKI_PAGE, ShortcutsWiki.editWiki); +export default class ShortcutsWiki { + constructor(shortcuts) { + shortcuts.add(EDIT_WIKI_PAGE, ShortcutsWiki.editWiki); } + static dependencies = [ShortcutsNavigation]; + static editWiki() { findAndFollowLink('.js-wiki-edit'); } |