UNPKG

@thasmo/external-svg-polyfill

Version:

Small and basic polyfill to support referencing external SVG files.

171 lines (170 loc) 6.89 kB
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); }); }); } }