e071889967f7f8902e62ad5281b669fd441ae10b9f5caff5d01c835f63c216221b2b39f41d4d837376aa7cefb6d53e712348a0e83b1ebd82ada8adeb0a36ee 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import merge from 'deepmerge';
  2. import Emitter from 'mitt';
  3. import Sprite from './sprite';
  4. import BrowserSymbol from './browser-symbol';
  5. import defaultConfig from './browser-sprite.config';
  6. import {
  7. arrayFrom,
  8. parse,
  9. moveGradientsOutsideSymbol,
  10. browserDetector as browser,
  11. getUrlWithoutFragment,
  12. updateUrls,
  13. locationChangeAngularEmitter,
  14. evalStylesIEWorkaround
  15. } from './utils';
  16. /**
  17. * Internal emitter events
  18. * @enum
  19. * @private
  20. */
  21. const Events = {
  22. MOUNT: 'mount',
  23. SYMBOL_MOUNT: 'symbol_mount'
  24. };
  25. export default class BrowserSprite extends Sprite {
  26. constructor(cfg = {}) {
  27. super(merge(defaultConfig, cfg));
  28. const emitter = Emitter();
  29. this._emitter = emitter;
  30. this.node = null;
  31. const { config } = this;
  32. if (config.autoConfigure) {
  33. this._autoConfigure(cfg);
  34. }
  35. if (config.syncUrlsWithBaseTag) {
  36. const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
  37. emitter.on(Events.MOUNT, () => this.updateUrls('#', baseUrl));
  38. }
  39. const handleLocationChange = this._handleLocationChange.bind(this);
  40. this._handleLocationChange = handleLocationChange;
  41. // Provide way to update sprite urls externally via dispatching custom window event
  42. if (config.listenLocationChangeEvent) {
  43. window.addEventListener(config.locationChangeEvent, handleLocationChange);
  44. }
  45. // Emit location change event in Angular automatically
  46. if (config.locationChangeAngularEmitter) {
  47. locationChangeAngularEmitter(config.locationChangeEvent);
  48. }
  49. // After sprite mounted
  50. emitter.on(Events.MOUNT, (spriteNode) => {
  51. if (config.moveGradientsOutsideSymbol) {
  52. moveGradientsOutsideSymbol(spriteNode);
  53. }
  54. });
  55. // After symbol mounted into sprite
  56. emitter.on(Events.SYMBOL_MOUNT, (symbolNode) => {
  57. if (config.moveGradientsOutsideSymbol) {
  58. moveGradientsOutsideSymbol(symbolNode.parentNode);
  59. }
  60. if (browser.isIE() || browser.isEdge()) {
  61. evalStylesIEWorkaround(symbolNode);
  62. }
  63. });
  64. }
  65. /**
  66. * @return {boolean}
  67. */
  68. get isMounted() {
  69. return !!this.node;
  70. }
  71. /**
  72. * Automatically configure following options
  73. * - `syncUrlsWithBaseTag`
  74. * - `locationChangeAngularEmitter`
  75. * - `moveGradientsOutsideSymbol`
  76. * @param {Object} cfg
  77. * @private
  78. */
  79. _autoConfigure(cfg) {
  80. const { config } = this;
  81. if (typeof cfg.syncUrlsWithBaseTag === 'undefined') {
  82. config.syncUrlsWithBaseTag = typeof document.getElementsByTagName('base')[0] !== 'undefined';
  83. }
  84. if (typeof cfg.locationChangeAngularEmitter === 'undefined') {
  85. config.locationChangeAngularEmitter = typeof window.angular !== 'undefined';
  86. }
  87. if (typeof cfg.moveGradientsOutsideSymbol === 'undefined') {
  88. config.moveGradientsOutsideSymbol = browser.isFirefox();
  89. }
  90. }
  91. /**
  92. * @param {Event} event
  93. * @param {Object} event.detail
  94. * @param {string} event.detail.oldUrl
  95. * @param {string} event.detail.newUrl
  96. * @private
  97. */
  98. _handleLocationChange(event) {
  99. const { oldUrl, newUrl } = event.detail;
  100. this.updateUrls(oldUrl, newUrl);
  101. }
  102. /**
  103. * Add new symbol. If symbol with the same id exists it will be replaced.
  104. * If sprite already mounted - `symbol.mount(sprite.node)` will be called.
  105. * @fires Events#SYMBOL_MOUNT
  106. * @param {BrowserSpriteSymbol} symbol
  107. * @return {boolean} `true` - symbol was added, `false` - replaced
  108. */
  109. add(symbol) {
  110. const sprite = this;
  111. const isNewSymbol = super.add(symbol);
  112. if (this.isMounted && isNewSymbol) {
  113. symbol.mount(sprite.node);
  114. this._emitter.emit(Events.SYMBOL_MOUNT, symbol.node);
  115. }
  116. return isNewSymbol;
  117. }
  118. /**
  119. * Attach to existing DOM node
  120. * @param {string|Element} target
  121. * @return {Element|null} attached DOM Element. null if node to attach not found.
  122. */
  123. attach(target) {
  124. const sprite = this;
  125. if (sprite.isMounted) {
  126. return sprite.node;
  127. }
  128. /** @type Element */
  129. const node = typeof target === 'string' ? document.querySelector(target) : target;
  130. sprite.node = node;
  131. // Already added symbols needs to be mounted
  132. this.symbols.forEach((symbol) => {
  133. symbol.mount(sprite.node);
  134. this._emitter.emit(Events.SYMBOL_MOUNT, symbol.node);
  135. });
  136. // Create symbols from existing DOM nodes, add and mount them
  137. arrayFrom(node.querySelectorAll('symbol'))
  138. .forEach((symbolNode) => {
  139. const symbol = BrowserSymbol.createFromExistingNode(symbolNode);
  140. symbol.node = symbolNode; // hack to prevent symbol mounting to sprite when adding
  141. sprite.add(symbol);
  142. });
  143. this._emitter.emit(Events.MOUNT, node);
  144. return node;
  145. }
  146. destroy() {
  147. const { config, symbols, _emitter } = this;
  148. symbols.forEach(s => s.destroy());
  149. _emitter.off('*');
  150. window.removeEventListener(config.locationChangeEvent, this._handleLocationChange);
  151. if (this.isMounted) {
  152. this.unmount();
  153. }
  154. }
  155. /**
  156. * @fires Events#MOUNT
  157. * @param {string|Element} [target]
  158. * @param {boolean} [prepend=false]
  159. * @return {Element|null} rendered sprite node. null if mount node not found.
  160. */
  161. mount(target = this.config.mountTo, prepend = false) {
  162. const sprite = this;
  163. if (sprite.isMounted) {
  164. return sprite.node;
  165. }
  166. const mountNode = typeof target === 'string' ? document.querySelector(target) : target;
  167. const node = sprite.render();
  168. this.node = node;
  169. if (prepend && mountNode.childNodes[0]) {
  170. mountNode.insertBefore(node, mountNode.childNodes[0]);
  171. } else {
  172. mountNode.appendChild(node);
  173. }
  174. this._emitter.emit(Events.MOUNT, node);
  175. return node;
  176. }
  177. /**
  178. * @return {Element}
  179. */
  180. render() {
  181. return parse(this.stringify());
  182. }
  183. /**
  184. * Detach sprite from the DOM
  185. */
  186. unmount() {
  187. this.node.parentNode.removeChild(this.node);
  188. }
  189. /**
  190. * Update URLs in sprite and usage elements
  191. * @param {string} oldUrl
  192. * @param {string} newUrl
  193. * @return {boolean} `true` - URLs was updated, `false` - sprite is not mounted
  194. */
  195. updateUrls(oldUrl, newUrl) {
  196. if (!this.isMounted) {
  197. return false;
  198. }
  199. const usages = document.querySelectorAll(this.config.usagesToUpdate);
  200. updateUrls(
  201. this.node,
  202. usages,
  203. `${getUrlWithoutFragment(oldUrl)}#`,
  204. `${getUrlWithoutFragment(newUrl)}#`
  205. );
  206. return true;
  207. }
  208. }