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