795 lines
14 KiB
JavaScript
795 lines
14 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
function ensureImported(element, target) {
|
|
|
|
if (element.ownerDocument !== target.ownerDocument) {
|
|
try {
|
|
|
|
// may fail on webkit
|
|
return target.ownerDocument.importNode(element, true);
|
|
} catch (e) {
|
|
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* appendTo utility
|
|
*/
|
|
|
|
/**
|
|
* Append a node to a target element and return the appended node.
|
|
*
|
|
* @param {SVGElement} element
|
|
* @param {SVGElement} target
|
|
*
|
|
* @return {SVGElement} the appended node
|
|
*/
|
|
function appendTo(element, target) {
|
|
return target.appendChild(ensureImported(element, target));
|
|
}
|
|
|
|
/**
|
|
* append utility
|
|
*/
|
|
|
|
/**
|
|
* Append a node to an element
|
|
*
|
|
* @param {SVGElement} element
|
|
* @param {SVGElement} node
|
|
*
|
|
* @return {SVGElement} the element
|
|
*/
|
|
function append(target, node) {
|
|
appendTo(node, target);
|
|
return target;
|
|
}
|
|
|
|
/**
|
|
* attribute accessor utility
|
|
*/
|
|
|
|
var LENGTH_ATTR = 2;
|
|
|
|
var CSS_PROPERTIES = {
|
|
'alignment-baseline': 1,
|
|
'baseline-shift': 1,
|
|
'clip': 1,
|
|
'clip-path': 1,
|
|
'clip-rule': 1,
|
|
'color': 1,
|
|
'color-interpolation': 1,
|
|
'color-interpolation-filters': 1,
|
|
'color-profile': 1,
|
|
'color-rendering': 1,
|
|
'cursor': 1,
|
|
'direction': 1,
|
|
'display': 1,
|
|
'dominant-baseline': 1,
|
|
'enable-background': 1,
|
|
'fill': 1,
|
|
'fill-opacity': 1,
|
|
'fill-rule': 1,
|
|
'filter': 1,
|
|
'flood-color': 1,
|
|
'flood-opacity': 1,
|
|
'font': 1,
|
|
'font-family': 1,
|
|
'font-size': LENGTH_ATTR,
|
|
'font-size-adjust': 1,
|
|
'font-stretch': 1,
|
|
'font-style': 1,
|
|
'font-variant': 1,
|
|
'font-weight': 1,
|
|
'glyph-orientation-horizontal': 1,
|
|
'glyph-orientation-vertical': 1,
|
|
'image-rendering': 1,
|
|
'kerning': 1,
|
|
'letter-spacing': 1,
|
|
'lighting-color': 1,
|
|
'marker': 1,
|
|
'marker-end': 1,
|
|
'marker-mid': 1,
|
|
'marker-start': 1,
|
|
'mask': 1,
|
|
'opacity': 1,
|
|
'overflow': 1,
|
|
'pointer-events': 1,
|
|
'shape-rendering': 1,
|
|
'stop-color': 1,
|
|
'stop-opacity': 1,
|
|
'stroke': 1,
|
|
'stroke-dasharray': 1,
|
|
'stroke-dashoffset': 1,
|
|
'stroke-linecap': 1,
|
|
'stroke-linejoin': 1,
|
|
'stroke-miterlimit': 1,
|
|
'stroke-opacity': 1,
|
|
'stroke-width': LENGTH_ATTR,
|
|
'text-anchor': 1,
|
|
'text-decoration': 1,
|
|
'text-rendering': 1,
|
|
'unicode-bidi': 1,
|
|
'visibility': 1,
|
|
'word-spacing': 1,
|
|
'writing-mode': 1
|
|
};
|
|
|
|
|
|
function getAttribute(node, name) {
|
|
if (CSS_PROPERTIES[name]) {
|
|
return node.style[name];
|
|
} else {
|
|
return node.getAttributeNS(null, name);
|
|
}
|
|
}
|
|
|
|
function setAttribute(node, name, value) {
|
|
var hyphenated = name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
|
|
var type = CSS_PROPERTIES[hyphenated];
|
|
|
|
if (type) {
|
|
|
|
// append pixel unit, unless present
|
|
if (type === LENGTH_ATTR && typeof value === 'number') {
|
|
value = String(value) + 'px';
|
|
}
|
|
|
|
node.style[hyphenated] = value;
|
|
} else {
|
|
node.setAttributeNS(null, name, value);
|
|
}
|
|
}
|
|
|
|
function setAttributes(node, attrs) {
|
|
|
|
var names = Object.keys(attrs), i, name;
|
|
|
|
for (i = 0, name; (name = names[i]); i++) {
|
|
setAttribute(node, name, attrs[name]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets or sets raw attributes on a node.
|
|
*
|
|
* @param {SVGElement} node
|
|
* @param {Object} [attrs]
|
|
* @param {String} [name]
|
|
* @param {String} [value]
|
|
*
|
|
* @return {String}
|
|
*/
|
|
function attr(node, name, value) {
|
|
if (typeof name === 'string') {
|
|
if (value !== undefined) {
|
|
setAttribute(node, name, value);
|
|
} else {
|
|
return getAttribute(node, name);
|
|
}
|
|
} else {
|
|
setAttributes(node, name);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
/**
|
|
* Taken from https://github.com/component/classes
|
|
*
|
|
* Without the component bits.
|
|
*/
|
|
|
|
/**
|
|
* toString reference.
|
|
*/
|
|
|
|
const toString = Object.prototype.toString;
|
|
|
|
/**
|
|
* Wrap `el` in a `ClassList`.
|
|
*
|
|
* @param {Element} el
|
|
* @return {ClassList}
|
|
* @api public
|
|
*/
|
|
|
|
function classes(el) {
|
|
return new ClassList(el);
|
|
}
|
|
|
|
function ClassList(el) {
|
|
if (!el || !el.nodeType) {
|
|
throw new Error('A DOM element reference is required');
|
|
}
|
|
this.el = el;
|
|
this.list = el.classList;
|
|
}
|
|
|
|
/**
|
|
* Add class `name` if not already present.
|
|
*
|
|
* @param {String} name
|
|
* @return {ClassList}
|
|
* @api public
|
|
*/
|
|
|
|
ClassList.prototype.add = function(name) {
|
|
this.list.add(name);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove class `name` when present, or
|
|
* pass a regular expression to remove
|
|
* any which match.
|
|
*
|
|
* @param {String|RegExp} name
|
|
* @return {ClassList}
|
|
* @api public
|
|
*/
|
|
|
|
ClassList.prototype.remove = function(name) {
|
|
if ('[object RegExp]' == toString.call(name)) {
|
|
return this.removeMatching(name);
|
|
}
|
|
|
|
this.list.remove(name);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove all classes matching `re`.
|
|
*
|
|
* @param {RegExp} re
|
|
* @return {ClassList}
|
|
* @api private
|
|
*/
|
|
|
|
ClassList.prototype.removeMatching = function(re) {
|
|
const arr = this.array();
|
|
for (let i = 0; i < arr.length; i++) {
|
|
if (re.test(arr[i])) {
|
|
this.remove(arr[i]);
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Toggle class `name`, can force state via `force`.
|
|
*
|
|
* For browsers that support classList, but do not support `force` yet,
|
|
* the mistake will be detected and corrected.
|
|
*
|
|
* @param {String} name
|
|
* @param {Boolean} force
|
|
* @return {ClassList}
|
|
* @api public
|
|
*/
|
|
|
|
ClassList.prototype.toggle = function(name, force) {
|
|
if ('undefined' !== typeof force) {
|
|
if (force !== this.list.toggle(name, force)) {
|
|
this.list.toggle(name); // toggle again to correct
|
|
}
|
|
} else {
|
|
this.list.toggle(name);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Return an array of classes.
|
|
*
|
|
* @return {Array}
|
|
* @api public
|
|
*/
|
|
|
|
ClassList.prototype.array = function() {
|
|
return Array.from(this.list);
|
|
};
|
|
|
|
/**
|
|
* Check if class `name` is present.
|
|
*
|
|
* @param {String} name
|
|
* @return {ClassList}
|
|
* @api public
|
|
*/
|
|
|
|
ClassList.prototype.has =
|
|
ClassList.prototype.contains = function(name) {
|
|
return this.list.contains(name);
|
|
};
|
|
|
|
function remove(element) {
|
|
var parent = element.parentNode;
|
|
|
|
if (parent) {
|
|
parent.removeChild(element);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* Clear utility
|
|
*/
|
|
|
|
/**
|
|
* Removes all children from the given element
|
|
*
|
|
* @param {DOMElement} element
|
|
* @return {DOMElement} the element (for chaining)
|
|
*/
|
|
function clear(element) {
|
|
var child;
|
|
|
|
while ((child = element.firstChild)) {
|
|
remove(child);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
function clone(element) {
|
|
return element.cloneNode(true);
|
|
}
|
|
|
|
var ns = {
|
|
svg: 'http://www.w3.org/2000/svg'
|
|
};
|
|
|
|
/**
|
|
* DOM parsing utility
|
|
*/
|
|
|
|
var SVG_START = '<svg xmlns="' + ns.svg + '"';
|
|
|
|
function parse(svg) {
|
|
|
|
var unwrap = false;
|
|
|
|
// ensure we import a valid svg document
|
|
if (svg.substring(0, 4) === '<svg') {
|
|
if (svg.indexOf(ns.svg) === -1) {
|
|
svg = SVG_START + svg.substring(4);
|
|
}
|
|
} else {
|
|
|
|
// namespace svg
|
|
svg = SVG_START + '>' + svg + '</svg>';
|
|
unwrap = true;
|
|
}
|
|
|
|
var parsed = parseDocument(svg);
|
|
|
|
if (!unwrap) {
|
|
return parsed;
|
|
}
|
|
|
|
var fragment = document.createDocumentFragment();
|
|
|
|
var parent = parsed.firstChild;
|
|
|
|
while (parent.firstChild) {
|
|
fragment.appendChild(parent.firstChild);
|
|
}
|
|
|
|
return fragment;
|
|
}
|
|
|
|
function parseDocument(svg) {
|
|
|
|
var parser;
|
|
|
|
// parse
|
|
parser = new DOMParser();
|
|
parser.async = false;
|
|
|
|
return parser.parseFromString(svg, 'text/xml');
|
|
}
|
|
|
|
/**
|
|
* Create utility for SVG elements
|
|
*/
|
|
|
|
|
|
/**
|
|
* Create a specific type from name or SVG markup.
|
|
*
|
|
* @param {String} name the name or markup of the element
|
|
* @param {Object} [attrs] attributes to set on the element
|
|
*
|
|
* @returns {SVGElement}
|
|
*/
|
|
function create(name, attrs) {
|
|
var element;
|
|
|
|
if (name.charAt(0) === '<') {
|
|
element = parse(name).firstChild;
|
|
element = document.importNode(element, true);
|
|
} else {
|
|
element = document.createElementNS(ns.svg, name);
|
|
}
|
|
|
|
if (attrs) {
|
|
attr(element, attrs);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
/**
|
|
* Events handling utility
|
|
*/
|
|
|
|
function on(node, event, listener, useCapture) {
|
|
node.addEventListener(event, listener, useCapture);
|
|
}
|
|
|
|
function off(node, event, listener, useCapture) {
|
|
node.removeEventListener(event, listener, useCapture);
|
|
}
|
|
|
|
/**
|
|
* Geometry helpers
|
|
*/
|
|
|
|
// fake node used to instantiate svg geometry elements
|
|
var node = null;
|
|
|
|
function getNode() {
|
|
if (node === null) {
|
|
node = create('svg');
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
function extend(object, props) {
|
|
var i, k, keys = Object.keys(props);
|
|
|
|
for (i = 0; (k = keys[i]); i++) {
|
|
object[k] = props[k];
|
|
}
|
|
|
|
return object;
|
|
}
|
|
|
|
|
|
function createPoint(x, y) {
|
|
var point = getNode().createSVGPoint();
|
|
|
|
switch (arguments.length) {
|
|
case 0:
|
|
return point;
|
|
case 2:
|
|
x = {
|
|
x: x,
|
|
y: y
|
|
};
|
|
break;
|
|
}
|
|
|
|
return extend(point, x);
|
|
}
|
|
|
|
/**
|
|
* Create matrix via args.
|
|
*
|
|
* @example
|
|
*
|
|
* createMatrix({ a: 1, b: 1 });
|
|
* createMatrix();
|
|
* createMatrix(1, 2, 0, 0, 30, 20);
|
|
*
|
|
* @return {SVGMatrix}
|
|
*/
|
|
function createMatrix(a, b, c, d, e, f) {
|
|
var matrix = getNode().createSVGMatrix();
|
|
|
|
switch (arguments.length) {
|
|
case 0:
|
|
return matrix;
|
|
case 1:
|
|
return extend(matrix, a);
|
|
case 6:
|
|
return extend(matrix, {
|
|
a: a,
|
|
b: b,
|
|
c: c,
|
|
d: d,
|
|
e: e,
|
|
f: f
|
|
});
|
|
}
|
|
}
|
|
|
|
function createTransform(matrix) {
|
|
if (matrix) {
|
|
return getNode().createSVGTransformFromMatrix(matrix);
|
|
} else {
|
|
return getNode().createSVGTransform();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serialization util
|
|
*/
|
|
|
|
var TEXT_ENTITIES = /([&<>]{1})/g;
|
|
var ATTR_ENTITIES = /([\n\r"]{1})/g;
|
|
|
|
var ENTITY_REPLACEMENT = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '\''
|
|
};
|
|
|
|
function escape(str, pattern) {
|
|
|
|
function replaceFn(match, entity) {
|
|
return ENTITY_REPLACEMENT[entity] || entity;
|
|
}
|
|
|
|
return str.replace(pattern, replaceFn);
|
|
}
|
|
|
|
function serialize(node, output) {
|
|
|
|
var i, len, attrMap, attrNode, childNodes;
|
|
|
|
switch (node.nodeType) {
|
|
|
|
// TEXT
|
|
case 3:
|
|
|
|
// replace special XML characters
|
|
output.push(escape(node.textContent, TEXT_ENTITIES));
|
|
break;
|
|
|
|
// ELEMENT
|
|
case 1:
|
|
output.push('<', node.tagName);
|
|
|
|
if (node.hasAttributes()) {
|
|
attrMap = node.attributes;
|
|
for (i = 0, len = attrMap.length; i < len; ++i) {
|
|
attrNode = attrMap.item(i);
|
|
output.push(' ', attrNode.name, '="', escape(attrNode.value, ATTR_ENTITIES), '"');
|
|
}
|
|
}
|
|
|
|
if (node.hasChildNodes()) {
|
|
output.push('>');
|
|
childNodes = node.childNodes;
|
|
for (i = 0, len = childNodes.length; i < len; ++i) {
|
|
serialize(childNodes.item(i), output);
|
|
}
|
|
output.push('</', node.tagName, '>');
|
|
} else {
|
|
output.push('/>');
|
|
}
|
|
break;
|
|
|
|
// COMMENT
|
|
case 8:
|
|
output.push('<!--', escape(node.nodeValue, TEXT_ENTITIES), '-->');
|
|
break;
|
|
|
|
// CDATA
|
|
case 4:
|
|
output.push('<![CDATA[', node.nodeValue, ']]>');
|
|
break;
|
|
|
|
default:
|
|
throw new Error('unable to handle node ' + node.nodeType);
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/**
|
|
* innerHTML like functionality for SVG elements.
|
|
* based on innerSVG (https://code.google.com/p/innersvg)
|
|
*/
|
|
|
|
|
|
function set(element, svg) {
|
|
|
|
var parsed = parse(svg);
|
|
|
|
// clear element contents
|
|
clear(element);
|
|
|
|
if (!svg) {
|
|
return;
|
|
}
|
|
|
|
if (!isFragment(parsed)) {
|
|
|
|
// extract <svg> from parsed document
|
|
parsed = parsed.documentElement;
|
|
}
|
|
|
|
var nodes = slice(parsed.childNodes);
|
|
|
|
// import + append each node
|
|
for (var i = 0; i < nodes.length; i++) {
|
|
appendTo(nodes[i], element);
|
|
}
|
|
|
|
}
|
|
|
|
function get(element) {
|
|
var child = element.firstChild,
|
|
output = [];
|
|
|
|
while (child) {
|
|
serialize(child, output);
|
|
child = child.nextSibling;
|
|
}
|
|
|
|
return output.join('');
|
|
}
|
|
|
|
function isFragment(node) {
|
|
return node.nodeName === '#document-fragment';
|
|
}
|
|
|
|
function innerSVG(element, svg) {
|
|
|
|
if (svg !== undefined) {
|
|
|
|
try {
|
|
set(element, svg);
|
|
} catch (e) {
|
|
throw new Error('error parsing SVG: ' + e.message);
|
|
}
|
|
|
|
return element;
|
|
} else {
|
|
return get(element);
|
|
}
|
|
}
|
|
|
|
|
|
function slice(arr) {
|
|
return Array.prototype.slice.call(arr);
|
|
}
|
|
|
|
/**
|
|
* Selection utilities
|
|
*/
|
|
|
|
function select(node, selector) {
|
|
return node.querySelector(selector);
|
|
}
|
|
|
|
function selectAll(node, selector) {
|
|
var nodes = node.querySelectorAll(selector);
|
|
|
|
return [].map.call(nodes, function(element) {
|
|
return element;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* prependTo utility
|
|
*/
|
|
|
|
/**
|
|
* Prepend a node to a target element and return the prepended node.
|
|
*
|
|
* @param {SVGElement} node
|
|
* @param {SVGElement} target
|
|
*
|
|
* @return {SVGElement} the prepended node
|
|
*/
|
|
function prependTo(node, target) {
|
|
return target.insertBefore(ensureImported(node, target), target.firstChild || null);
|
|
}
|
|
|
|
/**
|
|
* prepend utility
|
|
*/
|
|
|
|
/**
|
|
* Prepend a node to a target element
|
|
*
|
|
* @param {SVGElement} target
|
|
* @param {SVGElement} node
|
|
*
|
|
* @return {SVGElement} the target element
|
|
*/
|
|
function prepend(target, node) {
|
|
prependTo(node, target);
|
|
return target;
|
|
}
|
|
|
|
/**
|
|
* Replace utility
|
|
*/
|
|
|
|
function replace(element, replacement) {
|
|
element.parentNode.replaceChild(ensureImported(replacement, element), element);
|
|
return replacement;
|
|
}
|
|
|
|
/**
|
|
* transform accessor utility
|
|
*/
|
|
|
|
function wrapMatrix(transformList, transform) {
|
|
if (transform instanceof SVGMatrix) {
|
|
return transformList.createSVGTransformFromMatrix(transform);
|
|
}
|
|
|
|
return transform;
|
|
}
|
|
|
|
|
|
function setTransforms(transformList, transforms) {
|
|
var i, t;
|
|
|
|
transformList.clear();
|
|
|
|
for (i = 0; (t = transforms[i]); i++) {
|
|
transformList.appendItem(wrapMatrix(transformList, t));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get or set the transforms on the given node.
|
|
*
|
|
* @param {SVGElement} node
|
|
* @param {SVGTransform|SVGMatrix|Array<SVGTransform|SVGMatrix>} [transforms]
|
|
*
|
|
* @return {SVGTransform} the consolidated transform
|
|
*/
|
|
function transform(node, transforms) {
|
|
var transformList = node.transform.baseVal;
|
|
|
|
if (transforms) {
|
|
|
|
if (!Array.isArray(transforms)) {
|
|
transforms = [ transforms ];
|
|
}
|
|
|
|
setTransforms(transformList, transforms);
|
|
}
|
|
|
|
return transformList.consolidate();
|
|
}
|
|
|
|
exports.append = append;
|
|
exports.appendTo = appendTo;
|
|
exports.attr = attr;
|
|
exports.classes = classes;
|
|
exports.clear = clear;
|
|
exports.clone = clone;
|
|
exports.create = create;
|
|
exports.createMatrix = createMatrix;
|
|
exports.createPoint = createPoint;
|
|
exports.createTransform = createTransform;
|
|
exports.innerSVG = innerSVG;
|
|
exports.off = off;
|
|
exports.on = on;
|
|
exports.prepend = prepend;
|
|
exports.prependTo = prependTo;
|
|
exports.remove = remove;
|
|
exports.replace = replace;
|
|
exports.select = select;
|
|
exports.selectAll = selectAll;
|
|
exports.transform = transform;
|