Merge remote-tracking branch 'upstream/main'

This commit is contained in:
2026-08-26 09:31:34 +08:00
parent 9b8c4bfab6
commit 394eb0285d
34137 changed files with 3589424 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
export default {
/**
* Should following options be automatically configured:
* - `syncUrlsWithBaseTag`
* - `locationChangeAngularEmitter`
* - `moveGradientsOutsideSymbol`
* @type {boolean}
*/
autoConfigure: true,
/**
* Default mounting selector
* @type {string}
*/
mountTo: 'body',
/**
* Fix disappearing SVG elements when <base href> exists.
* Executes when sprite mounted.
* @see http://stackoverflow.com/a/18265336/796152
* @see https://github.com/everdimension/angular-svg-base-fix
* @see https://github.com/angular/angular.js/issues/8934#issuecomment-56568466
* @type {boolean}
*/
syncUrlsWithBaseTag: false,
/**
* Should sprite listen custom location change event
* @type {boolean}
*/
listenLocationChangeEvent: true,
/**
* Custom window event name which should be emitted to update sprite urls
* @type {string}
*/
locationChangeEvent: 'locationChange',
/**
* Emit location change event in Angular automatically
* @type {boolean}
*/
locationChangeAngularEmitter: false,
/**
* Selector to find symbols usages when updating sprite urls
* @type {string}
*/
usagesToUpdate: 'use[*|href]',
/**
* Fix Firefox bug when gradients and patterns don't work if they are within a symbol.
* Executes when sprite is rendered, but not mounted.
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=306674
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=353575
* @see https://bugzilla.mozilla.org/show_bug.cgi?id=1235364
* @type {boolean}
*/
moveGradientsOutsideSymbol: false
};
+253
View File
@@ -0,0 +1,253 @@
import merge from 'deepmerge';
import Emitter from 'mitt';
import Sprite from './sprite';
import BrowserSymbol from './browser-symbol';
import defaultConfig from './browser-sprite.config';
import {
arrayFrom,
parse,
moveGradientsOutsideSymbol,
browserDetector as browser,
getUrlWithoutFragment,
updateUrls,
locationChangeAngularEmitter,
evalStylesIEWorkaround
} from './utils';
/**
* Internal emitter events
* @enum
* @private
*/
const Events = {
MOUNT: 'mount',
SYMBOL_MOUNT: 'symbol_mount'
};
export default class BrowserSprite extends Sprite {
constructor(cfg = {}) {
super(merge(defaultConfig, cfg));
const emitter = Emitter();
this._emitter = emitter;
this.node = null;
const { config } = this;
if (config.autoConfigure) {
this._autoConfigure(cfg);
}
if (config.syncUrlsWithBaseTag) {
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
emitter.on(Events.MOUNT, () => this.updateUrls('#', baseUrl));
}
const handleLocationChange = this._handleLocationChange.bind(this);
this._handleLocationChange = handleLocationChange;
// Provide way to update sprite urls externally via dispatching custom window event
if (config.listenLocationChangeEvent) {
window.addEventListener(config.locationChangeEvent, handleLocationChange);
}
// Emit location change event in Angular automatically
if (config.locationChangeAngularEmitter) {
locationChangeAngularEmitter(config.locationChangeEvent);
}
// After sprite mounted
emitter.on(Events.MOUNT, (spriteNode) => {
if (config.moveGradientsOutsideSymbol) {
moveGradientsOutsideSymbol(spriteNode);
}
});
// After symbol mounted into sprite
emitter.on(Events.SYMBOL_MOUNT, (symbolNode) => {
if (config.moveGradientsOutsideSymbol) {
moveGradientsOutsideSymbol(symbolNode.parentNode);
}
if (browser.isIE() || browser.isEdge()) {
evalStylesIEWorkaround(symbolNode);
}
});
}
/**
* @return {boolean}
*/
get isMounted() {
return !!this.node;
}
/**
* Automatically configure following options
* - `syncUrlsWithBaseTag`
* - `locationChangeAngularEmitter`
* - `moveGradientsOutsideSymbol`
* @param {Object} cfg
* @private
*/
_autoConfigure(cfg) {
const { config } = this;
if (typeof cfg.syncUrlsWithBaseTag === 'undefined') {
config.syncUrlsWithBaseTag = typeof document.getElementsByTagName('base')[0] !== 'undefined';
}
if (typeof cfg.locationChangeAngularEmitter === 'undefined') {
config.locationChangeAngularEmitter = typeof window.angular !== 'undefined';
}
if (typeof cfg.moveGradientsOutsideSymbol === 'undefined') {
config.moveGradientsOutsideSymbol = browser.isFirefox();
}
}
/**
* @param {Event} event
* @param {Object} event.detail
* @param {string} event.detail.oldUrl
* @param {string} event.detail.newUrl
* @private
*/
_handleLocationChange(event) {
const { oldUrl, newUrl } = event.detail;
this.updateUrls(oldUrl, newUrl);
}
/**
* Add new symbol. If symbol with the same id exists it will be replaced.
* If sprite already mounted - `symbol.mount(sprite.node)` will be called.
* @fires Events#SYMBOL_MOUNT
* @param {BrowserSpriteSymbol} symbol
* @return {boolean} `true` - symbol was added, `false` - replaced
*/
add(symbol) {
const sprite = this;
const isNewSymbol = super.add(symbol);
if (this.isMounted && isNewSymbol) {
symbol.mount(sprite.node);
this._emitter.emit(Events.SYMBOL_MOUNT, symbol.node);
}
return isNewSymbol;
}
/**
* Attach to existing DOM node
* @param {string|Element} target
* @return {Element|null} attached DOM Element. null if node to attach not found.
*/
attach(target) {
const sprite = this;
if (sprite.isMounted) {
return sprite.node;
}
/** @type Element */
const node = typeof target === 'string' ? document.querySelector(target) : target;
sprite.node = node;
// Already added symbols needs to be mounted
this.symbols.forEach((symbol) => {
symbol.mount(sprite.node);
this._emitter.emit(Events.SYMBOL_MOUNT, symbol.node);
});
// Create symbols from existing DOM nodes, add and mount them
arrayFrom(node.querySelectorAll('symbol'))
.forEach((symbolNode) => {
const symbol = BrowserSymbol.createFromExistingNode(symbolNode);
symbol.node = symbolNode; // hack to prevent symbol mounting to sprite when adding
sprite.add(symbol);
});
this._emitter.emit(Events.MOUNT, node);
return node;
}
destroy() {
const { config, symbols, _emitter } = this;
symbols.forEach(s => s.destroy());
_emitter.off('*');
window.removeEventListener(config.locationChangeEvent, this._handleLocationChange);
if (this.isMounted) {
this.unmount();
}
}
/**
* @fires Events#MOUNT
* @param {string|Element} [target]
* @param {boolean} [prepend=false]
* @return {Element|null} rendered sprite node. null if mount node not found.
*/
mount(target = this.config.mountTo, prepend = false) {
const sprite = this;
if (sprite.isMounted) {
return sprite.node;
}
const mountNode = typeof target === 'string' ? document.querySelector(target) : target;
const node = sprite.render();
this.node = node;
if (prepend && mountNode.childNodes[0]) {
mountNode.insertBefore(node, mountNode.childNodes[0]);
} else {
mountNode.appendChild(node);
}
this._emitter.emit(Events.MOUNT, node);
return node;
}
/**
* @return {Element}
*/
render() {
return parse(this.stringify());
}
/**
* Detach sprite from the DOM
*/
unmount() {
this.node.parentNode.removeChild(this.node);
}
/**
* Update URLs in sprite and usage elements
* @param {string} oldUrl
* @param {string} newUrl
* @return {boolean} `true` - URLs was updated, `false` - sprite is not mounted
*/
updateUrls(oldUrl, newUrl) {
if (!this.isMounted) {
return false;
}
const usages = document.querySelectorAll(this.config.usagesToUpdate);
updateUrls(
this.node,
usages,
`${getUrlWithoutFragment(oldUrl)}#`,
`${getUrlWithoutFragment(newUrl)}#`
);
return true;
}
}
+58
View File
@@ -0,0 +1,58 @@
import SpriteSymbol from './symbol';
import parse from './utils/parse';
import wrapInSvgString from './utils/wrap-in-svg-string';
export default class BrowserSpriteSymbol extends SpriteSymbol {
get isMounted() {
return !!this.node;
}
/**
* @param {Element} node
* @return {BrowserSpriteSymbol}
*/
static createFromExistingNode(node) {
return new BrowserSpriteSymbol({
id: node.getAttribute('id'),
viewBox: node.getAttribute('viewBox'),
content: node.outerHTML
});
}
destroy() {
if (this.isMounted) {
this.unmount();
}
super.destroy();
}
/**
* @param {Element|string} target
* @return {Element}
*/
mount(target) {
if (this.isMounted) {
return this.node;
}
const mountTarget = typeof target === 'string' ? document.querySelector(target) : target;
const node = this.render();
this.node = node;
mountTarget.appendChild(node);
return node;
}
/**
* @return {Element}
*/
render() {
const content = this.stringify();
return parse(wrapInSvgString(content)).childNodes[0];
}
unmount() {
this.node.parentNode.removeChild(this.node);
}
}
+12
View File
@@ -0,0 +1,12 @@
import namespaces from 'svg-baker/namespaces';
const { svg, xlink } = namespaces;
export default {
attrs: {
[svg.name]: svg.uri,
[xlink.name]: xlink.uri,
style: ['position: absolute', 'width: 0', 'height: 0'].join('; '),
'aria-hidden': 'true'
}
};
+85
View File
@@ -0,0 +1,85 @@
import merge from 'deepmerge';
import wrapInSvgString from './utils/wrap-in-svg-string';
import defaultConfig from './sprite.config';
export default class Sprite {
/**
* @param {Object} [config]
*/
constructor(config) {
this.config = merge(defaultConfig, config || {});
this.symbols = [];
}
/**
* Add new symbol. If symbol with the same id exists it will be replaced.
* @param {SpriteSymbol} symbol
* @return {boolean} `true` - symbol was added, `false` - replaced
*/
add(symbol) {
const { symbols } = this;
const existing = this.find(symbol.id);
if (existing) {
symbols[symbols.indexOf(existing)] = symbol;
return false;
}
symbols.push(symbol);
return true;
}
/**
* Remove symbol & destroy it
* @param {string} id
* @return {boolean} `true` - symbol was found & successfully destroyed, `false` - otherwise
*/
remove(id) {
const { symbols } = this;
const symbol = this.find(id);
if (symbol) {
symbols.splice(symbols.indexOf(symbol), 1);
symbol.destroy();
return true;
}
return false;
}
/**
* @param {string} id
* @return {SpriteSymbol|null}
*/
find(id) {
return this.symbols.filter(s => s.id === id)[0] || null;
}
/**
* @param {string} id
* @return {boolean}
*/
has(id) {
return this.find(id) !== null;
}
/**
* @return {string}
*/
stringify() {
const { attrs } = this.config;
const stringifiedSymbols = this.symbols.map(s => s.stringify()).join('');
return wrapInSvgString(stringifiedSymbols, attrs);
}
/**
* @return {string}
*/
toString() {
return this.stringify();
}
destroy() {
this.symbols.forEach(s => s.destroy());
}
}
+25
View File
@@ -0,0 +1,25 @@
export default class SpriteSymbol {
constructor({ id, viewBox, content }) {
this.id = id;
this.viewBox = viewBox;
this.content = content;
}
/**
* @return {string}
*/
stringify() {
return this.content;
}
/**
* @return {string}
*/
toString() {
return this.stringify();
}
destroy() {
['id', 'viewBox', 'content'].forEach(prop => delete this[prop]);
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* @param {*} arrayLike
* @return {Array}
*/
export default function (arrayLike) {
return Array.prototype.slice.call(arrayLike, 0);
}
@@ -0,0 +1,8 @@
export default {
isChrome: () => /chrome/i.test(navigator.userAgent),
isFirefox: () => /firefox/i.test(navigator.userAgent),
// https://msdn.microsoft.com/en-us/library/ms537503(v=vs.85).aspx
isIE: () => /msie/i.test(navigator.userAgent) || /trident/i.test(navigator.userAgent),
isEdge: () => /edge/i.test(navigator.userAgent)
};
@@ -0,0 +1,9 @@
/**
* @param {string} name
* @param {*} data
*/
export default function (name, data) {
const event = document.createEvent('CustomEvent');
event.initCustomEvent(name, false, false, data);
window.dispatchEvent(event);
}
@@ -0,0 +1,22 @@
import arrayFrom from './array-from';
/**
* IE doesn't evaluate <style> tags in SVGs that are dynamically added to the page.
* This trick will trigger IE to read and use any existing SVG <style> tags.
* @see https://github.com/iconic/SVGInjector/issues/23
* @see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10898469/
*
* @param {Element} node DOM Element to search <style> tags in
* @return {Array<HTMLStyleElement>}
*/
export default function (node) {
const updatedNodes = [];
arrayFrom(node.querySelectorAll('style'))
.forEach((style) => {
style.textContent += '';
updatedNodes.push(style);
});
return updatedNodes;
}
@@ -0,0 +1,7 @@
/**
* @param {string} [url] If not provided - current URL will be used
* @return {string}
*/
export default function (url) {
return (url || window.location.href).split('#')[0];
}
+13
View File
@@ -0,0 +1,13 @@
export { default as arrayFrom } from './array-from';
export { default as browserDetector } from './browser-detector';
export { default as dispatchCustomEvent } from './dispatch-custom-event';
export { default as evalStylesIEWorkaround } from './eval-styles-ie-workaround';
export { default as getUrlWithoutFragment } from './get-url-without-fragment';
export { default as locationChangeAngularEmitter } from './location-change-angular-emitter';
export { default as moveGradientsOutsideSymbol } from './move-gradients-outside-symbol';
export { default as objectToAttrsString } from './object-to-attrs-string';
export { default as parse } from './parse';
export { default as selectAttributes } from './select-attributes';
export { default as stringify } from './stringify';
export { default as updateUrls } from './update-urls';
export { default as wrapInSvgString } from './wrap-in-svg-string';
@@ -0,0 +1,13 @@
/* global angular */
import dispatchEvent from './dispatch-custom-event';
/**
* @param {string} eventName
*/
export default function (eventName) {
angular.module('ng').run(['$rootScope', ($rootScope) => {
$rootScope.$on('$locationChangeSuccess', (e, newUrl, oldUrl) => {
dispatchEvent(eventName, { oldUrl, newUrl });
});
}]);
}
@@ -0,0 +1,17 @@
import arrayFrom from './array-from';
const defaultSelector = 'linearGradient, radialGradient, pattern, mask, clipPath';
/**
* @param {Element} svg
* @param {string} [selector]
* @return {Element}
*/
export default function (svg, selector = defaultSelector) {
arrayFrom(svg.querySelectorAll('symbol')).forEach((symbol) => {
arrayFrom(symbol.querySelectorAll(selector)).forEach((node) => {
symbol.parentNode.insertBefore(node, symbol);
});
});
return svg;
}
@@ -0,0 +1,10 @@
/**
* @param {Object} attrs
* @return {string}
*/
export default function (attrs) {
return Object.keys(attrs).map((attr) => {
const value = attrs[attr].toString().replace(/"/g, '&quot;');
return `${attr}="${value}"`;
}).join(' ');
}
+19
View File
@@ -0,0 +1,19 @@
/**
* @param {string} content
* @return {Element}
*/
export default function (content) {
const hasImportNode = !!document.importNode;
const doc = new DOMParser().parseFromString(content, 'image/svg+xml').documentElement;
/**
* Fix for browser which are throwing WrongDocumentError
* if you insert an element which is not part of the document
* @see http://stackoverflow.com/a/7986519/4624403
*/
if (hasImportNode) {
return document.importNode(doc, true);
}
return doc;
}
+20
View File
@@ -0,0 +1,20 @@
import arrayFrom from './array-from';
/**
* @param {NodeList} nodes
* @param {Function} [matcher]
* @return {Attr[]}
*/
export default function selectAttributes(nodes, matcher) {
const attrs = arrayFrom(nodes).reduce((acc, node) => {
if (!node.attributes) {
return acc;
}
const arrayfied = arrayFrom(node.attributes);
const matched = matcher ? arrayfied.filter(matcher) : arrayfied;
return acc.concat(matched);
}, []);
return attrs;
}
+20
View File
@@ -0,0 +1,20 @@
import arrayFrom from './array-from';
/**
* @param {NodeList|Node} nodes
* @param {boolean} [clone=true]
* @return {string}
*/
export default function (nodes, clone = true) {
const wrapper = document.createElement('div');
if (nodes instanceof NodeList) {
arrayFrom(nodes).forEach((node) => {
wrapper.appendChild(clone === true ? node.cloneNode(true) : node);
});
} else if (nodes instanceof Node) {
wrapper.appendChild(clone === true ? nodes.cloneNode(true) : nodes);
}
return wrapper.innerHTML;
}
+84
View File
@@ -0,0 +1,84 @@
import namespaces from 'svg-baker/namespaces';
import selectAttributes from './select-attributes';
import arrayFrom from './array-from';
const xLinkNS = namespaces.xlink.uri;
const xLinkAttrName = 'xlink:href';
// eslint-disable-next-line no-useless-escape
const specialUrlCharsPattern = /[{}|\\\^\[\]`"<>]/g;
function encoder(url) {
return url.replace(specialUrlCharsPattern, (match) => {
return `%${match[0].charCodeAt(0).toString(16).toUpperCase()}`;
});
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
}
/**
* @param {NodeList} nodes
* @param {string} startsWith
* @param {string} replaceWith
* @return {NodeList}
*/
function updateReferences(nodes, startsWith, replaceWith) {
arrayFrom(nodes).forEach((node) => {
const href = node.getAttribute(xLinkAttrName);
if (href && href.indexOf(startsWith) === 0) {
const newUrl = href.replace(startsWith, replaceWith);
node.setAttributeNS(xLinkNS, xLinkAttrName, newUrl);
}
});
return nodes;
}
/**
* List of SVG attributes to update url() target in them
*/
const attList = [
'clipPath',
'colorProfile',
'src',
'cursor',
'fill',
'filter',
'marker',
'markerStart',
'markerMid',
'markerEnd',
'mask',
'stroke',
'style'
];
const attSelector = attList.map(attr => `[${attr}]`).join(',');
/**
* Update URLs in svg image (like `fill="url(...)"`) and update referencing elements
* @param {Element} svg
* @param {NodeList} references
* @param {string|RegExp} startsWith
* @param {string} replaceWith
* @return {void}
*
* @example
* const sprite = document.querySelector('svg.sprite');
* const usages = document.querySelectorAll('use');
* updateUrls(sprite, usages, '#', 'prefix#');
*/
export default function (svg, references, startsWith, replaceWith) {
const startsWithEncoded = encoder(startsWith);
const replaceWithEncoded = encoder(replaceWith);
const nodes = svg.querySelectorAll(attSelector);
const attrs = selectAttributes(nodes, ({ localName, value }) => {
return attList.indexOf(localName) !== -1 && value.indexOf(`url(${startsWithEncoded}`) !== -1;
});
attrs.forEach(attr => attr.value = attr.value.replace(new RegExp(escapeRegExp(startsWithEncoded), 'g'), replaceWithEncoded));
updateReferences(references, startsWithEncoded, replaceWithEncoded);
}
@@ -0,0 +1,21 @@
import merge from 'deepmerge';
import namespaces from 'svg-baker/namespaces';
import objectToAttrsString from './object-to-attrs-string';
const { svg, xlink } = namespaces;
const defaultAttrs = {
[svg.name]: svg.uri,
[xlink.name]: xlink.uri
};
/**
* @param {string} [content]
* @param {Object} [attributes]
* @return {string}
*/
export default function (content = '', attributes) {
const attrs = merge(defaultAttrs, attributes || {});
const attrsRendered = objectToAttrsString(attrs);
return `<svg ${attrsRendered}>${content}</svg>`;
}