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; } }