123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- 'use strict';
- const { selectAll, selectOne, is } = require('css-select');
- const svgoCssSelectAdapter = require('./css-select-adapter');
- const CSSClassList = require('./css-class-list');
- const CSSStyleDeclaration = require('./css-style-declaration');
- /**
- * @type {(name: string) => { prefix: string, local: string }}
- */
- const parseName = (name) => {
- if (name == null) {
- return {
- prefix: '',
- local: '',
- };
- }
- if (name === 'xmlns') {
- return {
- prefix: 'xmlns',
- local: '',
- };
- }
- const chunks = name.split(':');
- if (chunks.length === 1) {
- return {
- prefix: '',
- local: chunks[0],
- };
- }
- return {
- prefix: chunks[0],
- local: chunks[1],
- };
- };
- var cssSelectOpts = {
- xmlMode: true,
- adapter: svgoCssSelectAdapter,
- };
- const attrsHandler = {
- get: (attributes, name) => {
- // eslint-disable-next-line no-prototype-builtins
- if (attributes.hasOwnProperty(name)) {
- return {
- name,
- get value() {
- return attributes[name];
- },
- set value(value) {
- attributes[name] = value;
- },
- };
- }
- },
- set: (attributes, name, attr) => {
- attributes[name] = attr.value;
- return true;
- },
- };
- var JSAPI = function (data, parentNode) {
- Object.assign(this, data);
- if (this.type === 'element') {
- if (this.attributes == null) {
- this.attributes = {};
- }
- if (this.children == null) {
- this.children = [];
- }
- Object.defineProperty(this, 'class', {
- writable: true,
- configurable: true,
- value: new CSSClassList(this),
- });
- Object.defineProperty(this, 'style', {
- writable: true,
- configurable: true,
- value: new CSSStyleDeclaration(this),
- });
- Object.defineProperty(this, 'parentNode', {
- writable: true,
- value: parentNode,
- });
- // temporary attrs polyfill
- // TODO remove after migration
- const element = this;
- Object.defineProperty(this, 'attrs', {
- configurable: true,
- get() {
- return new Proxy(element.attributes, attrsHandler);
- },
- set(value) {
- const newAttributes = {};
- for (const attr of Object.values(value)) {
- newAttributes[attr.name] = attr.value;
- }
- element.attributes = newAttributes;
- },
- });
- }
- };
- module.exports = JSAPI;
- /**
- * Perform a deep clone of this node.
- *
- * @return {Object} element
- */
- JSAPI.prototype.clone = function () {
- const { children, ...nodeData } = this;
- // Deep-clone node data.
- const clonedNode = new JSAPI(JSON.parse(JSON.stringify(nodeData)), null);
- if (children) {
- clonedNode.children = children.map((child) => {
- const clonedChild = child.clone();
- clonedChild.parentNode = clonedNode;
- return clonedChild;
- });
- }
- return clonedNode;
- };
- /**
- * Determine if item is an element
- * (any, with a specific name or in a names array).
- *
- * @param {String|Array} [param] element name or names arrays
- * @return {Boolean}
- */
- JSAPI.prototype.isElem = function (param) {
- if (this.type !== 'element') {
- return false;
- }
- if (param == null) {
- return true;
- }
- if (Array.isArray(param)) {
- return param.includes(this.name);
- }
- return this.name === param;
- };
- /**
- * Renames an element
- *
- * @param {String} name new element name
- * @return {Object} element
- */
- JSAPI.prototype.renameElem = function (name) {
- if (name && typeof name === 'string') this.name = name;
- return this;
- };
- /**
- * Determine if element is empty.
- *
- * @return {Boolean}
- */
- JSAPI.prototype.isEmpty = function () {
- return !this.children || !this.children.length;
- };
- /**
- * Find the closest ancestor of the current element.
- * @param elemName
- *
- * @return {?Object}
- */
- JSAPI.prototype.closestElem = function (elemName) {
- var elem = this;
- while ((elem = elem.parentNode) && !elem.isElem(elemName));
- return elem;
- };
- /**
- * Changes children by removing elements and/or adding new elements.
- *
- * @param {Number} start Index at which to start changing the children.
- * @param {Number} n Number of elements to remove.
- * @param {Array|Object} [insertion] Elements to add to the children.
- * @return {Array} Removed elements.
- */
- JSAPI.prototype.spliceContent = function (start, n, insertion) {
- if (arguments.length < 2) return [];
- if (!Array.isArray(insertion))
- insertion = Array.apply(null, arguments).slice(2);
- insertion.forEach(function (inner) {
- inner.parentNode = this;
- }, this);
- return this.children.splice.apply(
- this.children,
- [start, n].concat(insertion)
- );
- };
- /**
- * Determine if element has an attribute
- * (any, or by name or by name + value).
- *
- * @param {String} [name] attribute name
- * @param {String} [val] attribute value (will be toString()'ed)
- * @return {Boolean}
- */
- JSAPI.prototype.hasAttr = function (name, val) {
- if (this.type !== 'element') {
- return false;
- }
- if (Object.keys(this.attributes).length === 0) {
- return false;
- }
- if (name == null) {
- return true;
- }
- // eslint-disable-next-line no-prototype-builtins
- if (this.attributes.hasOwnProperty(name) === false) {
- return false;
- }
- if (val !== undefined) {
- return this.attributes[name] === val.toString();
- }
- return true;
- };
- /**
- * Determine if element has an attribute by local name
- * (any, or by name or by name + value).
- *
- * @param {String} [localName] local attribute name
- * @param {Number|String|RegExp|Function} [val] attribute value (will be toString()'ed or executed, otherwise ignored)
- * @return {Boolean}
- */
- JSAPI.prototype.hasAttrLocal = function (localName, val) {
- if (!this.attrs || !Object.keys(this.attrs).length) return false;
- if (!arguments.length) return !!this.attrs;
- var callback;
- switch (val != null && val.constructor && val.constructor.name) {
- case 'Number': // same as String
- case 'String':
- callback = stringValueTest;
- break;
- case 'RegExp':
- callback = regexpValueTest;
- break;
- case 'Function':
- callback = funcValueTest;
- break;
- default:
- callback = nameTest;
- }
- return this.someAttr(callback);
- function nameTest(attr) {
- const { local } = parseName(attr.name);
- return local === localName;
- }
- function stringValueTest(attr) {
- const { local } = parseName(attr.name);
- return local === localName && val == attr.value;
- }
- function regexpValueTest(attr) {
- const { local } = parseName(attr.name);
- return local === localName && val.test(attr.value);
- }
- function funcValueTest(attr) {
- const { local } = parseName(attr.name);
- return local === localName && val(attr.value);
- }
- };
- /**
- * Get a specific attribute from an element
- * (by name or name + value).
- *
- * @param {String} name attribute name
- * @param {String} [val] attribute value (will be toString()'ed)
- * @return {Object|Undefined}
- */
- JSAPI.prototype.attr = function (name, val) {
- if (this.hasAttr(name, val)) {
- return this.attrs[name];
- }
- };
- /**
- * Get computed attribute value from an element
- *
- * @param {String} name attribute name
- * @return {Object|Undefined}
- */
- JSAPI.prototype.computedAttr = function (name, val) {
- if (!arguments.length) return;
- for (
- var elem = this;
- elem && (!elem.hasAttr(name) || !elem.attributes[name]);
- elem = elem.parentNode
- );
- if (val != null) {
- return elem ? elem.hasAttr(name, val) : false;
- } else if (elem && elem.hasAttr(name)) {
- return elem.attributes[name];
- }
- };
- /**
- * Remove a specific attribute.
- *
- * @param {String|Array} name attribute name
- * @param {String} [val] attribute value
- * @return {Boolean}
- */
- JSAPI.prototype.removeAttr = function (name, val) {
- if (this.type !== 'element') {
- return false;
- }
- if (arguments.length === 0) {
- return false;
- }
- if (Array.isArray(name)) {
- for (const nameItem of name) {
- this.removeAttr(nameItem, val);
- }
- return false;
- }
- if (this.hasAttr(name, val) === false) {
- return false;
- }
- delete this.attributes[name];
- return true;
- };
- /**
- * Add attribute.
- *
- * @param {Object} [attr={}] attribute object
- * @return {Object|Boolean} created attribute or false if no attr was passed in
- */
- JSAPI.prototype.addAttr = function (attr) {
- attr = attr || {};
- if (attr.name === undefined) return false;
- this.attributes[attr.name] = attr.value;
- if (attr.name === 'class') {
- // newly added class attribute
- this.class.addClassValueHandler();
- }
- if (attr.name === 'style') {
- // newly added style attribute
- this.style.addStyleValueHandler();
- }
- return this.attrs[attr.name];
- };
- /**
- * Iterates over all attributes.
- *
- * @param {Function} callback callback
- * @param {Object} [context] callback context
- * @return {Boolean} false if there are no any attributes
- */
- JSAPI.prototype.eachAttr = function (callback, context) {
- if (this.type !== 'element') {
- return false;
- }
- if (callback == null) {
- return false;
- }
- for (const attr of Object.values(this.attrs)) {
- callback.call(context, attr);
- }
- return true;
- };
- /**
- * Tests whether some attribute passes the test.
- *
- * @param {Function} callback callback
- * @param {Object} [context] callback context
- * @return {Boolean} false if there are no any attributes
- */
- JSAPI.prototype.someAttr = function (callback, context) {
- if (this.type !== 'element') {
- return false;
- }
- for (const attr of Object.values(this.attrs)) {
- if (callback.call(context, attr)) return true;
- }
- return false;
- };
- /**
- * Evaluate a string of CSS selectors against the element and returns matched elements.
- *
- * @param {String} selectors CSS selector(s) string
- * @return {Array} null if no elements matched
- */
- JSAPI.prototype.querySelectorAll = function (selectors) {
- var matchedEls = selectAll(selectors, this, cssSelectOpts);
- return matchedEls.length > 0 ? matchedEls : null;
- };
- /**
- * Evaluate a string of CSS selectors against the element and returns only the first matched element.
- *
- * @param {String} selectors CSS selector(s) string
- * @return {Array} null if no element matched
- */
- JSAPI.prototype.querySelector = function (selectors) {
- return selectOne(selectors, this, cssSelectOpts);
- };
- /**
- * Test if a selector matches a given element.
- *
- * @param {String} selector CSS selector string
- * @return {Boolean} true if element would be selected by selector string, false if it does not
- */
- JSAPI.prototype.matches = function (selector) {
- return is(this, selector, cssSelectOpts);
- };
|