select.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. /*globals $, svgedit*/
  2. /*jslint vars: true, eqeq: true, forin: true*/
  3. /**
  4. * Package: svedit.select
  5. *
  6. * Licensed under the MIT License
  7. *
  8. * Copyright(c) 2010 Alexis Deveria
  9. * Copyright(c) 2010 Jeff Schiller
  10. */
  11. // Dependencies:
  12. // 1) jQuery
  13. // 2) pathseg.js
  14. // 3) browser.js
  15. // 4) math.js
  16. // 5) svgutils.js
  17. (function() {'use strict';
  18. if (!svgedit.select) {
  19. svgedit.select = {};
  20. }
  21. var svgFactory_;
  22. var config_;
  23. var selectorManager_; // A Singleton
  24. var gripRadius = svgedit.browser.isTouch() ? 10 : 4;
  25. // Class: svgedit.select.Selector
  26. // Private class for DOM element selection boxes
  27. //
  28. // Parameters:
  29. // id - integer to internally indentify the selector
  30. // elem - DOM element associated with this selector
  31. svgedit.select.Selector = function(id, elem) {
  32. // this is the selector's unique number
  33. this.id = id;
  34. // this holds a reference to the element for which this selector is being used
  35. this.selectedElement = elem;
  36. // this is a flag used internally to track whether the selector is being used or not
  37. this.locked = true;
  38. // this holds a reference to the <g> element that holds all visual elements of the selector
  39. this.selectorGroup = svgFactory_.createSVGElement({
  40. 'element': 'g',
  41. 'attr': {'id': ('selectorGroup' + this.id)}
  42. });
  43. // this holds a reference to the path rect
  44. this.selectorRect = this.selectorGroup.appendChild(
  45. svgFactory_.createSVGElement({
  46. 'element': 'path',
  47. 'attr': {
  48. 'id': ('selectedBox' + this.id),
  49. 'fill': 'none',
  50. 'stroke': '#22C',
  51. 'stroke-width': '1',
  52. 'stroke-dasharray': '5,5',
  53. // need to specify this so that the rect is not selectable
  54. 'style': 'pointer-events:none'
  55. }
  56. })
  57. );
  58. // this holds a reference to the grip coordinates for this selector
  59. this.gripCoords = {
  60. 'nw': null,
  61. 'n' : null,
  62. 'ne': null,
  63. 'e' : null,
  64. 'se': null,
  65. 's' : null,
  66. 'sw': null,
  67. 'w' : null
  68. };
  69. this.reset(this.selectedElement);
  70. };
  71. // Function: svgedit.select.Selector.reset
  72. // Used to reset the id and element that the selector is attached to
  73. //
  74. // Parameters:
  75. // e - DOM element associated with this selector
  76. svgedit.select.Selector.prototype.reset = function(e) {
  77. this.locked = true;
  78. this.selectedElement = e;
  79. this.resize();
  80. this.selectorGroup.setAttribute('display', 'inline');
  81. };
  82. // Function: svgedit.select.Selector.updateGripCursors
  83. // Updates cursors for corner grips on rotation so arrows point the right way
  84. //
  85. // Parameters:
  86. // angle - Float indicating current rotation angle in degrees
  87. svgedit.select.Selector.prototype.updateGripCursors = function(angle) {
  88. var dir,
  89. dir_arr = [],
  90. steps = Math.round(angle / 45);
  91. if (steps < 0) {steps += 8;}
  92. for (dir in selectorManager_.selectorGrips) {
  93. dir_arr.push(dir);
  94. }
  95. while (steps > 0) {
  96. dir_arr.push(dir_arr.shift());
  97. steps--;
  98. }
  99. var i = 0;
  100. for (dir in selectorManager_.selectorGrips) {
  101. selectorManager_.selectorGrips[dir].setAttribute('style', ('cursor:' + dir_arr[i] + '-resize'));
  102. i++;
  103. }
  104. };
  105. // Function: svgedit.select.Selector.showGrips
  106. // Show the resize grips of this selector
  107. //
  108. // Parameters:
  109. // show - boolean indicating whether grips should be shown or not
  110. svgedit.select.Selector.prototype.showGrips = function(show) {
  111. var bShow = show ? 'inline' : 'none';
  112. selectorManager_.selectorGripsGroup.setAttribute('display', bShow);
  113. var elem = this.selectedElement;
  114. this.hasGrips = show;
  115. if (elem && show) {
  116. this.selectorGroup.appendChild(selectorManager_.selectorGripsGroup);
  117. this.updateGripCursors(svgedit.utilities.getRotationAngle(elem));
  118. }
  119. };
  120. // Function: svgedit.select.Selector.resize
  121. // Updates the selector to match the element's size
  122. svgedit.select.Selector.prototype.resize = function() {
  123. var selectedBox = this.selectorRect,
  124. mgr = selectorManager_,
  125. selectedGrips = mgr.selectorGrips,
  126. selected = this.selectedElement,
  127. sw = selected.getAttribute('stroke-width'),
  128. current_zoom = svgFactory_.currentZoom();
  129. var offset = 1/current_zoom;
  130. if (selected.getAttribute('stroke') !== 'none' && !isNaN(sw)) {
  131. offset += (sw/2);
  132. }
  133. var tagName = selected.tagName;
  134. if (tagName === 'text') {
  135. offset += 2/current_zoom;
  136. }
  137. // loop and transform our bounding box until we reach our first rotation
  138. var tlist = svgedit.transformlist.getTransformList(selected);
  139. var m = svgedit.math.transformListToTransform(tlist).matrix;
  140. // This should probably be handled somewhere else, but for now
  141. // it keeps the selection box correctly positioned when zoomed
  142. m.e *= current_zoom;
  143. m.f *= current_zoom;
  144. var bbox = svgedit.utilities.getBBox(selected);
  145. if (tagName === 'g' && !$.data(selected, 'gsvg')) {
  146. // The bbox for a group does not include stroke vals, so we
  147. // get the bbox based on its children.
  148. var stroked_bbox = svgFactory_.getStrokedBBox(selected.childNodes);
  149. if (stroked_bbox) {
  150. bbox = stroked_bbox;
  151. }
  152. }
  153. // apply the transforms
  154. var l = bbox.x, t = bbox.y, w = bbox.width, h = bbox.height;
  155. bbox = {x:l, y:t, width:w, height:h};
  156. // we need to handle temporary transforms too
  157. // if skewed, get its transformed box, then find its axis-aligned bbox
  158. //*
  159. offset *= current_zoom;
  160. var nbox = svgedit.math.transformBox(l*current_zoom, t*current_zoom, w*current_zoom, h*current_zoom, m),
  161. aabox = nbox.aabox,
  162. nbax = aabox.x - offset,
  163. nbay = aabox.y - offset,
  164. nbaw = aabox.width + (offset * 2),
  165. nbah = aabox.height + (offset * 2);
  166. // now if the shape is rotated, un-rotate it
  167. var cx = nbax + nbaw/2,
  168. cy = nbay + nbah/2;
  169. var angle = svgedit.utilities.getRotationAngle(selected);
  170. if (angle) {
  171. var rot = svgFactory_.svgRoot().createSVGTransform();
  172. rot.setRotate(-angle, cx, cy);
  173. var rotm = rot.matrix;
  174. nbox.tl = svgedit.math.transformPoint(nbox.tl.x, nbox.tl.y, rotm);
  175. nbox.tr = svgedit.math.transformPoint(nbox.tr.x, nbox.tr.y, rotm);
  176. nbox.bl = svgedit.math.transformPoint(nbox.bl.x, nbox.bl.y, rotm);
  177. nbox.br = svgedit.math.transformPoint(nbox.br.x, nbox.br.y, rotm);
  178. // calculate the axis-aligned bbox
  179. var tl = nbox.tl;
  180. var minx = tl.x,
  181. miny = tl.y,
  182. maxx = tl.x,
  183. maxy = tl.y;
  184. var min = Math.min, max = Math.max;
  185. minx = min(minx, min(nbox.tr.x, min(nbox.bl.x, nbox.br.x) ) ) - offset;
  186. miny = min(miny, min(nbox.tr.y, min(nbox.bl.y, nbox.br.y) ) ) - offset;
  187. maxx = max(maxx, max(nbox.tr.x, max(nbox.bl.x, nbox.br.x) ) ) + offset;
  188. maxy = max(maxy, max(nbox.tr.y, max(nbox.bl.y, nbox.br.y) ) ) + offset;
  189. nbax = minx;
  190. nbay = miny;
  191. nbaw = (maxx-minx);
  192. nbah = (maxy-miny);
  193. }
  194. var dstr = 'M' + nbax + ',' + nbay
  195. + ' L' + (nbax+nbaw) + ',' + nbay
  196. + ' ' + (nbax+nbaw) + ',' + (nbay+nbah)
  197. + ' ' + nbax + ',' + (nbay+nbah) + 'z';
  198. selectedBox.setAttribute('d', dstr);
  199. var xform = angle ? 'rotate(' + [angle, cx, cy].join(',') + ')' : '';
  200. this.selectorGroup.setAttribute('transform', xform);
  201. // TODO(codedread): Is this if needed?
  202. // if (selected === selectedElements[0]) {
  203. this.gripCoords = {
  204. 'nw': [nbax, nbay],
  205. 'ne': [nbax+nbaw, nbay],
  206. 'sw': [nbax, nbay+nbah],
  207. 'se': [nbax+nbaw, nbay+nbah],
  208. 'n': [nbax + (nbaw)/2, nbay],
  209. 'w': [nbax, nbay + (nbah)/2],
  210. 'e': [nbax + nbaw, nbay + (nbah)/2],
  211. 's': [nbax + (nbaw)/2, nbay + nbah]
  212. };
  213. var dir;
  214. for (dir in this.gripCoords) {
  215. var coords = this.gripCoords[dir];
  216. selectedGrips[dir].setAttribute('cx', coords[0]);
  217. selectedGrips[dir].setAttribute('cy', coords[1]);
  218. }
  219. // we want to go 20 pixels in the negative transformed y direction, ignoring scale
  220. mgr.rotateGripConnector.setAttribute('x1', nbax + (nbaw)/2);
  221. mgr.rotateGripConnector.setAttribute('y1', nbay);
  222. mgr.rotateGripConnector.setAttribute('x2', nbax + (nbaw)/2);
  223. mgr.rotateGripConnector.setAttribute('y2', nbay - (gripRadius*5));
  224. mgr.rotateGrip.setAttribute('cx', nbax + (nbaw)/2);
  225. mgr.rotateGrip.setAttribute('cy', nbay - (gripRadius*5));
  226. // }
  227. };
  228. // Class: svgedit.select.SelectorManager
  229. svgedit.select.SelectorManager = function() {
  230. // this will hold the <g> element that contains all selector rects/grips
  231. this.selectorParentGroup = null;
  232. // this is a special rect that is used for multi-select
  233. this.rubberBandBox = null;
  234. // this will hold objects of type svgedit.select.Selector (see above)
  235. this.selectors = [];
  236. // this holds a map of SVG elements to their Selector object
  237. this.selectorMap = {};
  238. // this holds a reference to the grip elements
  239. this.selectorGrips = {
  240. 'nw': null,
  241. 'n' : null,
  242. 'ne': null,
  243. 'e' : null,
  244. 'se': null,
  245. 's' : null,
  246. 'sw': null,
  247. 'w' : null
  248. };
  249. this.selectorGripsGroup = null;
  250. this.rotateGripConnector = null;
  251. this.rotateGrip = null;
  252. this.initGroup();
  253. };
  254. // Function: svgedit.select.SelectorManager.initGroup
  255. // Resets the parent selector group element
  256. svgedit.select.SelectorManager.prototype.initGroup = function() {
  257. // remove old selector parent group if it existed
  258. if (this.selectorParentGroup && this.selectorParentGroup.parentNode) {
  259. this.selectorParentGroup.parentNode.removeChild(this.selectorParentGroup);
  260. }
  261. // create parent selector group and add it to svgroot
  262. this.selectorParentGroup = svgFactory_.createSVGElement({
  263. 'element': 'g',
  264. 'attr': {'id': 'selectorParentGroup'}
  265. });
  266. this.selectorGripsGroup = svgFactory_.createSVGElement({
  267. 'element': 'g',
  268. 'attr': {'display': 'none'}
  269. });
  270. this.selectorParentGroup.appendChild(this.selectorGripsGroup);
  271. svgFactory_.svgRoot().appendChild(this.selectorParentGroup);
  272. this.selectorMap = {};
  273. this.selectors = [];
  274. this.rubberBandBox = null;
  275. // add the corner grips
  276. var dir;
  277. for (dir in this.selectorGrips) {
  278. var grip = svgFactory_.createSVGElement({
  279. 'element': 'circle',
  280. 'attr': {
  281. 'id': ('selectorGrip_resize_' + dir),
  282. 'fill': '#22C',
  283. 'r': gripRadius,
  284. 'style': ('cursor:' + dir + '-resize'),
  285. // This expands the mouse-able area of the grips making them
  286. // easier to grab with the mouse.
  287. // This works in Opera and WebKit, but does not work in Firefox
  288. // see https://bugzilla.mozilla.org/show_bug.cgi?id=500174
  289. 'stroke-width': 2,
  290. 'pointer-events': 'all'
  291. }
  292. });
  293. $.data(grip, 'dir', dir);
  294. $.data(grip, 'type', 'resize');
  295. this.selectorGrips[dir] = this.selectorGripsGroup.appendChild(grip);
  296. }
  297. // add rotator elems
  298. this.rotateGripConnector = this.selectorGripsGroup.appendChild(
  299. svgFactory_.createSVGElement({
  300. 'element': 'line',
  301. 'attr': {
  302. 'id': ('selectorGrip_rotateconnector'),
  303. 'stroke': '#22C',
  304. 'stroke-width': '1'
  305. }
  306. })
  307. );
  308. this.rotateGrip = this.selectorGripsGroup.appendChild(
  309. svgFactory_.createSVGElement({
  310. 'element': 'circle',
  311. 'attr': {
  312. 'id': 'selectorGrip_rotate',
  313. 'fill': 'lime',
  314. 'r': gripRadius,
  315. 'stroke': '#22C',
  316. 'stroke-width': 2,
  317. 'style': 'cursor:url(' + config_.imgPath + 'rotate.png) 12 12, auto;'
  318. }
  319. })
  320. );
  321. $.data(this.rotateGrip, 'type', 'rotate');
  322. if ($('#canvasBackground').length) {return;}
  323. var dims = config_.dimensions;
  324. var canvasbg = svgFactory_.createSVGElement({
  325. 'element': 'svg',
  326. 'attr': {
  327. 'id': 'canvasBackground',
  328. 'width': dims[0],
  329. 'height': dims[1],
  330. 'x': 0,
  331. 'y': 0,
  332. 'overflow': (svgedit.browser.isWebkit() ? 'none' : 'visible'), // Chrome 7 has a problem with this when zooming out
  333. 'style': 'pointer-events:none'
  334. }
  335. });
  336. var rect = svgFactory_.createSVGElement({
  337. 'element': 'rect',
  338. 'attr': {
  339. 'width': '100%',
  340. 'height': '100%',
  341. 'x': 0,
  342. 'y': 0,
  343. 'stroke-width': 1,
  344. 'stroke': '#000',
  345. 'fill': '#FFF',
  346. 'style': 'pointer-events:none'
  347. }
  348. });
  349. // Both Firefox and WebKit are too slow with this filter region (especially at higher
  350. // zoom levels) and Opera has at least one bug
  351. // if (!svgedit.browser.isOpera()) rect.setAttribute('filter', 'url(#canvashadow)');
  352. canvasbg.appendChild(rect);
  353. svgFactory_.svgRoot().insertBefore(canvasbg, svgFactory_.svgContent());
  354. };
  355. // Function: svgedit.select.SelectorManager.requestSelector
  356. // Returns the selector based on the given element
  357. //
  358. // Parameters:
  359. // elem - DOM element to get the selector for
  360. svgedit.select.SelectorManager.prototype.requestSelector = function(elem) {
  361. if (elem == null) {return null;}
  362. var i,
  363. N = this.selectors.length;
  364. // If we've already acquired one for this element, return it.
  365. if (typeof(this.selectorMap[elem.id]) == 'object') {
  366. this.selectorMap[elem.id].locked = true;
  367. return this.selectorMap[elem.id];
  368. }
  369. for (i = 0; i < N; ++i) {
  370. if (this.selectors[i] && !this.selectors[i].locked) {
  371. this.selectors[i].locked = true;
  372. this.selectors[i].reset(elem);
  373. this.selectorMap[elem.id] = this.selectors[i];
  374. return this.selectors[i];
  375. }
  376. }
  377. // if we reached here, no available selectors were found, we create one
  378. this.selectors[N] = new svgedit.select.Selector(N, elem);
  379. this.selectorParentGroup.appendChild(this.selectors[N].selectorGroup);
  380. this.selectorMap[elem.id] = this.selectors[N];
  381. return this.selectors[N];
  382. };
  383. // Function: svgedit.select.SelectorManager.releaseSelector
  384. // Removes the selector of the given element (hides selection box)
  385. //
  386. // Parameters:
  387. // elem - DOM element to remove the selector for
  388. svgedit.select.SelectorManager.prototype.releaseSelector = function(elem) {
  389. if (elem == null) {return;}
  390. var i,
  391. N = this.selectors.length,
  392. sel = this.selectorMap[elem.id];
  393. if (!sel.locked) {
  394. // TODO(codedread): Ensure this exists in this module.
  395. console.log('WARNING! selector was released but was already unlocked');
  396. }
  397. for (i = 0; i < N; ++i) {
  398. if (this.selectors[i] && this.selectors[i] == sel) {
  399. delete this.selectorMap[elem.id];
  400. sel.locked = false;
  401. sel.selectedElement = null;
  402. sel.showGrips(false);
  403. // remove from DOM and store reference in JS but only if it exists in the DOM
  404. try {
  405. sel.selectorGroup.setAttribute('display', 'none');
  406. } catch(e) { }
  407. break;
  408. }
  409. }
  410. };
  411. // Function: svgedit.select.SelectorManager.getRubberBandBox
  412. // Returns the rubberBandBox DOM element. This is the rectangle drawn by the user for selecting/zooming
  413. svgedit.select.SelectorManager.prototype.getRubberBandBox = function() {
  414. if (!this.rubberBandBox) {
  415. this.rubberBandBox = this.selectorParentGroup.appendChild(
  416. svgFactory_.createSVGElement({
  417. 'element': 'rect',
  418. 'attr': {
  419. 'id': 'selectorRubberBand',
  420. 'fill': '#22C',
  421. 'fill-opacity': 0.15,
  422. 'stroke': '#22C',
  423. 'stroke-width': 0.5,
  424. 'display': 'none',
  425. 'style': 'pointer-events:none'
  426. }
  427. })
  428. );
  429. }
  430. return this.rubberBandBox;
  431. };
  432. /**
  433. * Interface: svgedit.select.SVGFactory
  434. * An object that creates SVG elements for the canvas.
  435. *
  436. * interface svgedit.select.SVGFactory {
  437. * SVGElement createSVGElement(jsonMap);
  438. * SVGSVGElement svgRoot();
  439. * SVGSVGElement svgContent();
  440. *
  441. * Number currentZoom();
  442. * Object getStrokedBBox(Element[]); // TODO(codedread): Remove when getStrokedBBox() has been put into svgutils.js
  443. * }
  444. */
  445. /**
  446. * Function: svgedit.select.init()
  447. * Initializes this module.
  448. *
  449. * Parameters:
  450. * config - an object containing configurable parameters (imgPath)
  451. * svgFactory - an object implementing the SVGFactory interface (see above).
  452. */
  453. svgedit.select.init = function(config, svgFactory) {
  454. config_ = config;
  455. svgFactory_ = svgFactory;
  456. selectorManager_ = new svgedit.select.SelectorManager();
  457. };
  458. /**
  459. * Function: svgedit.select.getSelectorManager
  460. *
  461. * Returns:
  462. * The SelectorManager instance.
  463. */
  464. svgedit.select.getSelectorManager = function() {
  465. return selectorManager_;
  466. };
  467. }());