diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/behaviors | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) | |
download | gitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/behaviors')
4 files changed, 137 insertions, 137 deletions
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index a31bcc2cb41..de248340738 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -1,31 +1,27 @@ import Clipboard from 'clipboard'; import $ from 'jquery'; import { sprintf, __ } from '~/locale'; -import { fixTitle, show } from '~/tooltips'; +import { fixTitle, add, show, once } from '~/tooltips'; function showTooltip(target, title) { - const { originalTitle } = target.dataset; - const hideTooltip = () => { - target.removeEventListener('mouseout', hideTooltip); - setTimeout(() => { + const { title: originalTitle } = target.dataset; + + once('hidden', (tooltip) => { + if (tooltip.target === target) { target.setAttribute('title', originalTitle); fixTitle(target); - }, 100); - }; + } + }); target.setAttribute('title', title); - fixTitle(target); show(target); - - target.addEventListener('mouseout', hideTooltip); + setTimeout(() => target.blur(), 1000); } function genericSuccess(e) { // Clear the selection and blur the trigger so it loses its border e.clearSelection(); - $(e.trigger).blur(); - showTooltip(e.trigger, __('Copied')); } @@ -88,24 +84,8 @@ export default function initCopyToClipboard() { * @param {HTMLElement} btnElement */ export function clickCopyToClipboardButton(btnElement) { - const $btnElement = $(btnElement); - // Ensure the button has already been tooltip'd. - // If the use hasn't yet interacted (i.e. hovered or clicked) - // with the button, Bootstrap hasn't yet initialized - // the tooltip, and its `data-original-title` will be `undefined`. - // This value is used in the functions above. - $btnElement.tooltip(); - btnElement.dispatchEvent(new MouseEvent('mouseover')); + add([btnElement], { show: true }); btnElement.click(); - - // Manually trigger the necessary events to hide the - // button's tooltip and allow the button to perform its - // tooltip cleanup (updating the title from "Copied" back - // to its original title, "Copy branch name"). - setTimeout(() => { - btnElement.dispatchEvent(new MouseEvent('mouseout')); - $btnElement.tooltip('hide'); - }, 2000); } diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 5479866c99a..8238f5523f3 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,6 +1,6 @@ -import { deprecatedCreateFlash as flash } from '~/flash'; +import { spriteIcon } from '~/lib/utils/common_utils'; import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; -import { s__, sprintf } from '~/locale'; +import { s__ } from '~/locale'; // Renders math using KaTeX in any element with the // `js-render-math` class @@ -13,30 +13,10 @@ import { s__, sprintf } from '~/locale'; const MAX_MATH_CHARS = 1000; const MAX_RENDER_TIME_MS = 2000; -// These messages might be used with inline errors in the future. Keep them around. For now, we will -// display a single error message using flash(). - -// const CHAR_LIMIT_EXCEEDED_MSG = sprintf( -// s__( -// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.', -// ), -// { maxChars: MAX_MATH_CHARS }, -// ); -// const RENDER_TIME_EXCEEDED_MSG = s__( -// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.", -// ); - -const RENDER_FLASH_MSG = sprintf( - s__( - 'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.', - ), - { maxChars: MAX_MATH_CHARS }, -); - // Wait for the browser to reflow the layout. Reflowing SVG takes time. // This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object". const waitForReflow = (fn) => { - window.requestAnimationFrame(fn); + window.requestIdleCallback(fn); }; /** @@ -67,37 +47,69 @@ class SafeMathRenderer { this.renderElement = this.renderElement.bind(this); this.render = this.render.bind(this); + this.attachEvents = this.attachEvents.bind(this); } - renderElement() { - if (!this.queue.length) { + renderElement(chosenEl) { + if (!this.queue.length && !chosenEl) { return; } - const el = this.queue.shift(); + const el = chosenEl || this.queue.shift(); + const forceRender = Boolean(chosenEl); const text = el.textContent; el.removeAttribute('style'); - - if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) { - if (!this.flashShown) { - flash(RENDER_FLASH_MSG); - this.flashShown = true; - } - + if (!forceRender && (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS)) { // Show unrendered math code + const wrapperElement = document.createElement('div'); const codeElement = document.createElement('pre'); + codeElement.className = 'code'; codeElement.textContent = el.textContent; - el.parentNode.replaceChild(codeElement, el); + + const { parentNode } = el; + parentNode.replaceChild(wrapperElement, el); + + const html = ` + <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-math-container js-lazy-render-math-container fade show" role="alert"> + ${spriteIcon('warning', 'text-warning-600 s16 gl-alert-icon')} + <div class="display-flex gl-alert-content"> + <div>${s__( + 'math|Displaying this math block may cause performance issues on this page', + )}</div> + <div class="gl-alert-actions"> + <button class="js-lazy-render-math btn gl-alert-action btn-primary btn-md gl-button">Display anyway</button> + </div> + </div> + <button type="button" class="close" data-dismiss="alert" aria-label="Close"> + ${spriteIcon('close', 's16')} + </button> + </div> + `; + + if (!wrapperElement.classList.contains('lazy-alert-shown')) { + wrapperElement.innerHTML = html; + wrapperElement.append(codeElement); + wrapperElement.classList.add('lazy-alert-shown'); + } // Render the next math this.renderElement(); } else { this.startTime = Date.now(); + /* Get the correct reference to the display container when: + * a.) Happy path: when the math block is present, and + * b.) When we've replace the block with <pre> for lazy rendering + */ + let displayContainer = el; + if (el.tagName === 'PRE') { + displayContainer = el.parentElement; + } + try { - el.innerHTML = this.katex.renderToString(text, { + displayContainer.innerHTML = this.katex.renderToString(text, { displayMode: el.getAttribute('data-math-style') === 'display', throwOnError: true, maxSize: 20, @@ -135,6 +147,22 @@ class SafeMathRenderer { // and less prone to timeouts. setTimeout(this.renderElement, 400); } + + attachEvents() { + document.body.addEventListener('click', (event) => { + if (!event.target.classList.contains('js-lazy-render-math')) { + return; + } + + const parent = event.target.closest('.js-lazy-render-math-container'); + + const pre = parent.nextElementSibling; + + parent.remove(); + + this.renderElement(pre); + }); + } } export default function renderMath($els) { @@ -146,6 +174,7 @@ export default function renderMath($els) { .then(([katex]) => { const renderer = new SafeMathRenderer($els.get(), katex); renderer.render(); + renderer.attachEvents(); }) .catch(() => {}); } diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js index 0513e807ed6..a8fe00d26e6 100644 --- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js +++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js @@ -1,91 +1,80 @@ -import { flatten } from 'lodash'; +import { memoize } from 'lodash'; import AccessorUtilities from '~/lib/utils/accessor'; import { s__ } from '~/locale'; -import { shouldDisableShortcuts } from './shortcuts_toggle'; -export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations'; - -let parsedCustomizations = {}; -const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe(); +const isCustomizable = (command) => + 'customizable' in command ? Boolean(command.customizable) : true; -if (localStorageIsSafe) { - try { - parsedCustomizations = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '{}'); - } catch (e) { - /* do nothing */ - } -} +export const LOCAL_STORAGE_KEY = 'gl-keyboard-shortcuts-customizations'; /** - * A map of command => keys of all keyboard shortcuts - * that have been customized by the user. + * @returns { Object.<string, string[]> } A map of command ID => keys of all + * keyboard shortcuts that have been customized by the user. These + * customizations are fetched from `localStorage`. This function is memoized, + * so its return value will not reflect changes made to the `localStorage` data + * after it has been called once. * * @example * { "globalShortcuts.togglePerformanceBar": ["p e r f"] } - * - * @type { Object.<string, string[]> } */ -export const customizations = parsedCustomizations; +export const getCustomizations = memoize(() => { + let parsedCustomizations = {}; + const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe(); + + if (localStorageIsSafe) { + try { + parsedCustomizations = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || '{}'); + } catch (e) { + /* do nothing */ + } + } + + return parsedCustomizations; +}); // All available commands -export const TOGGLE_PERFORMANCE_BAR = 'globalShortcuts.togglePerformanceBar'; -export const TOGGLE_CANARY = 'globalShortcuts.toggleCanary'; +export const TOGGLE_PERFORMANCE_BAR = { + id: 'globalShortcuts.togglePerformanceBar', + description: s__('KeyboardShortcuts|Toggle the Performance Bar'), + // eslint-disable-next-line @gitlab/require-i18n-strings + defaultKeys: ['p b'], +}; -/** All keybindings, grouped and ordered with descriptions */ -export const keybindingGroups = [ - { - groupId: 'globalShortcuts', - name: s__('KeyboardShortcuts|Global Shortcuts'), - keybindings: [ - { - description: s__('KeyboardShortcuts|Toggle the Performance Bar'), - command: TOGGLE_PERFORMANCE_BAR, - // eslint-disable-next-line @gitlab/require-i18n-strings - defaultKeys: ['p b'], - }, - { - description: s__('KeyboardShortcuts|Toggle GitLab Next'), - command: TOGGLE_CANARY, - // eslint-disable-next-line @gitlab/require-i18n-strings - defaultKeys: ['g x'], - }, - ], - }, -] +export const TOGGLE_CANARY = { + id: 'globalShortcuts.toggleCanary', + description: s__('KeyboardShortcuts|Toggle GitLab Next'), + // eslint-disable-next-line @gitlab/require-i18n-strings + defaultKeys: ['g x'], +}; - // For each keybinding object, add a `customKeys` property populated with the - // user's custom keybindings (if the command has been customized). - // `customKeys` will be `undefined` if the command hasn't been customized. - .map((group) => { - return { - ...group, - keybindings: group.keybindings.map((binding) => ({ - ...binding, - customKeys: customizations[binding.command], - })), - }; - }); +export const WEB_IDE_COMMIT = { + id: 'webIDE.commit', + description: s__('KeyboardShortcuts|Commit (when editing commit message)'), + defaultKeys: ['mod+enter'], + customizable: false, +}; -/** - * A simple map of command => keys. All user customizations are included in this map. - * This mapping is used to simplify `keysFor` below. - * - * @example - * { "globalShortcuts.togglePerformanceBar": ["p e r f"] } - */ -const commandToKeys = flatten(keybindingGroups.map((group) => group.keybindings)).reduce( - (acc, binding) => { - acc[binding.command] = binding.customKeys || binding.defaultKeys; - return acc; - }, - {}, -); +// All keybinding groups +export const GLOBAL_SHORTCUTS_GROUP = { + id: 'globalShortcuts', + name: s__('KeyboardShortcuts|Global Shortcuts'), + keybindings: [TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY], +}; + +export const WEB_IDE_GROUP = { + id: 'webIDE', + name: s__('KeyboardShortcuts|Web IDE'), + keybindings: [WEB_IDE_COMMIT], +}; + +/** All keybindings, grouped and ordered with descriptions */ +export const keybindingGroups = [GLOBAL_SHORTCUTS_GROUP, WEB_IDE_GROUP]; /** * Gets keyboard shortcuts associated with a command * - * @param {string} command The command string. All command - * strings are available as imports from this file. + * @param {string} command The command object. All command + * objects are available as imports from this file. * * @returns {string[]} An array of keyboard shortcut strings bound to the command * @@ -95,9 +84,11 @@ const commandToKeys = flatten(keybindingGroups.map((group) => group.keybindings) * Mousetrap.bind(keysFor(TOGGLE_PERFORMANCE_BAR), handler); */ export const keysFor = (command) => { - if (shouldDisableShortcuts()) { - return []; + if (!isCustomizable(command)) { + // if the command is defined with `customizable: false`, + // don't allow this command to be customized. + return command.defaultKeys; } - return commandToKeys[command]; + return getCustomizations()[command.id] || command.defaultKeys; }; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 4b63143c4ba..30424fee46a 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -30,7 +30,7 @@ $(() => { } $('body').on('click', '.js-toggle-button', function toggleButton(e) { - e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open'); + e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'selected'); toggleContainer($(this).closest('.js-toggle-container')); const targetTag = e.currentTarget.tagName.toLowerCase(); |