diff options
Diffstat (limited to 'app/assets/javascripts/lib/utils')
11 files changed, 133 insertions, 19 deletions
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 96d019f62f2..1ed0cc3130b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -4,15 +4,15 @@ import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils'; import $ from 'jquery'; -import Cookies from 'js-cookie'; import { isFunction, defer } from 'lodash'; +import Cookies from '~/lib/utils/cookies'; import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import { convertToCamelCase, convertToSnakeCase } from './text_utility'; import { isObject } from './type_utility'; import { getLocationHash } from './url_utility'; export const getPagePath = (index = 0) => { - const { page = '' } = document?.body?.dataset; + const { page = '' } = document.body.dataset; return page.split(':')[index]; }; @@ -105,7 +105,7 @@ export const handleLocationHash = () => { } if (isInIssuePage()) { - adjustment -= fixedIssuableTitle?.offsetHeight; + adjustment -= fixedIssuableTitle.offsetHeight; } if (isInMRPage()) { @@ -157,7 +157,7 @@ export const contentTop = () => { () => getOuterHeight('#js-peek'), () => getOuterHeight('.navbar-gitlab'), ({ desktop }) => { - const container = document.querySelector('.line-resolve-all-container'); + const container = document.querySelector('.discussions-counter'); let size = 0; if (!desktop && container) { @@ -282,23 +282,51 @@ export const getSelectedFragment = (restrictToNode) => { return documentFragment; }; +function execInsertText(text) { + if (text === '') return document.execCommand('delete'); + + return document.execCommand('insertText', false, text); +} + +/** + * This method inserts text into a textarea/input field. + * Uses `execCommand` if supported + * + * @param {HTMLElement} target - textarea/input to have text inserted into + * @param {String | function} text - text to be inserted + */ export const insertText = (target, text) => { - // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas const { selectionStart, selectionEnd, value } = target; - const textBefore = value.substring(0, selectionStart); const textAfter = value.substring(selectionEnd, value.length); - const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; - const newText = textBefore + insertedText + textAfter; - // eslint-disable-next-line no-param-reassign - target.value = newText; - // eslint-disable-next-line no-param-reassign - target.selectionStart = selectionStart + insertedText.length; - - // eslint-disable-next-line no-param-reassign - target.selectionEnd = selectionStart + insertedText.length; + // The `execCommand` is officially deprecated. However, for `insertText`, + // there is currently no alternative. We need to use it in order to trigger + // the browser's undo tracking when we insert text. + // Per https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand on 2022-04-11, + // The Clipboard API can be used instead of execCommand in many cases, + // but execCommand is still sometimes useful. In particular, the Clipboard + // API doesn't replace the insertText command + // So we attempt to use it if possible. Otherwise, fall back to just replacing + // the value as before. In this case, Undo will be broken with inserted text. + // Testing on older versions of Firefox: + // 87 and below: does not work and falls through to just replacing value. + // 87 was released in Mar of 2021 + // 89 and above: works well + // 89 was released in May of 2021 + if (!execInsertText(insertedText)) { + const newText = textBefore + insertedText + textAfter; + + // eslint-disable-next-line no-param-reassign + target.value = newText; + + // eslint-disable-next-line no-param-reassign + target.selectionStart = selectionStart + insertedText.length; + + // eslint-disable-next-line no-param-reassign + target.selectionEnd = selectionStart + insertedText.length; + } // Trigger autosave target.dispatchEvent(new Event('input')); diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue index 1d8eb73d3d7..3788d8ab20c 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -3,7 +3,6 @@ import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui'; import { __ } from '~/locale'; export default { - cancelAction: { text: __('Cancel') }, directives: { SafeHtml: GlSafeHtmlDirective, }, @@ -36,6 +35,16 @@ export default { required: false, default: 'confirm', }, + cancelText: { + type: String, + required: false, + default: __('Cancel'), + }, + cancelVariant: { + type: String, + required: false, + default: 'default', + }, modalHtmlMessage: { type: String, required: false, @@ -71,7 +80,14 @@ export default { }; }, cancelAction() { - return this.hideCancel ? null : this.$options.cancelAction; + return this.hideCancel + ? null + : { + text: this.cancelText, + attributes: { + variant: this.cancelVariant, + }, + }; }, shouldShowHeader() { return Boolean(this.title?.length); diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js index 1adb6f9c26f..173116062c9 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js @@ -7,6 +7,8 @@ export function confirmAction( primaryBtnText, secondaryBtnVariant, secondaryBtnText, + cancelBtnVariant, + cancelBtnText, modalHtmlMessage, title, hideCancel, @@ -28,6 +30,8 @@ export function confirmAction( secondaryVariant: secondaryBtnVariant, primaryVariant: primaryBtnVariant, primaryText: primaryBtnText, + cancelVariant: cancelBtnVariant, + cancelText: cancelBtnText, title, modalHtmlMessage, hideCancel, diff --git a/app/assets/javascripts/lib/utils/cookies.js b/app/assets/javascripts/lib/utils/cookies.js new file mode 100644 index 00000000000..be0491376c9 --- /dev/null +++ b/app/assets/javascripts/lib/utils/cookies.js @@ -0,0 +1,8 @@ +import CookiesBuilder from 'js-cookie'; + +// set default path for cookies +const Cookies = CookiesBuilder.withAttributes({ + path: gon.relative_url_root || '/', +}); + +export default Cookies; diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js index 095a29a2eff..05f34db662a 100644 --- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js @@ -7,7 +7,7 @@ import { formatDate } from './date_format_utility'; * * see https://github.com/hustcc/timeago.js/tree/v3.0.0/locales */ -const timeagoLanguageCode = languageCode().replace(/-/g, '_'); +export const timeagoLanguageCode = languageCode().replace(/-/g, '_'); /** * Registers timeago locales diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js index b52a736f153..4262329aae7 100644 --- a/app/assets/javascripts/lib/utils/dom_utils.js +++ b/app/assets/javascripts/lib/utils/dom_utils.js @@ -90,6 +90,20 @@ export const getParents = (element) => { return parents; }; +export const getParentByTagName = (element, tagName) => { + let parent = element.parentNode; + + do { + if (parent.nodeName?.toLowerCase() === tagName?.toLowerCase()) { + return parent; + } + + parent = parent.parentElement; + } while (parent); + + return undefined; +}; + /** * This method takes a HTML element and an object of attributes * to save repeated calls to `setAttribute` when multiple diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 6b9be34235b..c5190592bb6 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -22,6 +22,7 @@ const httpStatusCodes = { METHOD_NOT_ALLOWED: 405, CONFLICT: 409, GONE: 410, + PAYLOAD_TOO_LARGE: 413, UNPROCESSABLE_ENTITY: 422, TOO_MANY_REQUESTS: 429, INTERNAL_SERVER_ERROR: 500, diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 52fa90c7791..243de48948c 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -14,6 +14,8 @@ const LIST_LINE_HEAD_PATTERN = /^(?<indent>\s*)(?<leader>((?<isUl>[*+-])|(?<isOl // detect a horizontal rule that might be mistaken for a list item (not full pattern for an <hr>) const HR_PATTERN = /^((\s{0,3}-+\s*-+\s*-+\s*[\s-]*)|(\s{0,3}\*+\s*\*+\s*\*+\s*[\s*]*))$/; +let compositioningNoteText = false; + function selectedText(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); } @@ -363,10 +365,11 @@ function continueOlText(result, nextLineResult) { } function handleContinueList(e, textArea) { - if (!gon.features?.markdownContinueLists) return; if (!(e.key === 'Enter')) return; if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; if (textArea.selectionStart !== textArea.selectionEnd) return; + // prevent unintended line breaks were inserted using Japanese IME on MacOS + if (compositioningNoteText) return; const currentLine = lineBefore(textArea.value, textArea, false); const result = currentLine.match(LIST_LINE_HEAD_PATTERN); @@ -420,6 +423,14 @@ export function keypressNoteText(e) { handleSurroundSelectedText(e, textArea); } +export function compositionStartNoteText() { + compositioningNoteText = true; +} + +export function compositionEndNoteText() { + compositioningNoteText = false; +} + export function updateTextForToolbarBtn($toolbarBtn) { return updateText({ textArea: $toolbarBtn.closest('.md-area').find('textarea'), @@ -435,6 +446,8 @@ export function updateTextForToolbarBtn($toolbarBtn) { export function addMarkdownListeners(form) { $('.markdown-area', form) .on('keydown', keypressNoteText) + .on('compositionstart', compositionStartNoteText) + .on('compositionend', compositionEndNoteText) .each(function attachTextareaShortcutHandlers() { Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn); }); @@ -474,6 +487,8 @@ export function addEditorMarkdownListeners(editor) { export function removeMarkdownListeners(form) { $('.markdown-area', form) .off('keydown', keypressNoteText) + .off('compositionstart', compositionStartNoteText) + .off('compositionend', compositionEndNoteText) .each(function removeTextareaShortcutHandlers() { Shortcuts.removeMarkdownEditorShortcuts($(this)); }); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 419afa0a0a9..dad9cbcb6f6 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -6,6 +6,10 @@ import { } from '~/lib/utils/constants'; import { allSingleQuotes } from '~/lib/utils/regexp'; +export const COLON = ':'; +export const HYPHEN = '-'; +export const NEWLINE = '\n'; + /** * Adds a , to a string composed by numbers, at every 3 chars. * diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 335cd6a16e5..ff60fd2aecb 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -420,6 +420,19 @@ export function isSafeURL(url) { } /** + * Returns the sanitized url when not safe + * + * @param {String} url + * @returns {String} + */ +export function sanitizeUrl(url) { + if (!isSafeURL(url)) { + return 'about:blank'; + } + return url; +} + +/** * Returns a normalized url * * https://gitlab.com/foo/../baz => https://gitlab.com/baz diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js index 54f69ef8e1b..bd000bb26fe 100644 --- a/app/assets/javascripts/lib/utils/users_cache.js +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -35,6 +35,17 @@ class UsersCache extends Cache { // missing catch is intentional, error handling depends on use case } + updateById(userId, data) { + if (!this.hasData(userId)) { + return; + } + + this.internalStorage[userId] = { + ...this.internalStorage[userId], + ...data, + }; + } + retrieveStatusById(userId) { if (this.hasData(userId) && this.get(userId).status) { return Promise.resolve(this.get(userId).status); |