diff --git a/docs/release-notes/version-3.1.md b/docs/release-notes/version-3.1.md index 6de3a120c..b24aba73e 100644 --- a/docs/release-notes/version-3.1.md +++ b/docs/release-notes/version-3.1.md @@ -6,6 +6,7 @@ * [#8548](https://github.com/netbox-community/netbox/issues/8548) - Fix display of VC members when position is zero * [#8561](https://github.com/netbox-community/netbox/issues/8561) - Include option to connect a rear port to a console port +* [#8331](https://github.com/netbox-community/netbox/issues/8331) - Implement `replaceAll` string utility function to improve browser compatibility --- diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 33b94b478..678527f92 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index eb6b85087..66cf3d8d3 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/dist/status.js b/netbox/project-static/dist/status.js index bffd74dca..f87d11348 100644 Binary files a/netbox/project-static/dist/status.js and b/netbox/project-static/dist/status.js differ diff --git a/netbox/project-static/src/select/api/apiSelect.ts b/netbox/project-static/src/select/api/apiSelect.ts index e12a10421..5cd2c0055 100644 --- a/netbox/project-static/src/select/api/apiSelect.ts +++ b/netbox/project-static/src/select/api/apiSelect.ts @@ -8,11 +8,12 @@ import { DynamicParamsMap } from './dynamicParams'; import { isStaticParams, isOption } from './types'; import { hasMore, - isTruthy, hasError, - getElement, + isTruthy, getApiData, + getElement, isApiError, + replaceAll, createElement, uniqueByProperty, findFirstAdjacent, @@ -461,7 +462,7 @@ export class APISelect { // Set any primitive k/v pairs as data attributes on each option. for (const [k, v] of Object.entries(result)) { if (!['id', 'slug'].includes(k) && ['string', 'number', 'boolean'].includes(typeof v)) { - const key = k.replace(/_/g, '-'); + const key = replaceAll(k, '_', '-'); data[key] = String(v); } // Set option to disabled if the result contains a matching key and is truthy. @@ -659,7 +660,7 @@ export class APISelect { for (const [key, value] of this.pathValues.entries()) { for (const result of this.url.matchAll(new RegExp(`({{${key}}})`, 'g'))) { if (isTruthy(value)) { - url = url.replace(result[1], value.toString()); + url = replaceAll(url, result[1], value.toString()); } } } @@ -741,7 +742,7 @@ export class APISelect { * @param id DOM ID of the other element. */ private updatePathValues(id: string): void { - const key = id.replace(/^id_/gi, ''); + const key = replaceAll(id, /^id_/i, ''); const element = getElement(`id_${key}`); if (element !== null) { // If this element's URL contains Django template tags ({{), replace the template tag @@ -919,16 +920,18 @@ export class APISelect { style.setAttribute('data-netbox', id); // Scope the CSS to apply both the list item and the selected item. - style.innerHTML = ` + style.innerHTML = replaceAll( + ` div.ss-values div.ss-value[data-id="${id}"], div.ss-list div.ss-option:not(.ss-disabled)[data-id="${id}"] { background-color: ${bg} !important; color: ${fg} !important; } - ` - .replace(/\n/g, '') - .trim(); + `, + '\n', + '', + ).trim(); // Add the style element to the DOM. document.head.appendChild(style); diff --git a/netbox/project-static/src/tables/interfaceTable.ts b/netbox/project-static/src/tables/interfaceTable.ts index 6937a82e8..d2b20f322 100644 --- a/netbox/project-static/src/tables/interfaceTable.ts +++ b/netbox/project-static/src/tables/interfaceTable.ts @@ -1,4 +1,4 @@ -import { getElements, findFirstAdjacent } from '../util'; +import { getElements, replaceAll, findFirstAdjacent } from '../util'; type InterfaceState = 'enabled' | 'disabled'; type ShowHide = 'show' | 'hide'; @@ -105,9 +105,9 @@ class ButtonState { */ private toggleButton(): void { if (this.buttonState === 'show') { - this.button.innerText = this.button.innerText.replace(/Show/g, 'Hide'); + this.button.innerText = replaceAll(this.button.innerText, 'Show', 'Hide'); } else if (this.buttonState === 'hide') { - this.button.innerText = this.button.innerText.replace(/Hide/g, 'Show'); + this.button.innerText = replaceAll(this.button.innerHTML, 'Hide', 'Show'); } } diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index d85f9fbf9..b242cd567 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -315,7 +315,7 @@ export function* getRowValues(table: HTMLTableRowElement): Generator { for (const element of table.querySelectorAll('td')) { if (element !== null) { if (isTruthy(element.innerText) && element.innerText !== '—') { - yield element.innerText.replace(/[\n\r]/g, '').trim(); + yield replaceAll(element.innerText, '[\n\r]', '').trim(); } } } @@ -436,3 +436,49 @@ export function uniqueByProperty(arr: T[], } return Array.from(baseMap.values()); } + +/** + * Replace all occurrences of a pattern with a replacement string. + * + * This is a browser-compatibility-focused drop-in replacement for `String.prototype.replaceAll()`, + * introduced in ES2021. + * + * @param input string to be processed. + * @param pattern regex pattern string or RegExp object to search for. + * @param replacement replacement substring with which `pattern` matches will be replaced. + * @returns processed version of `input`. + */ +export function replaceAll(input: string, pattern: string | RegExp, replacement: string): string { + // Ensure input is a string. + if (typeof input !== 'string') { + throw new TypeError("replaceAll 'input' argument must be a string"); + } + // Ensure pattern is a string or RegExp. + if (typeof pattern !== 'string' && !(pattern instanceof RegExp)) { + throw new TypeError("replaceAll 'pattern' argument must be a string or RegExp instance"); + } + // Ensure replacement is able to be stringified. + switch (typeof replacement) { + case 'boolean': + replacement = String(replacement); + break; + case 'number': + replacement = String(replacement); + break; + case 'string': + break; + default: + throw new TypeError("replaceAll 'replacement' argument must be stringifyable"); + } + + if (pattern instanceof RegExp) { + // Add global flag to existing RegExp object and deduplicate + const flags = Array.from(new Set([...pattern.flags.split(''), 'g'])).join(''); + pattern = new RegExp(pattern.source, flags); + } else { + // Create a RegExp object with the global flag set. + pattern = new RegExp(pattern, 'g'); + } + + return input.replace(pattern, replacement); +}