summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r--app/assets/javascripts/lib/utils/accessor.js8
-rw-r--r--app/assets/javascripts/lib/utils/array_utility.js10
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js13
-rw-r--r--app/assets/javascripts/lib/utils/ignore_while_pending.js26
-rw-r--r--app/assets/javascripts/lib/utils/rails_ujs.js5
-rw-r--r--app/assets/javascripts/lib/utils/resize_observer.js22
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js54
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js14
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) {