vue-svg-inline-plugin
Version:
Vue plugin for inline replacement of SVG images with actual content of SVG files.
806 lines (570 loc) • 30.9 kB
JavaScript
/**
* @author Oliver Findl
* @version 2.2.3
* @license MIT
*/
;
/* 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 expressions */
const REGEXP_SVG_FILENAME = /.+\.svg(?:[?#].*)?$/i;
const REGEXP_SVG_CONTENT = /<svg(\s+[^>]+)?>([\s\S]+)<\/svg>/i;
const REGEXP_ATTRIBUTES = /\s*([^\s=]+)[\s=]+(?:"([^"]*)"|'([^']*)')?\s*/g;
const REGEXP_ATTRIBUTE_NAME = /^[a-z](?:[a-z0-9-:]*[a-z0-9])?$/i;
const REGEXP_VUE_DIRECTIVE = /^v-/i;
const REGEXP_WHITESPACE = /\s+/g;
const REGEXP_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 {void}
*/
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" && !REGEXP_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(REGEXP_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(REGEXP_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 */
REGEXP_ATTRIBUTES.lastIndex = 0;
/* parse attributes into attribute map */
let attribute;
while(attribute = REGEXP_ATTRIBUTES.exec(string)) { // eslint-disable-line no-cond-assign
/* check and fix last index of regexp */
if(attribute.index === REGEXP_ATTRIBUTES.lastIndex) REGEXP_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(!REGEXP_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(!REGEXP_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(!REGEXP_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(!REGEXP_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(!REGEXP_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(REGEXP_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(REGEXP_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(REGEXP_WHITESPACE).filter(value => !!value) : []; // svg
const nodeValues = nodeAttributes.has(attribute) ? nodeAttributes.get(attribute).split(REGEXP_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(REGEXP_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(REGEXP_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(REGEXP_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(REGEXP_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 node argument is not valid */
if(node.tagName !== "IMG") throw new Error(`[${PACKAGE_NAME}] Required argument is not valid! [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 };