@thasmo/external-svg-polyfill
Version:
Small and basic polyfill to support referencing external SVG files.
171 lines (170 loc) • 6.89 kB
JavaScript
export default class Polyfill {
constructor(options) {
this.defaults = {
target: 'svg use',
context: window.document.body || window.document.documentElement,
root: window.document.body || window.document.documentElement,
run: true,
prefix: true,
detect: true,
observe: true,
crossdomain: true,
namespace: 'external-svg-polyfill',
agents: [
/msie|trident/i,
/edge\/12/i,
/ucbrowser\/11/i,
],
};
this.options = Object.assign(Object.assign({}, this.defaults), options);
this.cache = {
files: new Map(),
elements: new Map(),
};
this.handler = {
viewportChange: this.onViewportChange.bind(this),
documentChange: this.onDocumentChanged.bind(this),
};
this.observer = new MutationObserver(this.handler.documentChange);
this.parser = window.document.createElement('a');
this.process = !this.options.detect || this.detect();
this.options.run && this.run();
}
run() {
this.updateElements();
this.options.observe && this.observe();
}
detect() {
return this.options.agents.some((agent) => agent.test(window.navigator.userAgent));
}
observe() {
this.observer.observe(this.options.context, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['href', 'xlink:href'],
});
window.addEventListener('resize', this.handler.viewportChange);
window.addEventListener('orientationchange', this.handler.viewportChange);
}
unobserve() {
this.observer.disconnect();
window.removeEventListener('resize', this.handler.viewportChange);
window.removeEventListener('orientationchange', this.handler.viewportChange);
}
destroy() {
this.unobserve();
this.cache.elements.forEach((value, element) => {
this.dispatchEvent(element, 'revoke', { value }, () => {
this.renderFrame(() => {
this.setLinkAttribute(element, value);
this.cache.elements.delete(element);
});
});
});
this.cache.files.forEach((file, address) => {
file && this.dispatchEvent(file, 'remove', { address }, () => {
this.renderFrame(() => this.options.root.removeChild(file));
this.cache.files.delete(address);
});
});
}
updateElements() {
const elements = typeof this.options.target === 'string'
? [].slice.call(this.options.context.querySelectorAll(this.options.target))
: this.options.target;
elements.forEach(this.processElement.bind(this));
}
processElement(element) {
const value = element.getAttribute('href') || element.getAttribute('xlink:href');
if (value && value.indexOf('#') !== 0 && !this.cache.elements.has(element)) {
this.parser.href = value;
if (this.process || (this.options.crossdomain && window.location.origin !== this.parser.origin)) {
const address = this.parser.href.split('#')[0];
const identifier = this.generateIdentifier(this.parser.hash, this.parser.pathname);
if (address && !this.cache.files.has(address)) {
this.dispatchEvent(element, 'load', { address }, () => {
this.cache.files.set(address, null);
this.loadFile(address);
});
}
this.dispatchEvent(element, 'apply', { address, identifier }, () => {
this.renderFrame(() => {
this.setLinkAttribute(element, `#${identifier}`);
this.cache.elements.set(element, value);
});
});
}
}
}
loadFile(address) {
const loader = new XMLHttpRequest();
loader.addEventListener('load', (event) => this.onFileLoaded.call(this, event, address));
loader.open('get', address);
loader.responseType = 'document';
loader.send();
}
generateIdentifier(identifier, prefix) {
identifier = identifier.replace('#', '');
prefix = prefix.replace(/^\//, '').replace(/\.svg$/, '').replace(/[^a-zA-Z0-9]/g, '-');
return this.options.prefix
? `${prefix}-${identifier}`
: identifier;
}
dispatchEvent(element, name, detail, callback) {
const event = window.document.createEvent('CustomEvent');
event.initCustomEvent(`${this.options.namespace}.${name}`, true, true, detail);
element.dispatchEvent(event);
if (!event.defaultPrevented && callback) {
callback();
}
}
renderFrame(callback) {
window.requestAnimationFrame(callback.bind(this));
}
setLinkAttribute(element, value) {
element.hasAttribute('href') && element.setAttribute('href', value);
element.hasAttribute('xlink:href') && element.setAttribute('xlink:href', value);
}
prefixValues(file, prefix) {
const value = file.getAttribute('id');
if (value) {
const identifier = this.generateIdentifier(value, prefix);
file.setAttribute('id', identifier);
}
[].slice.call(file.querySelectorAll('[id]')).forEach((reference) => {
const value = reference.getAttribute('id');
if (value) {
const identifier = this.generateIdentifier(value, prefix);
reference.setAttribute('id', identifier);
[].slice.call(file.querySelectorAll(`[fill="url(#${value})"]`)).forEach((referencee) => {
referencee.setAttribute('fill', `url(#${identifier})`);
});
}
});
}
onDocumentChanged() {
this.updateElements();
}
onViewportChange() {
this.updateElements();
}
onFileLoaded(event, address) {
const file = event.target.response.documentElement;
file.setAttribute('aria-hidden', 'true');
file.style.position = 'absolute';
file.style.overflow = 'hidden';
file.style.width = 0;
file.style.height = 0;
this.cache.files.set(address, file);
if (this.options.prefix) {
this.parser.href = address;
this.prefixValues(file, this.parser.pathname);
}
this.dispatchEvent(this.options.root, 'insert', { address, file }, () => {
this.renderFrame(() => {
this.options.root.insertAdjacentElement('afterbegin', file);
});
});
}
}