diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
commit | 41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch) | |
tree | 9c8d89a8624828992f06d892cd2f43818ff5dcc8 /app/assets/javascripts/lib | |
parent | 0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff) | |
download | gitlab-ce-41fe97390ceddf945f3d967b8fdb3de4c66b7dea.tar.gz |
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r-- | app/assets/javascripts/lib/utils/accessor.js | 8 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/array_utility.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 13 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/ignore_while_pending.js | 26 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/rails_ujs.js | 5 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/resize_observer.js | 22 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 54 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 14 |
8 files changed, 125 insertions, 27 deletions
diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js index d4a6d70c62c..f7cdc564538 100644 --- a/app/assets/javascripts/lib/utils/accessor.js +++ b/app/assets/javascripts/lib/utils/accessor.js @@ -50,8 +50,16 @@ function canUseLocalStorage() { return safe; } +/** + * Determines if `window.crypto` is available. + */ +function canUseCrypto() { + return window.crypto?.subtle !== undefined; +} + const AccessorUtilities = { canUseLocalStorage, + canUseCrypto, }; export default AccessorUtilities; diff --git a/app/assets/javascripts/lib/utils/array_utility.js b/app/assets/javascripts/lib/utils/array_utility.js index 197e7790ed7..04f9cb1cdb5 100644 --- a/app/assets/javascripts/lib/utils/array_utility.js +++ b/app/assets/javascripts/lib/utils/array_utility.js @@ -18,3 +18,13 @@ export const swapArrayItems = (array, leftIndex = 0, rightIndex = 0) => { copy[rightIndex] = temp; return copy; }; + +/** + * Return an array with all duplicate items from the given array + * + * @param {Array} array - The source array + * @returns {Array} new array with all duplicate items + */ +export const getDuplicateItemsFromArray = (array) => [ + ...new Set(array.filter((value, index) => array.indexOf(value) !== index)), +]; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index cf6ce2c4889..96d019f62f2 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -130,19 +130,6 @@ export const isInViewport = (el, offset = {}) => { ); }; -export const parseUrl = (url) => { - const parser = document.createElement('a'); - parser.href = url; - return parser; -}; - -export const parseUrlPathname = (url) => { - const parsedUrl = parseUrl(url); - // parsedUrl.pathname will return an absolute path for Firefox and a relative path for IE11 - // We have to make sure we always have an absolute path. - return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`; -}; - export const isMetaKey = (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; // Identify following special clicks diff --git a/app/assets/javascripts/lib/utils/ignore_while_pending.js b/app/assets/javascripts/lib/utils/ignore_while_pending.js new file mode 100644 index 00000000000..e85a573c8f2 --- /dev/null +++ b/app/assets/javascripts/lib/utils/ignore_while_pending.js @@ -0,0 +1,26 @@ +/** + * This will wrap the given function to make sure that it is only triggered once + * while executing asynchronously + * + * @param {Function} fn some function that returns a promise + * @returns A function that will only be triggered *once* while the promise is executing + */ +export const ignoreWhilePending = (fn) => { + const isPendingMap = new WeakMap(); + const defaultContext = {}; + + // We need this to be a function so we get the `this` + return function ignoreWhilePendingInner(...args) { + const context = this || defaultContext; + + if (isPendingMap.get(context)) { + return Promise.resolve(); + } + + isPendingMap.set(context, true); + + return fn.apply(this, args).finally(() => { + isPendingMap.delete(context); + }); + }; +}; diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js index 6b1985a23ba..b4f425da871 100644 --- a/app/assets/javascripts/lib/utils/rails_ujs.js +++ b/app/assets/javascripts/lib/utils/rails_ujs.js @@ -1,5 +1,6 @@ import Rails from '@rails/ujs'; import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal'; +import { ignoreWhilePending } from './ignore_while_pending'; function monkeyPatchConfirmModal() { /** @@ -18,8 +19,10 @@ function monkeyPatchConfirmModal() { * @param element {HTMLElement} Element that was clicked on * @returns {boolean} */ + const safeConfirm = ignoreWhilePending(confirmViaGlModal); + function confirmViaModal(message, element) { - confirmViaGlModal(message, element) + safeConfirm(message, element) .then((confirmed) => { if (confirmed) { Rails.confirm = () => true; diff --git a/app/assets/javascripts/lib/utils/resize_observer.js b/app/assets/javascripts/lib/utils/resize_observer.js index e72c6fe1679..5d194340b9e 100644 --- a/app/assets/javascripts/lib/utils/resize_observer.js +++ b/app/assets/javascripts/lib/utils/resize_observer.js @@ -10,22 +10,30 @@ export function createResizeObserver() { }); } -// watches for change in size of a container element (e.g. for lazy-loaded images) -// and scroll the target element to the top of the content area -// stop watching after any user input. So if user opens sidebar or manually -// scrolls the page we don't hijack their scroll position +/** + * Watches for change in size of a container element (e.g. for lazy-loaded images) + * and scrolls the target note to the top of the content area. + * Stops watching after any user input. So if user opens sidebar or manually + * scrolls the page we don't hijack their scroll position + * + * @param {Object} options + * @param {string} options.targetId - id of element to scroll to + * @param {string} options.container - Selector of element containing target + * + * @return {ResizeObserver|null} - ResizeObserver instance if target looks like a note DOM ID + */ export function scrollToTargetOnResize({ - target = window.location.hash, + targetId = window.location.hash.slice(1), container = '#content-body', } = {}) { - if (!target) return null; + if (!targetId) return null; const ro = createResizeObserver(); const containerEl = document.querySelector(container); let interactionListenersAdded = false; function keepTargetAtTop() { - const anchorEl = document.querySelector(target); + const anchorEl = document.getElementById(targetId); if (!anchorEl) return; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index ec6789d81ec..ac2eb34260c 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -9,7 +9,7 @@ const LINK_TAG_PATTERN = '[{text}](url)'; // a bullet point character (*+-) and an optional checkbox ([ ] [x]) // OR a number with a . after it and an optional checkbox ([ ] [x]) // followed by one or more whitespace characters -const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isOl>[*+-])|(?<isUl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/; +const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl>\d+\.))( \[([x ])\])?\s)(?<content>.)?/; function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); @@ -31,8 +31,19 @@ function lineBefore(text, textarea, trimNewlines = true) { return split[split.length - 1]; } -function lineAfter(text, textarea) { - return text.substring(textarea.selectionEnd).trim().split('\n')[0]; +function lineAfter(text, textarea, trimNewlines = true) { + let split = text.substring(textarea.selectionEnd); + + if (trimNewlines) { + split = split.trim(); + } else { + // remove possible leading newline to get at the real line + split = split.replace(/^\n/, ''); + } + + split = split.split('\n'); + + return split[0]; } function convertMonacoSelectionToAceFormat(sel) { @@ -329,6 +340,25 @@ function handleSurroundSelectedText(e, textArea) { } /* eslint-enable @gitlab/require-i18n-strings */ +/** + * Returns the content for a new line following a list item. + * + * @param {Object} result - regex match of the current line + * @param {Object?} nextLineResult - regex match of the next line + * @returns string with the new list item + */ +function continueOlText(result, nextLineResult) { + const { indent, leader } = result.groups; + const { indent: nextIndent, isOl: nextIsOl } = nextLineResult?.groups ?? {}; + + const [numStr, postfix = ''] = leader.split('.'); + + const incrementBy = nextIsOl && nextIndent === indent ? 0 : 1; + const num = parseInt(numStr, 10) + incrementBy; + + return `${indent}${num}.${postfix}`; +} + function handleContinueList(e, textArea) { if (!gon.features?.markdownContinueLists) return; if (!(e.key === 'Enter')) return; @@ -339,7 +369,7 @@ function handleContinueList(e, textArea) { const result = currentLine.match(LIST_LINE_HEAD_PATTERN); if (result) { - const { indent, content, leader } = result.groups; + const { leader, indent, content, isOl } = result.groups; const prevLineEmpty = !content; if (prevLineEmpty) { @@ -349,12 +379,22 @@ function handleContinueList(e, textArea) { return; } - const itemInsert = `${indent}${leader}`; + let itemToInsert; + + if (isOl) { + const nextLine = lineAfter(textArea.value, textArea, false); + const nextLineResult = nextLine.match(LIST_LINE_HEAD_PATTERN); + + itemToInsert = continueOlText(result, nextLineResult); + } else { + // isUl + itemToInsert = `${indent}${leader}`; + } e.preventDefault(); updateText({ - tag: itemInsert, + tag: itemToInsert, textArea, blockTag: '', wrap: false, @@ -367,6 +407,8 @@ function handleContinueList(e, textArea) { export function keypressNoteText(e) { const textArea = this; + if ($(textArea).atwho?.('isSelecting')) return; + handleContinueList(e, textArea); handleSurroundSelectedText(e, textArea); } diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 12462a2575e..335cd6a16e5 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -18,6 +18,20 @@ function resetRegExp(regex) { return regex; } +/** + * Returns the absolute pathname for a relative or absolute URL string. + * + * A few examples of inputs and outputs: + * 1) 'http://a.com/b/c/d' => '/b/c/d' + * 2) '/b/c/d' => '/b/c/d' + * 3) 'b/c/d' => '/b/c/d' or '[path]/b/c/d' depending of the current path of the + * document.location + */ +export const parseUrlPathname = (url) => { + const { pathname } = new URL(url, document.location.href); + return pathname; +}; + // Returns a decoded url parameter value // - Treats '+' as '%20' function decodeUrlParameter(val) { |