UNPKG

30.9 kBJavaScriptView Raw
1/**
2 * @author Oliver Findl
3 * @version 2.2.3
4 * @license MIT
5 */
6
7"use strict";
8
9/* import package.json file as PACKAGE_JSON constant */
10import PACKAGE_JSON from "../package.json";
11
12/* define PACKAGE_NAME constant */
13const PACKAGE_NAME = PACKAGE_JSON.name;
14
15/* define PACKAGE_VERSION constant */
16const PACKAGE_VERSION = PACKAGE_JSON.version;
17
18/* import polyfills if requested */
19// It is not possible to perform conditional import, so we use require syntax instead.
20// if(typeof IMPORT_POLYFILLS !== "undefined" && !!IMPORT_POLYFILLS) import "./polyfills"; // eslint-disable-line no-extra-boolean-cast
21if(typeof IMPORT_POLYFILLS !== "undefined" && !!IMPORT_POLYFILLS) require("./polyfills"); // eslint-disable-line no-extra-boolean-cast, no-undef
22
23/* define default options object */
24const DEFAULT_OPTIONS = {
25 directive: {
26 name: "v-svg-inline",
27 spriteModifierName: "sprite"
28 },
29 attributes: {
30 clone: [ "viewbox" ],
31 merge: [ "class", "style" ],
32 add: [ {
33 name: "focusable",
34 value: false
35 }, {
36 name: "role",
37 value: "presentation"
38 }, {
39 name: "tabindex",
40 value: -1
41 } ],
42 data: [],
43 remove: [ "alt", "src", "data-src" ]
44 },
45 cache: {
46 version: PACKAGE_VERSION,
47 persistent: true,
48 removeRevisions: true
49 },
50 intersectionObserverOptions: {},
51 axios: null,
52 xhtml: false
53};
54
55/* define reference id for image node intersection observer */
56const OBSERVER_REF_ID = "observer";
57
58/* define reference id for svg symbol container node */
59const CONTAINER_REF_ID = "container";
60
61/* define id for cache map local storage key */
62// Will be defined dynamically based on supplied options.cache.version value.
63// const CACHE_ID = `${PACKAGE_NAME}:${PACKAGE_VERSION}`;
64
65/* define id for image node flags */
66const FLAGS_ID = `${PACKAGE_NAME}-flags`;
67
68/* define id for svg symbol node*/
69const SYMBOL_ID = `${PACKAGE_NAME}-sprite`; // + `-<NUMBER>` - will be added dynamically
70
71/* define id for svg symbol container node */
72const CONTAINER_ID = `${SYMBOL_ID}-${CONTAINER_REF_ID}`;
73
74/* define all regular expressions */
75const REGEXP_SVG_FILENAME = /.+\.svg(?:[?#].*)?$/i;
76const REGEXP_SVG_CONTENT = /<svg(\s+[^>]+)?>([\s\S]+)<\/svg>/i;
77const REGEXP_ATTRIBUTES = /\s*([^\s=]+)[\s=]+(?:"([^"]*)"|'([^']*)')?\s*/g;
78const REGEXP_ATTRIBUTE_NAME = /^[a-z](?:[a-z0-9-:]*[a-z0-9])?$/i;
79const REGEXP_VUE_DIRECTIVE = /^v-/i;
80const REGEXP_WHITESPACE = /\s+/g;
81const REGEXP_TEMPLATE_LITERALS_WHITESPACE = /[\n\t]+/g;
82
83/* define correct response statuses */
84const CORRECT_RESPONSE_STATUSES = new Set([
85 200, // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200
86 304 // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304
87]);
88
89/**
90 * Install method for Vue plugin.
91 * @param {Function|Object} VueOrApp - Vue reference (Vue@2) or Vue instance (Vue@3).
92 * @param {Object} options - Options object.
93 * @returns {void}
94 */
95const install = (VueOrApp = null, options = {}) => {
96
97 /* store basic types references */
98 const _str = "string";
99 const _fnc = "function";
100 const _obj = "object";
101
102 /* throw error if VueOrApp argument is missing */
103 if(!VueOrApp) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [VueOrApp]`);
104
105 /* throw error if VueOrApp argument is not valid */
106 if(![ _fnc, _obj ].includes(typeof VueOrApp)) throw new TypeError(`[${PACKAGE_NAME}] Required argument is not valid! [VueOrApp]`);
107
108 /* throw error if VueOrApp argument is missing directive method */
109 if(!VueOrApp.directive) throw new Error(`[${PACKAGE_NAME}] Required method is missing! [VueOrApp.directive]`);
110
111 /* throw error if VueOrApp.directive method is not valid */
112 if(typeof VueOrApp.directive !== _fnc) throw new TypeError(`[${PACKAGE_NAME}] Required method is not valid! [VueOrApp.directive]`);
113
114 /* throw error if VueOrApp argument is missing version property */
115 if(!VueOrApp.version) throw new Error(`[${PACKAGE_NAME}] Required property is missing! [VueOrApp.version]`);
116
117 /* throw error if VueOrApp.version property is not valid */
118 if(typeof VueOrApp.version !== _str) throw new TypeError(`[${PACKAGE_NAME}] Required property is not valid! [VueOrApp.version]`);
119
120 /* throw error if Vue@1 is detected */
121 if(VueOrApp.version.startsWith("1.")) throw new Error(`[${PACKAGE_NAME}] Vue@1 is not supported!`);
122
123 /* merge default options object with supplied options object */
124 ["directive", "attributes", "cache", "intersectionObserverOptions"].forEach(option => options[option] = Object.assign({}, DEFAULT_OPTIONS[option], options[option] || {}));
125 options = Object.assign({}, DEFAULT_OPTIONS, options);
126
127 /* loop over all directives options */
128 for(const option in options.directive) {
129
130 /* cast directive option to string */
131 options.directive[option] = options.directive[option].toString().trim().toLowerCase();
132
133 /* throw error if directive option is not valid */
134 if(!options.directive[option] || option === "name" && !REGEXP_ATTRIBUTE_NAME.test(options.directive[option])) throw new TypeError(`[${PACKAGE_NAME}] Option is not valid! [options.directives.${option}="${options.directives[option]}"]`);
135
136 }
137
138 /* remove starting `v-` from directive name option */
139 options.directive.name = options.directive.name.replace(REGEXP_VUE_DIRECTIVE, "");
140
141 /* loop over all attributes options */
142 for(const option in options.attributes) {
143
144 /* throw error if option is not valid */
145 if(!Array.isArray(options.attributes[option])) throw new TypeError(`[${PACKAGE_NAME}] Option is not valid! [options.attributes.${option}=${JSON.stringify(options.attributes[option])}]`);
146
147 /* cast option values to strings */
148 options.attributes[option] = option === "add" ? options.attributes[option].map(attribute => ({
149 name: attribute.name.toString().trim().toLowerCase(),
150 value: attribute.value.toString().trim()
151 })) : options.attributes[option].map(attribute => attribute.toString().trim().toLowerCase());
152
153 /* cast option from array to set */
154 options.attributes[option] = new Set(options.attributes[option]);
155
156 }
157
158 /* loop over all cache options */
159 for(const option in options.cache) {
160
161 /* cast option value to string if option is version or boolean otherwise */
162 options.cache[option] = option === "version" ? options.cache[option].toString().trim().toLowerCase() : !!options.cache[option];
163
164 }
165
166 /* cast xhtml option to boolean */
167 options.xhtml = !!options.xhtml;
168
169 /* store Vue@3 flag */
170 const isVue3 = /* !(VueOrApp instanceof Function) && */ VueOrApp.version.startsWith("3.");
171
172 /* check if fetch is available */
173 options._fetch = "fetch" in window && typeof fetch === _fnc;
174
175 /* check if axios is available */
176 options._axios = "axios" in window && typeof axios === _fnc;
177
178 /**
179 * Validate Axios instance get method.
180 * @param {Axios} axios - Axios instance.
181 * @returns {Boolean} Validation result.
182 */
183 const validateAxiosGetMethod = (axios = null) => !!axios && typeof axios === _fnc && "get" in axios && typeof axios.get === _fnc;
184
185 /* axios validation result */
186 let axiosIsValid = false;
187
188 /* create new axios instance if not provided or not valid */
189 options.axios = ((axiosIsValid = validateAxiosGetMethod(options.axios)) ? options.axios : null) || (options._axios && "create" in axios && typeof axios.create === _fnc ? axios.create() : null); // eslint-disable-line no-cond-assign
190
191 /* check if axios instance exists and is valid */
192 options._axios = axiosIsValid || validateAxiosGetMethod(options.axios);
193
194 /* throw error if fetch and axios are not available */
195 if(!options._fetch && !options._axios) throw new Error(`[${PACKAGE_NAME}] Feature is not supported by browser! [fetch || axios]`);
196
197 /* check if intersection observer is available */
198 options._observer = "IntersectionObserver" in window;
199
200 /* throw error if intersection observer is not available */
201 // We log error instead and disable lazy processing of image nodes in processing function - processImageNode().
202 // if(!options._observer) throw new Error(`[${PACKAGE_NAME}] Feature is not supported by browser! [IntersectionObserver]`);
203 if(!options._observer) console.error(`[${PACKAGE_NAME}] Feature is not supported by browser! Disabling lazy processing of image nodes. [IntersectionObserver]`); // eslint-disable-line no-console
204
205 /* check if local storage is available */
206 options._storage = "localStorage" in window;
207
208 /* throw error if local storage is not available */
209 // We log error instead and disable caching of SVG files in processing function - fetchSvgFile().
210 // if(!options._storage && options.cache.persistent) throw new Error(`[${PACKAGE_NAME}] Feature is not supported by browser! [localStorage]`);
211 if(!options._storage && options.cache.persistent) console.error(`[${PACKAGE_NAME}] Feature is not supported by browser! Disabling persistent cache of SVG files. [localStorage]`); // eslint-disable-line no-console
212
213 /* define id for cache map local storage key */
214 const CACHE_ID = `${PACKAGE_NAME}:${options.cache.version}`;
215
216 /* remove previous cache map revisions */
217 if(options._storage && options.cache.removeRevisions) Object.entries(localStorage).map(item => item.shift()).filter(item => item.startsWith(`${PACKAGE_NAME}:`) && !item.endsWith(`:${options.cache.version}`)).forEach(item => localStorage.removeItem(item));
218
219 /* create empty cache map or restore stored cache map */
220 const cache = options._storage && options.cache.persistent ? new Map(JSON.parse(localStorage.getItem(CACHE_ID) || "[]")) : new Map;
221
222 /* create empty symbol set */
223 const symbols = new Set;
224
225 /* create empty reference map */
226 const refs = new Map;
227
228 /**
229 * Create image node intersection observer.
230 * @returns {IntersectionObserver} Image node intersection observer.
231 */
232 const createImageNodeIntersectionObserver = () => {
233
234 /* throw error if intersection observer is not available in browser */
235 if(!options._observer) throw new Error(`[${PACKAGE_NAME}] Feature is not supported by browser! [IntersectionObserver]`);
236
237 /* throw error if image node intersection observer already exists */
238 if(refs.has(OBSERVER_REF_ID)) throw new Error(`[${PACKAGE_NAME}] Can not create image node intersection observer, intersection observer already exists!`);
239
240 /* create image node intersection observer */
241 const observer = new IntersectionObserver((entries, observer) => {
242
243 /* loop over all observer entries */
244 for(const entry of entries) {
245
246 /* skip if entry is not intersecting */
247 if(!entry.isIntersecting) continue;
248
249 /* store image node reference */
250 const node = entry.target;
251
252 /* process image node */
253 processImageNode(node);
254
255 /* stop observing image node */
256 observer.unobserve(node);
257
258 }
259
260 }, options.intersectionObserverOptions);
261
262 /* set image node intersection observer reference into reference map */
263 refs.set(OBSERVER_REF_ID, observer);
264
265 /* return image node intersection observer reference */
266 return observer;
267
268 };
269
270 /**
271 * Return image node intersection observer reference.
272 * @returns {IntersectionObserver} Image node intersection observer reference.
273 */
274 const getImageNodeIntersectionObserver = () => {
275
276 /* return image node intersection observer reference */
277 return refs.has(OBSERVER_REF_ID) ? refs.get(OBSERVER_REF_ID) : createImageNodeIntersectionObserver();
278
279 };
280
281 /**
282 * Create and append SVG symbol container node into document body.
283 * @returns {SVGSVGElement} SVG symbol container node reference.
284 */
285 const createSvgSymbolContainer = () => {
286
287 /* throw error if SVG symbol container node already exists */
288 if(refs.has(CONTAINER_REF_ID)) throw new Error(`[${PACKAGE_NAME}] Can not create SVG symbol container node, container node already exists!`);
289
290 /* create svg symbol container node */
291 let container = createNode(`<svg xmlns="http://www.w3.org/2000/svg" id="${CONTAINER_ID}" style="display: none !important;"></svg>`);
292
293 /* append svg symbol container node into document body */
294 document.body.appendChild(container);
295
296 /* set svg symbol container node reference into reference map */
297 refs.set(CONTAINER_REF_ID, container = document.getElementById(CONTAINER_ID));
298
299 /* return svg symbol container node reference */
300 return container;
301
302 };
303
304 /**
305 * Return SVG symbol container node reference.
306 * @returns {SVGSVGElement} SVG symbol container node reference.
307 */
308 const getSvgSymbolContainer = () => {
309
310 /* return svg symbol container node reference */
311 return refs.has(CONTAINER_REF_ID) ? refs.get(CONTAINER_REF_ID) : createSvgSymbolContainer();
312
313 };
314
315 /**
316 * Create document fragment from string representation of node.
317 * @param {String} string - String representation of node.
318 * @returns {DocumentFragment} Document fragment created from string representation of node.
319 */
320 const createNode = (string = "") => {
321
322 /* throw error if string argument is missing */
323 if(!string) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [string]`);
324
325 /* cast string argument to string */
326 string = string.toString().trim();
327
328 /* throw error if string argument is not valid */
329 if(!string.startsWith("<") || !string.endsWith(">")) throw new TypeError(`[${PACKAGE_NAME}] Argument is not valid! [string="${string}"]`);
330
331 /* remove unncessary whitespace from string argument */
332 string = string.replace(REGEXP_TEMPLATE_LITERALS_WHITESPACE, "");
333
334 /* return document fragment created from string argument */
335 return document.createRange().createContextualFragment(string);
336
337 };
338
339 /**
340 * Replace node with new node.
341 * @param {HTMLElement} node - Node.
342 * @param {HTMLElement|DocumentFragment} newNode - New node.
343 * @returns {*}
344 */
345 const replaceNode = (node = null, newNode = null) => {
346
347 /* throw error if node argument is missing */
348 if(!node) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [node]`);
349
350 /* throw error if newNode argument is missing */
351 if(!newNode) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [newNode]`);
352
353 /* throw error if node argument is missing parentNode property */
354 if(!node.parentNode) throw new Error(`[${PACKAGE_NAME}] Required property is missing! [node.parentNode]`);
355
356 /* replace node with new node */
357 node.parentNode.replaceChild(newNode, node);
358
359 };
360
361 /**
362 * Create attribute map from string representation of node.
363 * @param {String} string - String representation of node.
364 * @returns {Map} Attribute map.
365 */
366 const createAttributeMapFromString = (string = "") => {
367
368 /* throw error if string argument is missing */
369 if(!string) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [string]`);
370
371 /* cast string argument to string */
372 string = string.toString().trim();
373
374 /* create empty attribute map */
375 const attributes = new Map;
376
377 /* set last index of regexp */
378 REGEXP_ATTRIBUTES.lastIndex = 0;
379
380 /* parse attributes into attribute map */
381 let attribute;
382 while(attribute = REGEXP_ATTRIBUTES.exec(string)) { // eslint-disable-line no-cond-assign
383
384 /* check and fix last index of regexp */
385 if(attribute.index === REGEXP_ATTRIBUTES.lastIndex) REGEXP_ATTRIBUTES.lastIndex++;
386
387 /* store attribute name reference */
388 const name = (attribute[1] || "").trim().toLowerCase();
389
390 /* skip loop if attribute name is not set or if it is tag */
391 if(!name || name.startsWith("<") || name.endsWith(">")) continue;
392
393 /* throw error if attribute name is not valid */
394 if(!REGEXP_ATTRIBUTE_NAME.test(name)) throw new TypeError(`[${PACKAGE_NAME}] Attribute name is not valid! [attribute="${name}"]`);
395
396 /* store attribute value reference */
397 const value = (attribute[2] || attribute[3] || "").trim();
398
399 /* store attribute in attribute map and handle xhtml transformation if xhtml option is enabled */
400 attributes.set(name, value ? value : (options.xhtml ? name : ""));
401
402 }
403
404 /* return attribute map */
405 return attributes;
406
407 };
408
409 /**
410 * Create attribute map from named node attribute map.
411 * @param {NamedNodeMap} namedNodeAttributeMap - Named node attribute map.
412 * @returns {Map} Attribute map.
413 */
414 const createAttributeMapFromNamedNodeMap = (namedNodeAttributeMap = null) => {
415
416 /* throw error if namedNodeAttributeMap argument is missing */
417 if(!namedNodeAttributeMap) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [namedNodeAttributeMap]`);
418
419 /* throw error if path argument is not valid */
420 if(!(namedNodeAttributeMap instanceof NamedNodeMap)) throw new TypeError(`[${PACKAGE_NAME}] Argument is not valid! [namedNodeAttributeMap]`);
421
422 /* transform named node attribute map into attribute map */
423 const attributes = new Map([ ...namedNodeAttributeMap ].map(({ name, value }) => {
424
425 /* parse attribute name */
426 name = (name || "").trim().toLowerCase();
427
428 /* throw error if attribute name is not valid */
429 if(!REGEXP_ATTRIBUTE_NAME.test(name)) throw new TypeError(`[${PACKAGE_NAME}] Attribute name is not valid! [attribute="${name}"]`);
430
431 /* parse attribute value */
432 value = (value || "").trim();
433
434 /* return array of attribute name and attribute value and handle xhtml transformation if xhtml option is enabled */
435 return [ name, value ? value : (options.xhtml ? name : "") ];
436
437 }));
438
439 /* return attribute map */
440 return attributes;
441
442 };
443
444 /**
445 * Fetch SVG file and create SVG file object.
446 * @param {String} path - Path to SVG file.
447 * @returns {Promise<Object>} SVG file object.
448 */
449 const fetchSvgFile = (path = "") => {
450
451 /* throw error if fetch and axios are not available */
452 if(!options._fetch && !options._axios) throw new Error(`[${PACKAGE_NAME}] Feature is not supported by browser! [fetch || axios]`);
453
454 /* throw error if path argument is missing */
455 if(!path) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [path]`);
456
457 /* cast path argument to string */
458 path = path.toString().trim();
459
460 /* throw error if path argument is not valid */
461 if(!REGEXP_SVG_FILENAME.test(path)) throw new TypeError(`[${PACKAGE_NAME}] Argument is not valid! [path="${path}"]`);
462
463 /* return promise */
464 return new Promise((resolve, reject) => {
465
466 /* create svg file object and store svg file path in it */
467 const file = { path };
468
469 /* resolve svg file object if it is already defined in cache map */
470 if(cache.has(file.path)) {
471 file.content = cache.get(file.path);
472 return resolve(file);
473 }
474
475 /* fetch svg file */
476 (options._axios ? options.axios.get : fetch)(file.path)
477
478 /* validate response status and return response data as string */
479 .then(response => {
480
481 /* throw error if response status is wrong */
482 if(!CORRECT_RESPONSE_STATUSES.has(response.status | 0)) throw new Error(`Wrong response status! [response.status=${response.status}]`); // PACKAGE_NAME prefix is not required here, it will be added in reject handler.
483
484 /* return response data as string */
485 return options._axios ? response.data.toString() : response.text();
486
487 })
488
489 /* store and resolve svg file object */
490 .then(content => {
491
492 /* store svg file content in svg file object */
493 file.content = content.trim();
494
495 /* store svg file object in cache map */
496 cache.set(file.path, file.content);
497
498 /* store cache map in local storage */
499 if(options._storage && options.cache.persistent) localStorage.setItem(CACHE_ID, JSON.stringify([ ...cache ]));
500
501 /* resolve svg file object */
502 return resolve(file);
503
504 })
505
506 /* catch errors */
507 .catch(reject);
508
509 });
510
511 };
512
513 /**
514 * Parse SVG file object according to image node.
515 * @param {Object} file - SVG file object.
516 * @param {HTMLImageElement} node - Image node.
517 * @returns {String} String representation of SVG node.
518 */
519 const parseSvgFile = (file = null, node = null) => {
520
521 /* throw error if file argument is missing */
522 if(!file) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [file]`);
523
524 /* throw error if node argument is missing */
525 if(!node) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [node]`);
526
527 /* throw error if file argument is missing path property */
528 if(!file.path) throw new Error(`[${PACKAGE_NAME}] Required property is missing! [file.path]`);
529
530 /* cast path property of file argument to string */
531 file.path = file.path.toString().trim();
532
533 /* throw error if path property of file argument is not valid */
534 if(!REGEXP_SVG_FILENAME.test(file.path)) throw new TypeError(`[${PACKAGE_NAME}] Argument property is not valid! [file.path="${file.path}"]`);
535
536 /* throw error if file argument is missing content property */
537 if(!file.content) throw new Error(`[${PACKAGE_NAME}] Required property is missing! [file.content]`);
538
539 /* cast content property of file argument to string */
540 file.content = file.content.toString().trim();
541
542 /* throw error if content property of file argument is not valid */
543 if(!REGEXP_SVG_CONTENT.test(file.content)) throw new TypeError(`[${PACKAGE_NAME}] Argument property is not valid! [file.content="${file.content}"]`);
544
545 /* throw error if node argument is missing outerHTML property */
546 if(!node.outerHTML) throw new Error(`[${PACKAGE_NAME}] Required property is missing! [node.outerHTML]`);
547
548 /* check if image node should be handled as svg inline sprite */
549 if(node[FLAGS_ID].has("sprite")) {
550
551 /* replace svg file content with symbol usage reference, which will be defined in svg symbol container node */
552 file.content = file.content.replace(REGEXP_SVG_CONTENT, (svg, attributes, symbol) => { // eslint-disable-line no-unused-vars
553
554 /* check if requested svg file path is already defined in symbol set */
555 const symbolAlreadyDefined = symbols.has(file.path);
556
557 /* generate id for symbol */
558 const id = `${SYMBOL_ID}-${symbolAlreadyDefined ? [ ...symbols ].indexOf(file.path) : symbols.size}`;
559
560 /* create new symbol if symbol is not defined in symbol set */
561 if(!symbolAlreadyDefined) {
562
563 /* create new symbol node */
564 const symbolNode = createNode(`
565 <svg xmlns="http://www.w3.org/2000/svg">
566 <symbol id="${id}"${attributes}>
567 ${symbol}
568 </symbol>
569 </svg>
570 `);
571
572 /* add new symbol node into svg symbol container node */
573 getSvgSymbolContainer().appendChild(symbolNode.firstChild.firstChild);
574
575 /* store svg file path in symbol set */
576 symbols.add(file.path);
577
578 }
579
580 /* return symbol node usage reference */
581 return `
582 <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"${options.attributes.clone.size && (attributes = createAttributeMapFromString(attributes)) ? ` ${[ ...options.attributes.clone ].filter(attribute => !!attribute && attributes.has(attribute)).map(attribute => `${attribute}="${attributes.get(attribute)}"`).join(" ")}` : "" }>
583 <use xlink:href="#${id}" href="#${id}"></use>
584 </svg>
585 `;
586
587 });
588
589 }
590
591 /* inject attributes from attribute map into svg file content */
592 return file.content.replace(REGEXP_SVG_CONTENT, (svg, attributes, symbol) => { // eslint-disable-line no-unused-vars
593
594 /* extract attribute maps */
595 const fileAttributes = createAttributeMapFromString(attributes); // svg
596 const nodeAttributes = createAttributeMapFromNamedNodeMap(node.attributes); // img
597
598 /* merge attribute maps */
599 attributes = new Map([ ...fileAttributes, ...nodeAttributes ]);
600
601 /* store attribute names reference for attributes that should have unique values */
602 const uniqueAttributeValues = new Set([ "class" ]);
603
604 /* loop over all attributes to merge */
605 for(const attribute of options.attributes.merge) {
606
607 /* extract attribute values */
608 const fileValues = fileAttributes.has(attribute) ? fileAttributes.get(attribute).split(REGEXP_WHITESPACE).filter(value => !!value) : []; // svg
609 const nodeValues = nodeAttributes.has(attribute) ? nodeAttributes.get(attribute).split(REGEXP_WHITESPACE).filter(value => !!value) : []; // img
610
611 /* skip loop if xhtml option is enabled and there are not any values */
612 if(options.xhtml && !fileValues.length && !nodeValues.length) continue;
613
614 /* merge attribute values */
615 const values = [ ...fileValues, ...nodeValues ];
616
617 /* set attribute values into attribute map */
618 attributes.set(attribute, (uniqueAttributeValues.has(attribute) ? [ ...new Set(values) ] : values).join(" ").trim());
619
620 }
621
622 /* loop over all attributes to add */
623 for(const attribute of options.attributes.add) {
624
625 /* extract attribute values */
626 let values = attribute.value.split(REGEXP_WHITESPACE).filter(value => !!value);
627
628 /* check if attribute is already defined in attribute map */
629 if(attributes.has(attribute.name)) {
630
631 /* throw error if attribute to add already exists and can not be merged */
632 if(!options.attributes.merge.has(attribute.name)) throw new Error(`[${PACKAGE_NAME}] Can not add attribute, attribute already exists. [${attribute.name}]`);
633
634 /* extract attribute values */
635 const oldValues = attributes.get(attribute.name).split(REGEXP_WHITESPACE).filter(value => !!value);
636
637 /* skip loop if xhtml option is enabled and there are not any values */
638 if(options.xhtml && !values.length && !oldValues.length) continue;
639
640 /* merge attribute values */
641 values = [ ...oldValues, ...values ];
642
643 }
644
645 /* set attribute values into attribute map */
646 attributes.set(attribute.name, (uniqueAttributeValues.has(attribute.name) ? [ ...new Set(values) ] : values).join(" ").trim());
647
648 }
649
650 /* loop over all attributes to transform into data-attributes */
651 for(const attribute of options.attributes.data) {
652
653 /* skip if attribute is not defined in attribute map */
654 if(!attributes.has(attribute)) continue;
655
656 /* extract attribute values */
657 let values = attributes.get(attribute).split(REGEXP_WHITESPACE).filter(value => !!value);
658
659 /* store data-attribute name reference */
660 const dataAttribute = `data-${attribute}`;
661
662 /* check if data-attribute is already defined in attribute map */
663 if(attributes.has(dataAttribute)) {
664
665 /* throw error if data-attribute already exists and can not be merged */
666 if(!options.attributes.merge.has(dataAttribute)) throw new Error(`[${PACKAGE_NAME}] Can not transform attribute to data-attribute, data-attribute already exists. [${attribute}]`);
667
668 /* extract data-attribute values */
669 const oldValues = attributes.get(dataAttribute).split(REGEXP_WHITESPACE).filter(value => !!value);
670
671 /* skip loop if xhtml option is enabled and there are not any values */
672 if(options.xhtml && !values.length && !oldValues.length) continue;
673
674 /* merge attribute values */
675 values = [ ...oldValues, ...values ];
676
677 }
678
679 /* set data-attribute values into attribute map */
680 attributes.set(dataAttribute, (uniqueAttributeValues.has(attribute) ? [ ...new Set(values) ] : values).join(" ").trim());
681
682 /* add attribute to remove from attribute map into options.attributes.remove set if there is not already present */
683 if(!options.attributes.remove.has(attribute)) options.attributes.remove.add(attribute);
684
685 }
686
687 /* loop over all attributes to remove */
688 for(const attribute of options.attributes.remove) {
689
690 /* skip if attribute is not defined in attribute map */
691 if(!attributes.has(attribute)) continue;
692
693 /* remove attribute from attribute map */
694 attributes.delete(attribute);
695
696 }
697
698 /* return string representation of svg node with injected attributes */
699 return `
700 <svg${attributes.size ? ` ${[ ...attributes.keys() ].filter(attribute => !!attribute).map(attribute => `${attribute}="${attributes.get(attribute)}"`).join(" ")}` : ""}>
701 ${symbol}
702 </svg>
703 `;
704
705 });
706
707 };
708
709 /**
710 * Process image node - replace image node with SVG node.
711 * @param {HTMLImageElement} node - Image node.
712 * @returns {*}
713 */
714 const processImageNode = (node = null) => {
715
716 /* throw error if node argument is missing */
717 if(!node) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [node]`);
718
719 /* throw error if node argument is missing data-src and src property */
720 if(!node.dataset.src && !node.src) throw new Error(`[${PACKAGE_NAME}] Required property is missing! [node.data-src || node.src]`);
721
722 /* cast data-src and src properties of node argument argument to strings if defined */
723 if(node.dataset.src) node.dataset.src = node.dataset.src.toString().trim();
724 if(node.src) node.src = node.src.toString().trim();
725
726 /* fetch svg file */
727 fetchSvgFile(node.dataset.src || node.src)
728
729 /* process svg file object */
730 .then(file => {
731
732 /* parse svg file object */
733 const svgString = parseSvgFile(file, node);
734
735 /* create svg node */
736 const svgNode = createNode(svgString);
737
738 /* replace image node with svg node */
739 replaceNode(node, svgNode);
740
741 })
742
743 /* catch errors */
744 .catch(error => console.error(`[${PACKAGE_NAME}] ${error.toString()}`)); // eslint-disable-line no-console
745
746 };
747
748 /**
749 * BeforeMount hook function for Vue directive.
750 * @param {HTMLImageElement} node - Node that is binded with directive.
751 * @param {Object} binding - Object containing directive properties.
752 * @param {VNode} vnode - Virtual node created by Vue compiler.
753 * @returns {*}
754 */
755 const beforeMount = (node = null, binding = null, vnode = null) => { // eslint-disable-line no-unused-vars
756
757 /* throw error if node argument is missing */
758 if(!node) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [node]`);
759
760 /* throw error if node argument is not valid */
761 if(node.tagName !== "IMG") throw new Error(`[${PACKAGE_NAME}] Required argument is not valid! [node]`);
762
763 /* throw error if vnode argument is missing */
764 if(!vnode) throw new Error(`[${PACKAGE_NAME}] Required argument is missing! [vnode]`);
765
766 /* create empty image node flag set if it is not already defined */
767 if(!node[FLAGS_ID]) node[FLAGS_ID] = new Set;
768
769 /* skip if image node is already processed */
770 if(node[FLAGS_ID].has("processed")) return;
771
772 /* set internal processed flag to image node */
773 node[FLAGS_ID].add("processed");
774
775 /* store vnode directives reference based on Vue version */
776 const directives = isVue3 ? vnode.dirs : vnode.data.directives;
777
778 /* throw error if image node has more than 1 directive */
779 if(directives.length > 1) throw new Error(`[${PACKAGE_NAME}] Node has more than 1 directive! [${isVue3 ? "vnode.dirs" : "vnode.data.directives"}]`);
780
781 /* set internal sprite flag to image node */
782 if(!!directives[0].modifiers[options.directive.spriteModifierName]) node[FLAGS_ID].add("sprite"); // eslint-disable-line no-extra-boolean-cast
783
784 /* disable lazy processing of image node if intersection observer is not available */
785 if(!options._observer && node.dataset.src) {
786
787 /* transform data-src attribute to src attribute of image node */
788 node.src = node.dataset.src;
789 delete node.dataset.src;
790
791 }
792
793 /* process image node */
794 if(node.dataset.src) getImageNodeIntersectionObserver().observe(node);
795 else processImageNode(node);
796
797 };
798
799 /* define vue svg inline directive */
800 VueOrApp.directive(options.directive.name, isVue3 ? { beforeMount } : { bind: beforeMount });
801
802};
803
804/* export Vue plugin */
805export default { install };