diff options
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r-- | app/assets/javascripts/lib/utils/axios_startup_calls.js | 52 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/axios_utils.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 97 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/constants.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/datetime_utility.js | 7 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/dom_utils.js | 22 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/grammar.js | 18 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 43 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_utility.js | 81 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 13 |
10 files changed, 225 insertions, 113 deletions
diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js new file mode 100644 index 00000000000..cb2e8a76c08 --- /dev/null +++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js @@ -0,0 +1,52 @@ +import { isEmpty } from 'lodash'; +import { mergeUrlParams } from './url_utility'; + +// We should probably not couple this utility to `gon.gitlab_url` +// Also, this would replace occurrences that aren't at the beginning of the string +const removeGitLabUrl = url => url.replace(gon.gitlab_url, ''); + +const getFullUrl = req => { + const url = removeGitLabUrl(req.url); + return mergeUrlParams(req.params || {}, url); +}; + +const setupAxiosStartupCalls = axios => { + const { startup_calls: startupCalls } = window.gl || {}; + + if (!startupCalls || isEmpty(startupCalls)) { + return; + } + + // TODO: To save performance of future axios calls, we can + // remove this interceptor once the "startupCalls" have been loaded + axios.interceptors.request.use(req => { + const fullUrl = getFullUrl(req); + + const existing = startupCalls[fullUrl]; + + if (existing) { + // eslint-disable-next-line no-param-reassign + req.adapter = () => + existing.fetchCall.then(res => { + const fetchHeaders = {}; + res.headers.forEach((val, key) => { + fetchHeaders[key] = val; + }); + + // eslint-disable-next-line promise/no-nesting + return res.json().then(data => ({ + data, + status: res.status, + statusText: res.statusText, + headers: fetchHeaders, + config: req, + request: req, + })); + }); + } + + return req; + }); +}; + +export default setupAxiosStartupCalls; diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 4eec5bffc66..9d517f45caa 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -1,6 +1,7 @@ import axios from 'axios'; import csrf from './csrf'; import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation'; +import setupAxiosStartupCalls from './axios_startup_calls'; axios.defaults.headers.common[csrf.headerKey] = csrf.token; // Used by Rails to check if it is a valid XHR request @@ -14,6 +15,8 @@ axios.interceptors.request.use(config => { return config; }); +setupAxiosStartupCalls(axios); + // Remove the global counter axios.interceptors.response.use( response => { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index a60748215ab..8bf9a281151 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -53,16 +53,6 @@ export const getCspNonceValue = () => { return metaTag && metaTag.content; }; -export const ajaxGet = url => - axios - .get(url, { - params: { format: 'js' }, - responseType: 'text', - }) - .then(({ data }) => { - $.globalEval(data, { nonce: getCspNonceValue() }); - }); - export const rstrip = val => { if (val) { return val.replace(/\s+$/, ''); @@ -105,6 +95,7 @@ export const handleLocationHash = () => { const topPadding = 8; const diffFileHeader = document.querySelector('.js-file-title'); const versionMenusContainer = document.querySelector('.mr-version-menus-container'); + const fixedIssuableTitle = document.querySelector('.issue-sticky-header'); let adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -133,6 +124,10 @@ export const handleLocationHash = () => { adjustment -= versionMenusContainer.offsetHeight; } + if (isInIssuePage()) { + adjustment -= fixedIssuableTitle.offsetHeight; + } + if (isInMRPage()) { adjustment -= topPadding; } @@ -370,34 +365,6 @@ export const insertText = (target, text) => { target.dispatchEvent(event); }; -export const nodeMatchesSelector = (node, selector) => { - const matches = - Element.prototype.matches || - Element.prototype.matchesSelector || - Element.prototype.mozMatchesSelector || - Element.prototype.msMatchesSelector || - Element.prototype.oMatchesSelector || - Element.prototype.webkitMatchesSelector; - - if (matches) { - return matches.call(node, selector); - } - - // IE11 doesn't support `node.matches(selector)` - - let { parentNode } = node; - - if (!parentNode) { - parentNode = document.createElement('div'); - // eslint-disable-next-line no-param-reassign - node = node.cloneNode(true); - parentNode.appendChild(node); - } - - const matchingNodes = parentNode.querySelectorAll(selector); - return Array.prototype.indexOf.call(matchingNodes, node) !== -1; -}; - /** this will take in the headers from an API response and normalize them this way we don't run into production issues when nginx gives us lowercased header keys @@ -413,24 +380,6 @@ export const normalizeHeaders = headers => { }; /** - this will take in the getAllResponseHeaders result and normalize them - this way we don't run into production issues when nginx gives us lowercased header keys -*/ -export const normalizeCRLFHeaders = headers => { - const headersObject = {}; - const headersArray = headers.split('\n'); - - headersArray.forEach(header => { - const keyValue = header.split(': '); - - // eslint-disable-next-line prefer-destructuring - headersObject[keyValue[0]] = keyValue[1]; - }); - - return normalizeHeaders(headersObject); -}; - -/** * Parses pagination object string values into numbers. * * @param {Object} paginationInformation @@ -638,13 +587,6 @@ export const setFaviconOverlay = overlayPath => { ); }; -export const setFavicon = faviconPath => { - const faviconEl = document.getElementById('favicon'); - if (faviconEl && faviconPath) { - faviconEl.setAttribute('href', faviconPath); - } -}; - export const resetFavicon = () => { const faviconEl = document.getElementById('favicon'); @@ -883,35 +825,6 @@ export const searchBy = (query = '', searchSpace = {}) => { */ export const isScopedLabel = ({ title = '' }) => title.indexOf('::') !== -1; -window.gl = window.gl || {}; -window.gl.utils = { - ...(window.gl.utils || {}), - getPagePath, - isInGroupsPage, - isInProjectPage, - getProjectSlug, - getGroupSlug, - isInIssuePage, - ajaxGet, - rstrip, - updateTooltipTitle, - disableButtonIfEmptyField, - handleLocationHash, - isInViewport, - parseUrl, - parseUrlPathname, - getUrlParamsArray, - isMetaKey, - isMetaClick, - scrollToElement, - getParameterByName, - getSelectedFragment, - insertText, - nodeMatchesSelector, - spriteIcon, - imagePath, -}; - // Methods to set and get Cookie export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 }); diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index eb6c9bf7eb6..993d51370ec 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -1,5 +1,7 @@ export const BYTES_IN_KIB = 1024; export const HIDDEN_CLASS = 'hidden'; +export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80; +export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12; export const DATETIME_RANGE_TYPES = { fixed: 'fixed', diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 6b69d2febe0..6e02fc1eb91 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -89,13 +89,15 @@ export const getDayName = date => * @example * dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000" * @param {date} datetime + * @param {String} format + * @param {Boolean} UTC convert local time to UTC * @returns {String} */ -export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => { +export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z', utc = false) => { if (isString(datetime) && datetime.match(/\d+-\d+\d+ /)) { throw new Error(__('Invalid date')); } - return dateFormat(datetime, format); + return dateFormat(datetime, format, utc); }; /** @@ -425,7 +427,6 @@ export const dayInQuarter = (date, quarter) => { window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), - getTimeago, localTimeAgo, }; diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js index 8fa235f8afb..d9b0e8c4476 100644 --- a/app/assets/javascripts/lib/utils/dom_utils.js +++ b/app/assets/javascripts/lib/utils/dom_utils.js @@ -1,3 +1,4 @@ +import { has } from 'lodash'; import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils'; export const addClassIfElementExists = (element, className) => { @@ -25,3 +26,24 @@ export const toggleContainerClasses = (containerEl, classList) => { }); } }; + +/** + * Return a object mapping element dataset names to booleans. + * + * This is useful for data- attributes whose presense represent + * a truthiness, no matter the value of the attribute. The absense of the + * attribute represents falsiness. + * + * This can be useful when Rails-provided boolean-like values are passed + * directly to the HAML template, rather than cast to a string. + * + * @param {HTMLElement} element - The DOM element to inspect + * @param {string[]} names - The dataset (i.e., camelCase) names to inspect + * @returns {Object.<string, boolean>} + */ +export const parseBooleanDataAttributes = ({ dataset }, names) => + names.reduce((acc, name) => { + acc[name] = has(dataset, name); + + return acc; + }, {}); diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js index 18f9e2ed846..b1f38429369 100644 --- a/app/assets/javascripts/lib/utils/grammar.js +++ b/app/assets/javascripts/lib/utils/grammar.js @@ -20,18 +20,22 @@ export const toNounSeriesText = items => { if (items.length === 0) { return ''; } else if (items.length === 1) { - return items[0]; + return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false); } else if (items.length === 2) { - return sprintf(s__('nounSeries|%{firstItem} and %{lastItem}'), { - firstItem: items[0], - lastItem: items[1], - }); + return sprintf( + s__('nounSeries|%{firstItem} and %{lastItem}'), + { + firstItem: items[0], + lastItem: items[1], + }, + false, + ); } return items.reduce((item, nextItem, idx) => idx === items.length - 1 - ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }) - : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }), + ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }, false) + : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }, false), ); }; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 0dfc144c363..4d25ee9e4bd 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -27,9 +27,28 @@ function lineAfter(text, textarea) { .split('\n')[0]; } +function convertMonacoSelectionToAceFormat(sel) { + return { + start: { + row: sel.startLineNumber, + column: sel.startColumn, + }, + end: { + row: sel.endLineNumber, + column: sel.endColumn, + }, + }; +} + +function getEditorSelectionRange(editor) { + return window.gon.features?.monacoBlobs + ? convertMonacoSelectionToAceFormat(editor.getSelection()) + : editor.getSelectionRange(); +} + function editorBlockTagText(text, blockTag, selected, editor) { const lines = text.split('\n'); - const selectionRange = editor.getSelectionRange(); + const selectionRange = getEditorSelectionRange(editor); const shouldRemoveBlock = lines[selectionRange.start.row - 1] === blockTag && lines[selectionRange.end.row + 1] === blockTag; @@ -90,8 +109,12 @@ function moveCursor({ const endPosition = startPosition + select.length; return textArea.setSelectionRange(startPosition, endPosition); } else if (editor) { - editor.navigateLeft(tag.length - tag.indexOf(select)); - editor.getSelection().selectAWord(); + if (window.gon.features?.monacoBlobs) { + editor.selectWithinSelection(select, tag); + } else { + editor.navigateLeft(tag.length - tag.indexOf(select)); + editor.getSelection().selectAWord(); + } return; } } @@ -115,7 +138,11 @@ function moveCursor({ } } else if (editor && editorSelectionStart.row === editorSelectionEnd.row) { if (positionBetweenTags) { - editor.navigateLeft(tag.length); + if (window.gon.features?.monacoBlobs) { + editor.moveCursor(tag.length * -1); + } else { + editor.navigateLeft(tag.length); + } } } } @@ -140,7 +167,7 @@ export function insertMarkdownText({ let textToInsert; if (editor) { - const selectionRange = editor.getSelectionRange(); + const selectionRange = getEditorSelectionRange(editor); editorSelectionStart = selectionRange.start; editorSelectionEnd = selectionRange.end; @@ -237,7 +264,11 @@ export function insertMarkdownText({ } if (editor) { - editor.insert(textToInsert); + if (window.gon.features?.monacoBlobs) { + editor.replaceSelectedText(textToInsert, select); + } else { + editor.insert(textToInsert); + } } else { insertText(textArea, textToInsert); } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index be3fe1ed620..e2953ce330c 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,4 +1,9 @@ -import { isString } from 'lodash'; +import { isString, memoize } from 'lodash'; + +import { + TRUNCATE_WIDTH_DEFAULT_WIDTH, + TRUNCATE_WIDTH_DEFAULT_FONT_SIZE, +} from '~/lib/utils/constants'; /** * Adds a , to a string composed by numbers, at every 3 chars. @@ -73,7 +78,79 @@ export const slugifyWithUnderscore = str => slugify(str, '_'); * @param {Number} maxLength * @returns {String} */ -export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`; +export const truncate = (string, maxLength) => { + if (string.length - 1 > maxLength) { + return `${string.substr(0, maxLength - 1)}…`; + } + + return string; +}; + +/** + * This function calculates the average char width. It does so by placing a string in the DOM and measuring the width. + * NOTE: This will cause a reflow and should be used sparsely! + * The default fontFamily is 'sans-serif' and 12px in ECharts, so that is the default basis for calculating the average with. + * https://echarts.apache.org/en/option.html#xAxis.nameTextStyle.fontFamily + * https://echarts.apache.org/en/option.html#xAxis.nameTextStyle.fontSize + * @param {Object} options + * @param {Number} options.fontSize style to size the text for measurement + * @param {String} options.fontFamily style of font family to measure the text with + * @param {String} options.chars string of chars to use as a basis for calculating average width + * @return {Number} + */ +const getAverageCharWidth = memoize(function getAverageCharWidth(options = {}) { + const { + fontSize = 12, + fontFamily = 'sans-serif', + // eslint-disable-next-line @gitlab/require-i18n-strings + chars = ' ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + } = options; + const div = document.createElement('div'); + + div.style.fontFamily = fontFamily; + div.style.fontSize = `${fontSize}px`; + // Place outside of view + div.style.position = 'absolute'; + div.style.left = -1000; + div.style.top = -1000; + + div.innerHTML = chars; + + document.body.appendChild(div); + const width = div.clientWidth; + document.body.removeChild(div); + + return width / chars.length / fontSize; +}); + +/** + * This function returns a truncated version of `string` if its estimated rendered width is longer than `maxWidth`, + * otherwise it will return the original `string` + * Inspired by https://bl.ocks.org/tophtucker/62f93a4658387bb61e4510c37e2e97cf + * @param {String} string text to truncate + * @param {Object} options + * @param {Number} options.maxWidth largest rendered width the text may have + * @param {Number} options.fontSize size of the font used to render the text + * @return {String} either the original string or a truncated version + */ +export const truncateWidth = (string, options = {}) => { + const { + maxWidth = TRUNCATE_WIDTH_DEFAULT_WIDTH, + fontSize = TRUNCATE_WIDTH_DEFAULT_FONT_SIZE, + } = options; + const { truncateIndex } = string.split('').reduce( + (memo, char, index) => { + let newIndex = index; + if (memo.width > maxWidth) { + newIndex = memo.truncateIndex; + } + return { width: memo.width + getAverageCharWidth() * fontSize, truncateIndex: newIndex }; + }, + { width: 0, truncateIndex: 0 }, + ); + + return truncate(string, truncateIndex); +}; /** * Truncate SHA to 8 characters diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 0472b8cf51f..c6c34b831ee 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -344,9 +344,15 @@ export function objectToQuery(obj) { * @param {Object} params The query params to be set/updated * @param {String} url The url to be operated on * @param {Boolean} clearParams Indicates whether existing query params should be removed or not + * @param {Boolean} railsArraySyntax When enabled, changes the array syntax from `keys=` to `keys[]=` according to Rails conventions * @returns {String} A copy of the original with the updated query params */ -export const setUrlParams = (params, url = window.location.href, clearParams = false) => { +export const setUrlParams = ( + params, + url = window.location.href, + clearParams = false, + railsArraySyntax = false, +) => { const urlObj = new URL(url); const queryString = urlObj.search; const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString); @@ -355,11 +361,12 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f if (params[key] === null || params[key] === undefined) { searchParams.delete(key); } else if (Array.isArray(params[key])) { + const keyName = railsArraySyntax ? `${key}[]` : key; params[key].forEach((val, idx) => { if (idx === 0) { - searchParams.set(key, val); + searchParams.set(keyName, val); } else { - searchParams.append(key, val); + searchParams.append(keyName, val); } }); } else { |