/*! * svg.select.js - An extension of svg.js which allows to select elements with mouse * @version 3.0.1 * https://github.com/svgdotjs/svg.select.js * * @copyright Ulrich-Matthias Schäfer * @license MIT */; ;(function() { "use strict"; function SelectHandler(el) { this.el = el; el.remember('_selectHandler', this); this.pointSelection = {isSelected: false}; this.rectSelection = {isSelected: false}; // helper list with position settings of each type of point this.pointsList = { lt: [ 0, 0 ], rt: [ 'width', 0 ], rb: [ 'width', 'height' ], lb: [ 0, 'height' ], t: [ 'width', 0 ], r: [ 'width', 'height' ], b: [ 'width', 'height' ], l: [ 0, 'height' ] }; // helper function to get point coordinates based on settings above and an object (bbox in our case) this.pointCoord = function (setting, object, isPointCentered) { var coord = typeof setting !== 'string' ? setting : object[setting]; // Top, bottom, right and left points are placed in the center of element width/height return isPointCentered ? coord / 2 : coord } this.pointCoords = function (point, object) { var settings = this.pointsList[point]; return { x: this.pointCoord(settings[0], object, (point === 't' || point === 'b')), y: this.pointCoord(settings[1], object, (point === 'r' || point === 'l')) } } } SelectHandler.prototype.init = function (value, options) { var bbox = this.el.bbox(); this.options = {}; // store defaults list of points in order to verify users config var points = this.el.selectize.defaults.points; // Merging the defaults and the options-object together for (var i in this.el.selectize.defaults) { this.options[i] = this.el.selectize.defaults[i]; if (options[i] !== undefined) { this.options[i] = options[i]; } } // prepare & validate list of points to be added (or excluded) var pointsLists = ['points', 'pointsExclude']; for (var i in pointsLists) { var option = this.options[pointsLists[i]]; if (typeof option === 'string') { if (option.length > 0) { // if set as comma separated string list => convert it into an array option = option.split(/\s*,\s*/i); } else { option = []; } } else if (typeof option === 'boolean' && pointsLists[i] === 'points') { // this is not needed, but let's have it for legacy support option = option ? points : []; } this.options[pointsLists[i]] = option; } // intersect correct all points options with users config (exclude unwanted points) // ES5 -> NO arrow functions nor Array.includes() this.options.points = [ points, this.options.points ].reduce( function (a, b) { return a.filter( function (c) { return b.indexOf(c) > -1; } ) } ); // exclude pointsExclude, if wanted this.options.points = [ this.options.points, this.options.pointsExclude ].reduce( function (a, b) { return a.filter( function (c) { return b.indexOf(c) < 0; } ) } ); this.parent = this.el.parent(); this.nested = (this.nested || this.parent.group()); this.nested.matrix(new SVG.Matrix(this.el).translate(bbox.x, bbox.y)); // When deepSelect is enabled and the element is a line/polyline/polygon, draw only points for moving if (this.options.deepSelect && ['line', 'polyline', 'polygon'].indexOf(this.el.type) !== -1) { this.selectPoints(value); } else { this.selectRect(value); } this.observe(); this.cleanup(); }; SelectHandler.prototype.selectPoints = function (value) { this.pointSelection.isSelected = value; // When set is already there we dont have to create one if (this.pointSelection.set) { return this; } // Create our set of elements this.pointSelection.set = this.parent.set(); // draw the points and mark the element as selected this.drawPoints(); return this; }; // create the point-array which contains the 2 points of a line or simply the points-array of polyline/polygon SelectHandler.prototype.getPointArray = function () { var bbox = this.el.bbox(); return this.el.array().valueOf().map(function (el) { return [el[0] - bbox.x, el[1] - bbox.y]; }); }; // Draws a points SelectHandler.prototype.drawPoints = function () { var _this = this, array = this.getPointArray(); // go through the array of points for (var i = 0, len = array.length; i < len; ++i) { var curriedEvent = (function (k) { return function (ev) { ev = ev || window.event; ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; ev.stopPropagation(); var x = ev.pageX || ev.touches[0].pageX; var y = ev.pageY || ev.touches[0].pageY; _this.el.fire('point', {x: x, y: y, i: k, event: ev}); }; })(i); // add every point to the set // add css-classes and a touchstart-event which fires our event for moving points var point = this.drawPoint(array[i][0], array[i][1]) .addClass(this.options.classPoints) .addClass(this.options.classPoints + '_point') .on('touchstart', curriedEvent) .on('mousedown', curriedEvent) this.pointSelection.set.add(point); } }; // The function to draw single point SelectHandler.prototype.drawPoint = function (cx, cy) { var pointType = this.options.pointType; switch (pointType) { case 'circle': return this.drawCircle(cx, cy); case 'rect': return this.drawRect(cx, cy); default: if (typeof pointType === 'function') { return pointType.call(this, cx, cy); } throw new Error('Unknown ' + pointType + ' point type!'); } }; // The function to draw the circle point SelectHandler.prototype.drawCircle = function (cx, cy) { return this.nested.circle(this.options.pointSize) .stroke(this.options.pointStroke) .fill(this.options.pointFill) .center(cx, cy); }; // The function to draw the rect point SelectHandler.prototype.drawRect = function (cx, cy) { return this.nested.rect(this.options.pointSize, this.options.pointSize) .stroke(this.options.pointStroke) .fill(this.options.pointFill) .center(cx, cy); }; // every time a point is moved, we have to update the positions of our point SelectHandler.prototype.updatePointSelection = function () { var array = this.getPointArray(); this.pointSelection.set.each(function (i) { if (this.cx() === array[i][0] && this.cy() === array[i][1]) { return; } this.center(array[i][0], array[i][1]); }); }; SelectHandler.prototype.updateRectSelection = function () { var _this = this, bbox = this.el.bbox(); this.rectSelection.set.get(0).attr({ width: bbox.width, height: bbox.height }); // set.get(1) is always in the upper left corner. no need to move it if (this.options.points.length) { this.options.points.map(function (point, index) { var coords = _this.pointCoords(point, bbox); _this.rectSelection.set.get(index + 1).center(coords.x, coords.y); }); } if (this.options.rotationPoint) { var length = this.rectSelection.set.length(); this.rectSelection.set.get(length - 1).center(bbox.width / 2, 20); } }; SelectHandler.prototype.selectRect = function (value) { var _this = this, bbox = this.el.bbox(); this.rectSelection.isSelected = value; // when set is already p this.rectSelection.set = this.rectSelection.set || this.parent.set(); // helperFunction to create a mouse-down function which triggers the event specified in `eventName` function getMoseDownFunc(eventName) { return function (ev) { ev = ev || window.event; ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; ev.stopPropagation(); var x = ev.pageX || ev.touches[0].pageX; var y = ev.pageY || ev.touches[0].pageY; _this.el.fire(eventName, {x: x, y: y, event: ev}); }; } // create the selection-rectangle and add the css-class if (!this.rectSelection.set.get(0)) { this.rectSelection.set.add(this.nested.rect(bbox.width, bbox.height).addClass(this.options.classRect)); } // Draw Points at the edges, if enabled if (this.options.points.length && this.rectSelection.set.length() < 2) { var ename ="touchstart", mname = "mousedown"; this.options.points.map(function (point, index) { var coords = _this.pointCoords(point, bbox); var pointElement = _this.drawPoint(coords.x, coords.y) .attr('class', _this.options.classPoints + '_' + point) .on(mname, getMoseDownFunc(point)) .on(ename, getMoseDownFunc(point)); _this.rectSelection.set.add(pointElement); }); this.rectSelection.set.each(function () { this.addClass(_this.options.classPoints); }); } // draw rotationPint, if enabled if (this.options.rotationPoint && ((this.options.points && !this.rectSelection.set.get(9)) || (!this.options.points && !this.rectSelection.set.get(1)))) { var curriedEvent = function (ev) { ev = ev || window.event; ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; ev.stopPropagation(); var x = ev.pageX || ev.touches[0].pageX; var y = ev.pageY || ev.touches[0].pageY; _this.el.fire('rot', {x: x, y: y, event: ev}); }; var pointElement = this.drawPoint(bbox.width / 2, 20) .attr('class', this.options.classPoints + '_rot') .on("touchstart", curriedEvent) .on("mousedown", curriedEvent); this.rectSelection.set.add(pointElement); } }; SelectHandler.prototype.handler = function () { var bbox = this.el.bbox(); this.nested.matrix(new SVG.Matrix(this.el).translate(bbox.x, bbox.y)); if (this.rectSelection.isSelected) { this.updateRectSelection(); } if (this.pointSelection.isSelected) { this.updatePointSelection(); } }; SelectHandler.prototype.observe = function () { var _this = this; if (MutationObserver) { if (this.rectSelection.isSelected || this.pointSelection.isSelected) { this.observerInst = this.observerInst || new MutationObserver(function () { _this.handler(); }); this.observerInst.observe(this.el.node, {attributes: true}); } else { try { this.observerInst.disconnect(); delete this.observerInst; } catch (e) { } } } else { this.el.off('DOMAttrModified.select'); if (this.rectSelection.isSelected || this.pointSelection.isSelected) { this.el.on('DOMAttrModified.select', function () { _this.handler(); }); } } }; SelectHandler.prototype.cleanup = function () { //var _this = this; if (!this.rectSelection.isSelected && this.rectSelection.set) { // stop watching the element, remove the selection this.rectSelection.set.each(function () { this.remove(); }); this.rectSelection.set.clear(); delete this.rectSelection.set; } if (!this.pointSelection.isSelected && this.pointSelection.set) { // Remove all points, clear the set, stop watching the element this.pointSelection.set.each(function () { this.remove(); }); this.pointSelection.set.clear(); delete this.pointSelection.set; } if (!this.pointSelection.isSelected && !this.rectSelection.isSelected) { this.nested.remove(); delete this.nested; } }; SVG.extend(SVG.Element, { // Select element with mouse selectize: function (value, options) { // Check the parameters and reassign if needed if (typeof value === 'object') { options = value; value = true; } var selectHandler = this.remember('_selectHandler') || new SelectHandler(this); selectHandler.init(value === undefined ? true : value, options || {}); return this; } }); SVG.Element.prototype.selectize.defaults = { points: ['lt', 'rt', 'rb', 'lb', 't', 'r', 'b', 'l'], // which points to draw, default all pointsExclude: [], // easier option if to exclude few than rewrite all classRect: 'svg_select_boundingRect', // Css-class added to the rect classPoints: 'svg_select_points', // Css-class added to the points pointSize: 7, // size of point rotationPoint: true, // If true, rotation point is drawn. Needed for rotation! deepSelect: false, // If true, moving of single points is possible (only line, polyline, polyon) pointType: 'circle', // Point type: circle or rect, default circle pointFill: "#000", // Point fill color pointStroke: { width: 1, color: "#000" } // Point stroke properties }; }());