You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
989 lines
24 KiB
989 lines
24 KiB
1 year ago
|
/*
|
||
|
** Zabbix
|
||
|
** Copyright (C) 2001-2023 Zabbix SIA
|
||
|
**
|
||
|
** This program is free software; you can redistribute it and/or modify
|
||
|
** it under the terms of the GNU General Public License as published by
|
||
|
** the Free Software Foundation; either version 2 of the License, or
|
||
|
** (at your option) any later version.
|
||
|
**
|
||
|
** This program is distributed in the hope that it will be useful,
|
||
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
** GNU General Public License for more details.
|
||
|
**
|
||
|
** You should have received a copy of the GNU General Public License
|
||
|
** along with this program; if not, write to the Free Software
|
||
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||
|
**/
|
||
|
|
||
|
|
||
|
/**
|
||
|
* SVGCanvas class.
|
||
|
*
|
||
|
* Implements basic functionality needed to render SVG from JS.
|
||
|
*
|
||
|
* @param {object} options Canvas options.
|
||
|
* @param {number} options.width Canvas width (width attribute of a SVG image).
|
||
|
* @param {number} options.height Canvas height (height attribute of a SVG image).
|
||
|
* @param {boolean} options.mask Masking option for textarea elements (@see SVGCanvas.prototype.createTextarea)
|
||
|
* @param {boolean} shadowBuffer Shadow buffer (double buffering) support. If set to true, additional hidden
|
||
|
* group element is created within SVG.
|
||
|
*/
|
||
|
function SVGCanvas(options, shadowBuffer) {
|
||
|
this.options = options;
|
||
|
this.id = 0;
|
||
|
this.elements = [];
|
||
|
this.textPadding = 5;
|
||
|
this.maskColor = '#3d3d3d';
|
||
|
this.mask = false;
|
||
|
|
||
|
if (typeof options.mask !== 'undefined') {
|
||
|
this.mask = (options.mask === true);
|
||
|
}
|
||
|
if (typeof options.useViewBox !== 'boolean') {
|
||
|
options.useViewBox = false;
|
||
|
}
|
||
|
|
||
|
this.buffer = null;
|
||
|
|
||
|
var svg_options = options.useViewBox
|
||
|
? {
|
||
|
'viewBox': '0 0 ' + options.width + ' ' + options.height,
|
||
|
'style': 'max-width: ' + options.width + 'px; max-height: ' + options.height + 'px;',
|
||
|
'preserveAspectRatio': 'xMinYMin meet'
|
||
|
}
|
||
|
: {
|
||
|
'width': options.width,
|
||
|
'height': options.height
|
||
|
};
|
||
|
|
||
|
this.root = this.createElement('svg', svg_options, null);
|
||
|
if (shadowBuffer === true) {
|
||
|
this.buffer = this.root.add('g', {
|
||
|
class: 'shadow-buffer',
|
||
|
style: 'visibility: hidden;'
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Predefined namespaces for SVG as key => value
|
||
|
SVGCanvas.NAMESPACES = {
|
||
|
xlink: 'http://www.w3.org/1999/xlink'
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Generate unique id.
|
||
|
* Id is unique within page context.
|
||
|
*
|
||
|
* @return {number} Unique id.
|
||
|
*/
|
||
|
SVGCanvas.getUniqueId = function () {
|
||
|
if (typeof SVGCanvas.uniqueId === 'undefined') {
|
||
|
SVGCanvas.uniqueId = 0;
|
||
|
}
|
||
|
|
||
|
return SVGCanvas.uniqueId++;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Create new SVG element.
|
||
|
* Additional workaround is added to implement textarea element as a text element with a set of tspan subelements.
|
||
|
*
|
||
|
* @param {string} type Element type (SVG tag).
|
||
|
* @param {object} attributes Element attributes (SVG tag attributes) as key => value pairs.
|
||
|
* @param {SVGElement} parent Parent element if any (or null if none).
|
||
|
* @param {mixed} content Element textContent of a set of subelements.
|
||
|
*
|
||
|
* @return {SVGElement} Created element.
|
||
|
*/
|
||
|
SVGCanvas.prototype.createElement = function (type, attributes, parent, content) {
|
||
|
var element;
|
||
|
|
||
|
if(type.toLowerCase() === 'textarea') {
|
||
|
var textarea = new SVGTextArea(this);
|
||
|
element = textarea.create(attributes, parent, content);
|
||
|
}
|
||
|
else {
|
||
|
element = new SVGElement(this, type, attributes, parent, content);
|
||
|
this.elements.push(element);
|
||
|
}
|
||
|
|
||
|
return element;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get elements by specified attributes.
|
||
|
*
|
||
|
* SVG elements with specified attributes are returned as array of SVGElement (if any).
|
||
|
*
|
||
|
* @return {array} Elements that match specified attributes.
|
||
|
*/
|
||
|
SVGCanvas.prototype.getElementsByAttributes = function (attributes) {
|
||
|
var names = Object.keys(attributes),
|
||
|
elements = this.elements.filter(function (item) {
|
||
|
for (var i = 0; i < names.length; i++) {
|
||
|
if (item.attributes[names[i]] !== attributes[names[i]]) {
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
});
|
||
|
|
||
|
return elements;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Add element to the SVG root element (svg tag).
|
||
|
*
|
||
|
* @return {SVGElement} Created element.
|
||
|
*/
|
||
|
SVGCanvas.prototype.add = function (type, attributes, content) {
|
||
|
return this.root.add(type, attributes, content);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Attach SVG element to the specified container in DOM.
|
||
|
*
|
||
|
* @param {object} container DOM node.
|
||
|
*/
|
||
|
SVGCanvas.prototype.render = function (container) {
|
||
|
if (this.root.element.parentNode) {
|
||
|
this.root.element.parentNode.removeChild(this.root.element);
|
||
|
}
|
||
|
|
||
|
container.appendChild(this.root.element);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Resize canvas.
|
||
|
*
|
||
|
* @param {number} width New width.
|
||
|
* @param {number} height New height.
|
||
|
*
|
||
|
* @return {boolean} true if size is changed and false if size is the same as previous.
|
||
|
*/
|
||
|
SVGCanvas.prototype.resize = function (width, height) {
|
||
|
if (this.options.width !== width || this.options.height !== height) {
|
||
|
this.options.width = width;
|
||
|
this.options.height = height;
|
||
|
this.root.update({'width': width, 'height': height});
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* SVGTextArea class.
|
||
|
*
|
||
|
* Implements textarea (multiline text) for svg.
|
||
|
*
|
||
|
* @param {object} canvas Instance of SVGCanvas.
|
||
|
*
|
||
|
*/
|
||
|
function SVGTextArea(canvas) {
|
||
|
this.canvas = canvas;
|
||
|
this.element = null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse text line and extract links as <a> elements.
|
||
|
*
|
||
|
* @param {string} text Text line to be parsed.
|
||
|
*
|
||
|
* @return {mixed} Parsed text as {array} if links are present or as {string} if there are no links in text.
|
||
|
*/
|
||
|
SVGTextArea.parseLinks = function (text) {
|
||
|
var index,
|
||
|
offset = 0,
|
||
|
link,
|
||
|
parts = [];
|
||
|
|
||
|
while ((index = text.search(/((ftp|file|https?):\/\/[^\s]+)/i)) !== -1) {
|
||
|
if (offset !== index) {
|
||
|
parts.push(text.substring(offset, index));
|
||
|
}
|
||
|
|
||
|
text = text.substring(index);
|
||
|
index = text.search(/\s/);
|
||
|
|
||
|
if (index === -1) {
|
||
|
index = text.length;
|
||
|
}
|
||
|
|
||
|
link = text.substring(0, index);
|
||
|
text = text.substring(index);
|
||
|
offset = 0;
|
||
|
parts.push({
|
||
|
type: 'a',
|
||
|
attributes: {
|
||
|
href: link,
|
||
|
onclick: 'window.location = ' + JSON.stringify(link) + '; return false;' // Workaround for Safari.
|
||
|
},
|
||
|
content: link
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (text !== '') {
|
||
|
if (parts.length !== 0) {
|
||
|
parts.push(text);
|
||
|
}
|
||
|
else {
|
||
|
parts = text;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return parts;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Wrap text line to the specified width.
|
||
|
*
|
||
|
* @param {string} line Text line to be wrapped.
|
||
|
*
|
||
|
* @return {array} Wrapped line as {array} of strings.
|
||
|
*/
|
||
|
SVGTextArea.prototype.wrapLine = function (line) {
|
||
|
if (this.canvas.buffer === null || typeof this.clip === 'undefined') {
|
||
|
// No text wrapping without shadow buffer of clipping object.
|
||
|
return [line];
|
||
|
}
|
||
|
|
||
|
var max_width = this.clip.attributes.width,
|
||
|
current;
|
||
|
|
||
|
if (typeof max_width === 'undefined' && typeof this.clip.attributes.rx !== 'undefined') {
|
||
|
max_width = parseInt(this.clip.attributes.rx * 2, 10);
|
||
|
}
|
||
|
|
||
|
max_width -= this.canvas.textPadding * 2;
|
||
|
|
||
|
if (typeof this.canvas.wrapper === 'undefined') {
|
||
|
this.canvas.wrapper = {
|
||
|
text: this.canvas.buffer.add('text', this.attributes),
|
||
|
node: document.createTextNode('')
|
||
|
};
|
||
|
|
||
|
this.canvas.wrapper.text.element.appendChild(this.canvas.wrapper.node);
|
||
|
}
|
||
|
else {
|
||
|
this.canvas.wrapper.text.update(this.attributes);
|
||
|
}
|
||
|
|
||
|
var text = this.canvas.wrapper.text.element,
|
||
|
node = this.canvas.wrapper.node,
|
||
|
size,
|
||
|
wrapped = [];
|
||
|
|
||
|
node.textContent = line;
|
||
|
size = text.getBBox();
|
||
|
|
||
|
// Check length of the line in pixels.
|
||
|
if (Math.ceil(size.width) > max_width) {
|
||
|
var words = line.split(' ');
|
||
|
current = [];
|
||
|
|
||
|
while (words.length > 0) {
|
||
|
current.push(words.shift());
|
||
|
node.textContent = current.join(' ');
|
||
|
size = text.getBBox();
|
||
|
|
||
|
if (Math.ceil(size.width) > max_width) {
|
||
|
if (current.length > 1) {
|
||
|
words.unshift(current.pop());
|
||
|
wrapped.push(current.join(' '));
|
||
|
current = [];
|
||
|
}
|
||
|
else {
|
||
|
// Word is too long to fit the line.
|
||
|
wrapped.push(current.pop());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (current.length > 0) {
|
||
|
wrapped.push(current.join(' '));
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
wrapped.push(line);
|
||
|
}
|
||
|
|
||
|
return wrapped;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get horizontal offset (position in pixels) of text anchor.
|
||
|
*
|
||
|
* @return {numeric} Horizontal offset in pixels.
|
||
|
*/
|
||
|
SVGTextArea.prototype.getHorizontalOffset = function () {
|
||
|
switch (this.anchor.horizontal) {
|
||
|
case 'center':
|
||
|
return Math.floor(this.width/2);
|
||
|
|
||
|
case 'right':
|
||
|
return this.width;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Get text-anchor attribute value from horizontal anchor value.
|
||
|
*
|
||
|
* @return {string} Value of text-anchor attribute.
|
||
|
*/
|
||
|
SVGTextArea.prototype.getHorizontalAnchor = function() {
|
||
|
var mapping = {
|
||
|
left: 'start',
|
||
|
center: 'middle',
|
||
|
right: 'end'
|
||
|
};
|
||
|
|
||
|
if (typeof mapping[this.anchor.horizontal] === 'string') {
|
||
|
return mapping[this.anchor.horizontal];
|
||
|
}
|
||
|
|
||
|
return mapping.left;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Parse content, get the lines, perform line wrapping and link parsing.
|
||
|
*
|
||
|
* @param {mixed} content Text contents or array of line objects.
|
||
|
* @param {boolean} parse_links Set to true if link parsing should be performed.
|
||
|
*
|
||
|
* @return {numeric} Horizontal offset in pixels.
|
||
|
*/
|
||
|
SVGTextArea.prototype.parseContent = function(content, parse_links) {
|
||
|
var skip = 0.9,
|
||
|
anchor = this.getHorizontalAnchor();
|
||
|
|
||
|
this.lines = [];
|
||
|
|
||
|
if (typeof content === 'string') {
|
||
|
var items = [];
|
||
|
|
||
|
content.split("\n").forEach(function (line) {
|
||
|
items.push({
|
||
|
content: line,
|
||
|
attributes: {}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
content = items;
|
||
|
}
|
||
|
|
||
|
content.forEach(function (line) {
|
||
|
if (line.content.trim() !== '') {
|
||
|
var content = line.content.replace(/[\r\n]/g, '');
|
||
|
|
||
|
this.wrapLine(content).forEach(function (wrapped) {
|
||
|
if (parse_links === true) {
|
||
|
wrapped = SVGTextArea.parseLinks(wrapped);
|
||
|
}
|
||
|
|
||
|
this.lines.push({
|
||
|
type: 'tspan',
|
||
|
attributes: SVGElement.mergeAttributes({
|
||
|
x: this.offset,
|
||
|
dy: skip + 'em',
|
||
|
'text-anchor': anchor
|
||
|
}, line.attributes),
|
||
|
content: wrapped
|
||
|
});
|
||
|
|
||
|
skip = 1.2;
|
||
|
}, this);
|
||
|
}
|
||
|
else {
|
||
|
skip += 1.2;
|
||
|
}
|
||
|
}, this);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Align text position based on horizontal and vertical anchor values.
|
||
|
*/
|
||
|
SVGTextArea.prototype.alignToAnchor = function() {
|
||
|
if (typeof this.anchor !== 'object') {
|
||
|
this.anchor = {
|
||
|
horizontal: 'left'
|
||
|
};
|
||
|
}
|
||
|
|
||
|
this.x -= this.getHorizontalOffset();
|
||
|
|
||
|
switch (this.anchor.vertical) {
|
||
|
case 'middle':
|
||
|
this.y -= Math.floor(this.height/2);
|
||
|
break;
|
||
|
|
||
|
case 'bottom':
|
||
|
this.y -= this.height;
|
||
|
break;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Create clipping object to clip (and/or mask) text outside the specified shape.
|
||
|
*/
|
||
|
SVGTextArea.prototype.createClipping = function() {
|
||
|
if (typeof this.clip !== 'undefined') {
|
||
|
var offset = this.getHorizontalOffset();
|
||
|
// Clipping shape should be applied to the text. Clipping mode (clip or mask) depends on mask attribute.
|
||
|
|
||
|
if (typeof this.clip.attributes.x !== 'undefined' && typeof this.clip.attributes.y !== 'undefined') {
|
||
|
this.clip.attributes.x -= (this.x + offset);
|
||
|
this.clip.attributes.y -= this.y;
|
||
|
}
|
||
|
else if (typeof this.clip.attributes.cx !== 'undefined' && typeof this.clip.attributes.cy !== 'undefined') {
|
||
|
this.clip.attributes.cx -= (this.x + offset);
|
||
|
this.clip.attributes.cy -= this.y;
|
||
|
}
|
||
|
|
||
|
var unique_id = SVGCanvas.getUniqueId();
|
||
|
|
||
|
if (this.canvas.mask) {
|
||
|
this.clip.attributes.fill = '#ffffff';
|
||
|
this.element.add('mask', {
|
||
|
id: 'mask-' + unique_id
|
||
|
}, [{
|
||
|
type: 'rect',
|
||
|
attributes: {
|
||
|
x: -offset,
|
||
|
y: 0,
|
||
|
'width': this.width,
|
||
|
'height': this.height,
|
||
|
fill: this.canvas.maskColor
|
||
|
}
|
||
|
},
|
||
|
this.clip
|
||
|
]);
|
||
|
|
||
|
this.text.element.setAttribute('mask', 'url(#mask-' + unique_id + ')');
|
||
|
}
|
||
|
else {
|
||
|
this.element.add('clipPath', {
|
||
|
id: 'clip-' + unique_id
|
||
|
}, [this.clip]);
|
||
|
|
||
|
this.text.element.setAttribute('clip-path', 'url(#clip-' + unique_id + ')');
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Create new textarea element.
|
||
|
*
|
||
|
* Textarea element has poor support in supported browsers so following workaround is used. Textarea element is a text
|
||
|
* element with a set of tspan subelements and additional logic for text background and masking / clipping.
|
||
|
*
|
||
|
* @param {string} type Element type (SVG tag).
|
||
|
* @param {object} attributes Element attributes (SVG tag attributes).
|
||
|
* @param {number} attributes.x Element position on x axis.
|
||
|
* @param {number} attributes.y Element position on y axis.
|
||
|
* @param {object} attributes.anchor Anchor used for text placement.
|
||
|
* @param {string} attributes.anchor.horizontal Horizontal anchor used for text placement.
|
||
|
* @param {string} attributes.anchor.vertical Vertical anchor used for text placement.
|
||
|
* @param {object} attributes.background Attributes of rectangle placed behind text (text background).
|
||
|
* @param {object} attributes.clip SVG element used for clipping or masking (depends on canvas mask option).
|
||
|
* @param {SVGElement} parent Parent element if any (or null if none).
|
||
|
* @param {mixed} content Element textContent of a set of subelements.
|
||
|
*
|
||
|
* @return {SVGElement} Created element.
|
||
|
*/
|
||
|
SVGTextArea.prototype.create = function(attributes, parent, content) {
|
||
|
if (typeof content === 'string' && content.trim() === '') {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if (Array.isArray(content)) {
|
||
|
var i;
|
||
|
|
||
|
for (i = 0; i < content.length; i++) {
|
||
|
if (content[i].content.trim() !== '') {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (i === content.length) {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
['x', 'y', 'anchor', 'background', 'clip'].forEach(function (key) {
|
||
|
this[key] = attributes[key];
|
||
|
}, this);
|
||
|
|
||
|
this.offset = 0;
|
||
|
this.element = this.canvas.createElement('g', {}, parent);
|
||
|
|
||
|
var parse_links = attributes['parse-links'],
|
||
|
size;
|
||
|
|
||
|
['x', 'y', 'anchor', 'background', 'clip', 'parse-links'].forEach(function (key) {
|
||
|
delete attributes[key];
|
||
|
});
|
||
|
|
||
|
this.attributes = attributes;
|
||
|
|
||
|
if (typeof this.background === 'object') {
|
||
|
this.background = this.element.add('rect', this.background);
|
||
|
this.x -= this.canvas.textPadding;
|
||
|
this.y -= this.canvas.textPadding;
|
||
|
this.offset = this.canvas.textPadding;
|
||
|
}
|
||
|
else {
|
||
|
this.background = null;
|
||
|
}
|
||
|
|
||
|
this.parseContent(content, parse_links);
|
||
|
this.text = this.element.add('text', attributes, this.lines);
|
||
|
|
||
|
size = this.ZBX_getBBox();
|
||
|
this.width = Math.ceil(size.width);
|
||
|
this.height = Math.ceil(size.height + size.y);
|
||
|
|
||
|
// Workaround for EDGE for proper text height calculation.
|
||
|
if (ED && this.lines.length > 0
|
||
|
&& typeof attributes['font-size'] !== 'undefined' && parseInt(attributes['font-size']) > 16) {
|
||
|
this.height = Math.ceil(this.lines.length * parseInt(attributes['font-size']) * 1.2);
|
||
|
}
|
||
|
|
||
|
this.alignToAnchor();
|
||
|
|
||
|
if (this.background !== null) {
|
||
|
this.background.update({
|
||
|
width: this.width + (this.canvas.textPadding * 2),
|
||
|
height: this.height + (this.canvas.textPadding * 2)
|
||
|
});
|
||
|
}
|
||
|
|
||
|
this.createClipping();
|
||
|
|
||
|
this.text.element.setAttribute('transform', 'translate(' + this.getHorizontalOffset() + ' ' + this.offset + ')');
|
||
|
this.element.element.setAttribute('transform', 'translate(' + this.x + ' ' + this.y + ')');
|
||
|
|
||
|
return this.element;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* getBBox workaround for Firefox and probably also old versions of IE.
|
||
|
*
|
||
|
* Firefox is not able to get element dimensions using getBBox unless it is appended to the DOM.
|
||
|
* The workaround creates a SVG element and appends it to the DOM to be able get element dimensions using the getBBox.
|
||
|
*
|
||
|
* Read more about this bug here https://bugzilla.mozilla.org/show_bug.cgi?id=612118
|
||
|
*/
|
||
|
SVGTextArea.prototype.ZBX_getBBox = function() {
|
||
|
try {
|
||
|
return this.text.element.getBBox();
|
||
|
}
|
||
|
catch (err) {
|
||
|
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
|
||
|
ret;
|
||
|
|
||
|
svg.appendChild(this.text.element);
|
||
|
document.body.appendChild(svg);
|
||
|
ret = this.text.element.getBBox();
|
||
|
svg.parentNode.removeChild(svg);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* ImageCache class.
|
||
|
*
|
||
|
* Implements basic functionality needed to preload images, get image attributes and avoid flickering.
|
||
|
*/
|
||
|
function ImageCache() {
|
||
|
this.lock = 0;
|
||
|
this.images = {};
|
||
|
this.context = null;
|
||
|
this.callback = null;
|
||
|
this.queue = [];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Invoke callback (if any), update image preload task queue.
|
||
|
*/
|
||
|
ImageCache.prototype.invokeCallback = function () {
|
||
|
if (typeof this.callback === 'function') {
|
||
|
this.callback.call(this.context);
|
||
|
}
|
||
|
|
||
|
// Preloads next image list if any.
|
||
|
var task = this.queue.pop();
|
||
|
|
||
|
if (typeof task !== 'undefined') {
|
||
|
this.preload(task.urls, task.callback, task.context);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Handle image processing event (loaded or error).
|
||
|
*/
|
||
|
ImageCache.prototype.handleCallback = function () {
|
||
|
this.lock--;
|
||
|
|
||
|
// If all images are loaded (error is treated as "loaded"), invoke callback.
|
||
|
if (this.lock === 0) {
|
||
|
this.invokeCallback();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Callback for successful image load.
|
||
|
*
|
||
|
* @param {string} id Image id.
|
||
|
* @param {object} image Loaded image.
|
||
|
*/
|
||
|
ImageCache.prototype.onImageLoaded = function (id, image) {
|
||
|
this.images[id] = image;
|
||
|
this.handleCallback();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Callback for image loading errors.
|
||
|
*
|
||
|
* @param {string} id Image id.
|
||
|
*/
|
||
|
ImageCache.prototype.onImageError = function (id) {
|
||
|
this.images[id] = null;
|
||
|
this.handleCallback();
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Preload images.
|
||
|
*
|
||
|
* @param {object} urls Urls of images to be preloaded (urls are provided in key=>value format).
|
||
|
* @param {function} callback Callback to be called when loading is finished. Can be null if no callback is needed.
|
||
|
* @param {object} context Context of a callback. (@see first argument of Function.prototype.apply)
|
||
|
*
|
||
|
* @return {boolean} true if preloader started loading images and false if preloader is busy.
|
||
|
*/
|
||
|
ImageCache.prototype.preload = function (urls, callback, context) {
|
||
|
// If preloader is busy, new preloading task is pushed to queue.
|
||
|
if (this.lock !== 0) {
|
||
|
this.queue.push({
|
||
|
'urls': urls,
|
||
|
'callback': callback,
|
||
|
'context': context
|
||
|
});
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
this.context = context;
|
||
|
this.callback = callback;
|
||
|
|
||
|
var images = 0;
|
||
|
var object = this;
|
||
|
|
||
|
Object.keys(urls).forEach(function (key) {
|
||
|
var url = urls[key];
|
||
|
|
||
|
if (typeof url !== 'string') {
|
||
|
object.onImageError.call(object, key);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (typeof object.images[key] !== 'undefined') {
|
||
|
// Image is pre-loaded already.
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
var image = new Image();
|
||
|
|
||
|
image.onload = function () {
|
||
|
object.onImageLoaded.call(object, key, image);
|
||
|
};
|
||
|
|
||
|
image.onerror = function () {
|
||
|
object.onImageError.call(object, key);
|
||
|
};
|
||
|
|
||
|
image.src = url;
|
||
|
|
||
|
object.lock++;
|
||
|
images++;
|
||
|
});
|
||
|
|
||
|
if (images === 0) {
|
||
|
this.invokeCallback();
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* SVGElement class.
|
||
|
*
|
||
|
* Implements basic functionality needed to create SVG elements.
|
||
|
*
|
||
|
* @see SVGCanvas.prototype.createElement
|
||
|
*
|
||
|
* @param {SVGCanvas} renderer SVGCanvas used to render elements.
|
||
|
* @param {string} type Type of SVG element.
|
||
|
* @param {object} attributes Element attributes (SVG tag attributes) as key => value pairs.
|
||
|
* @param {SVGElement} parent Parent element if any (or null if none).
|
||
|
* @param {mixed} content Element textContent of a set of subelements.
|
||
|
*/
|
||
|
function SVGElement(renderer, type, attributes, parent, content) {
|
||
|
this.id = renderer.id++;
|
||
|
this.type = type;
|
||
|
this.attributes = attributes;
|
||
|
this.content = content;
|
||
|
this.canvas = renderer;
|
||
|
this.parent = parent;
|
||
|
this.items = [];
|
||
|
this.element = null;
|
||
|
this.invalid = false;
|
||
|
|
||
|
if (type !== null) {
|
||
|
this.create();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Add clild SVG element.
|
||
|
*
|
||
|
* @see SVGCanvas.prototype.createElement
|
||
|
*
|
||
|
* @param {mixed} type Type of SVG element or array of objects containing type, attribute and content fields.
|
||
|
* @param {object} attributes Element attributes (SVG tag attributes) as key => value pairs.
|
||
|
* @param {mixed} content Element textContent of a set of subelements.
|
||
|
*
|
||
|
* @return {mixed} SVGElement created or array of SVGElement is type was Array.
|
||
|
*/
|
||
|
SVGElement.prototype.add = function (type, attributes, content) {
|
||
|
// Multiple items to add.
|
||
|
if (Array.isArray(type)) {
|
||
|
var items = [];
|
||
|
|
||
|
type.forEach(function (element) {
|
||
|
if (typeof element !== 'object' || typeof element.type !== 'string') {
|
||
|
throw 'Invalid element configuration!';
|
||
|
}
|
||
|
|
||
|
items.push(this.add(element.type, element.attributes, element.content));
|
||
|
}, this);
|
||
|
|
||
|
return items;
|
||
|
}
|
||
|
|
||
|
if (typeof attributes === 'undefined' || attributes === null) {
|
||
|
attributes = {};
|
||
|
}
|
||
|
|
||
|
var element = this.canvas.createElement(type, attributes, this, content);
|
||
|
|
||
|
if (type.toLowerCase() !== 'textarea') {
|
||
|
this.items.push(element);
|
||
|
}
|
||
|
|
||
|
return element;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove all children elements.
|
||
|
*
|
||
|
* @return {SVGElement}
|
||
|
*/
|
||
|
SVGElement.prototype.clear = function () {
|
||
|
var items = this.items;
|
||
|
|
||
|
items.forEach(function (item) {
|
||
|
item.remove();
|
||
|
});
|
||
|
|
||
|
this.items = [];
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Update attributes of SVG element.
|
||
|
*
|
||
|
* @param {object} attributes New element attributes (SVG tag attributes) as key => value pairs.
|
||
|
*
|
||
|
* @return {SVGElement}
|
||
|
*/
|
||
|
SVGElement.prototype.update = function (attributes) {
|
||
|
Object.keys(attributes).forEach(function (name) {
|
||
|
var attribute = name.split(':');
|
||
|
|
||
|
if (attribute.length === 1) {
|
||
|
this.element.setAttributeNS(null, name, attributes[name]);
|
||
|
}
|
||
|
else if (attribute.length === 2 && typeof SVGCanvas.NAMESPACES[attribute[0]] !== 'undefined') {
|
||
|
this.element.setAttributeNS(SVGCanvas.NAMESPACES[attribute[0]], name, attributes[name]);
|
||
|
}
|
||
|
}, this);
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Moves element from one parent to another.
|
||
|
*
|
||
|
* @param {object} target New parent element.
|
||
|
*
|
||
|
* @return {SVGElement}
|
||
|
*/
|
||
|
SVGElement.prototype.moveTo = function (target) {
|
||
|
this.parent.items = this.parent.items.filter(function (item) {
|
||
|
return item.id !== this.id;
|
||
|
}, this);
|
||
|
|
||
|
this.parent = target;
|
||
|
this.parent.items.push(this);
|
||
|
target.element.appendChild(this.element);
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Mark element as invalid (flag used to force redraw of element).
|
||
|
*
|
||
|
* @return {SVGElement}
|
||
|
*/
|
||
|
SVGElement.prototype.invalidate = function () {
|
||
|
this.invalid = true;
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Remove element from parent and from DOM.
|
||
|
*
|
||
|
* @return {SVGElement}
|
||
|
*/
|
||
|
SVGElement.prototype.remove = function () {
|
||
|
this.clear();
|
||
|
|
||
|
if (this.element !== null) {
|
||
|
// Workaround for IE as .remove() does not work in IE.
|
||
|
if (typeof this.element.remove !== 'function') {
|
||
|
if (typeof this.element.parentNode !== 'undefined') {
|
||
|
this.element.parentNode.removeChild(this.element);
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
this.element.remove();
|
||
|
}
|
||
|
this.element = null;
|
||
|
}
|
||
|
|
||
|
if (this.parent !== null && typeof this.parent.items !== 'undefined') {
|
||
|
this.parent.items = this.parent.items.filter(function (item) {
|
||
|
return item.id !== this.id;
|
||
|
}, this);
|
||
|
}
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Replace existing DOM element with a new one.
|
||
|
*
|
||
|
* @param {object} target New DOM element.
|
||
|
*
|
||
|
* @return {SVGElement}
|
||
|
*/
|
||
|
SVGElement.prototype.replace = function (target) {
|
||
|
if (this.element !== null && this.invalid === false) {
|
||
|
this.element.parentNode.insertBefore(target.element, this.element);
|
||
|
}
|
||
|
|
||
|
this.remove();
|
||
|
|
||
|
Object.keys(target).forEach(function (key) {
|
||
|
this[key] = target[key];
|
||
|
}, this);
|
||
|
|
||
|
return this;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Create SVG DOM element.
|
||
|
*
|
||
|
* @return {object} DOM element.
|
||
|
*/
|
||
|
SVGElement.prototype.create = function () {
|
||
|
var element = (this.type !== '')
|
||
|
? document.createElementNS('http://www.w3.org/2000/svg', this.type)
|
||
|
: document.createTextNode(this.content);
|
||
|
|
||
|
this.remove();
|
||
|
this.element = element;
|
||
|
|
||
|
if (this.type !== '') {
|
||
|
this.update(this.attributes);
|
||
|
|
||
|
if (Array.isArray(this.content)) {
|
||
|
this.content.forEach(function (element) {
|
||
|
if (typeof element === 'string') {
|
||
|
// Treat element as a text node.
|
||
|
element = {
|
||
|
type: '',
|
||
|
attributes: null,
|
||
|
content: element
|
||
|
};
|
||
|
}
|
||
|
|
||
|
if (typeof element !== 'object' || typeof element.type !== 'string') {
|
||
|
throw 'Invalid element configuration!';
|
||
|
}
|
||
|
|
||
|
this.add(element.type, element.attributes, element.content);
|
||
|
}, this);
|
||
|
|
||
|
this.content = null;
|
||
|
}
|
||
|
else if ((/string|number|boolean/).test(typeof this.content)) {
|
||
|
element.textContent = this.content;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (this.parent !== null && this.parent.element !== null) {
|
||
|
this.parent.element.appendChild(element);
|
||
|
}
|
||
|
|
||
|
return element;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Merge source and target attributes. If both source and attributes contain the same set of keys, values from
|
||
|
* attributes are used.
|
||
|
*
|
||
|
* @param {object} source Source object attributes.
|
||
|
* @param {object} attributes New object attributes.
|
||
|
*
|
||
|
* @return {object} Merged set of attributes.
|
||
|
*/
|
||
|
SVGElement.mergeAttributes = function (source, attributes) {
|
||
|
var merged = {};
|
||
|
|
||
|
if (typeof source === 'object') {
|
||
|
Object.keys(source).forEach(function (key){
|
||
|
merged[key] = source[key];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (typeof attributes === 'object') {
|
||
|
Object.keys(attributes).forEach(function (key){
|
||
|
merged[key] = attributes[key];
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return merged;
|
||
|
};
|