select.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  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) browser.js
  14. // 3) math.js
  15. // 4) svgutils.js
  16. (function() {'use strict';
  17. if (!svgedit.select) {
  18. svgedit.select = {};
  19. }
  20. var svgFactory_;
  21. var config_;
  22. var selectorManager_; // A Singleton
  23. var gripRadius = svgedit.browser.isTouch() ? 10 : 4;
  24. // Class: svgedit.select.Selector
  25. // Private class for DOM element selection boxes
  26. //
  27. // Parameters:
  28. // id - integer to internally indentify the selector
  29. // elem - DOM element associated with this selector
  30. svgedit.select.Selector = function(id, elem) {
  31. // this is the selector's unique number
  32. this.id = id;
  33. // this holds a reference to the element for which this selector is being used
  34. this.selectedElement = elem;
  35. // this is a flag used internally to track whether the selector is being used or not
  36. this.locked = true;
  37. // this holds a reference to the <g> element that holds all visual elements of the selector
  38. this.selectorGroup = svgFactory_.createSVGElement({
  39. 'element': 'g',
  40. 'attr': {'id': ('selectorGroup' + this.id)}
  41. });
  42. // this holds a reference to the path rect
  43. this.selectorRect = this.selectorGroup.appendChild(
  44. svgFactory_.createSVGElement({
  45. 'element': 'path',
  46. 'attr': {
  47. 'id': ('selectedBox' + this.id),
  48. 'fill': 'none',
  49. 'stroke': '#22C',
  50. 'stroke-width': '1',
  51. 'stroke-dasharray': '5,5',
  52. // need to specify this so that the rect is not selectable
  53. 'style': 'pointer-events:none'
  54. }
  55. })
  56. );
  57. // this holds a reference to the grip coordinates for this selector
  58. this.gripCoords = {
  59. 'nw': null,
  60. 'n' : null,
  61. 'ne': null,
  62. 'e' : null,
  63. 'se': null,
  64. 's' : null,
  65. 'sw': null,
  66. 'w' : null
  67. };
  68. this.reset(this.selectedElement);
  69. };
  70. // Function: svgedit.select.Selector.reset
  71. // Used to reset the id and element that the selector is attached to
  72. //
  73. // Parameters:
  74. // e - DOM element associated with this selector
  75. svgedit.select.Selector.prototype.reset = function(e) {
  76. this.locked = true;
  77. this.selectedElement = e;
  78. this.resize();
  79. this.selectorGroup.setAttribute('display', 'inline');
  80. };
  81. // Function: svgedit.select.Selector.updateGripCursors
  82. // Updates cursors for corner grips on rotation so arrows point the right way
  83. //
  84. // Parameters:
  85. // angle - Float indicating current rotation angle in degrees
  86. svgedit.select.Selector.prototype.updateGripCursors = function(angle) {
  87. var dir,
  88. dir_arr = [],
  89. steps = Math.round(angle / 45);
  90. if (steps < 0) {steps += 8;}
  91. for (dir in selectorManager_.selectorGrips) {
  92. dir_arr.push(dir);
  93. }
  94. while (steps > 0) {
  95. dir_arr.push(dir_arr.shift());
  96. steps--;
  97. }
  98. var i = 0;
  99. for (dir in selectorManager_.selectorGrips) {
  100. selectorManager_.selectorGrips[dir].setAttribute('style', ('cursor:' + dir_arr[i] + '-resize'));
  101. i++;
  102. }
  103. };
  104. // Function: svgedit.select.Selector.showGrips
  105. // Show the resize grips of this selector
  106. //
  107. // Parameters:
  108. // show - boolean indicating whether grips should be shown or not
  109. svgedit.select.Selector.prototype.showGrips = function(show) {
  110. // TODO: use suspendRedraw() here
  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 sr_handle = svgFactory_.svgRoot().suspendRedraw(100);
  195. var dstr = 'M' + nbax + ',' + nbay
  196. + ' L' + (nbax+nbaw) + ',' + nbay
  197. + ' ' + (nbax+nbaw) + ',' + (nbay+nbah)
  198. + ' ' + nbax + ',' + (nbay+nbah) + 'z';
  199. selectedBox.setAttribute('d', dstr);
  200. var xform = angle ? 'rotate(' + [angle, cx, cy].join(',') + ')' : '';
  201. this.selectorGroup.setAttribute('transform', xform);
  202. // TODO(codedread): Is this if needed?
  203. // if (selected === selectedElements[0]) {
  204. this.gripCoords = {
  205. 'nw': [nbax, nbay],
  206. 'ne': [nbax+nbaw, nbay],
  207. 'sw': [nbax, nbay+nbah],
  208. 'se': [nbax+nbaw, nbay+nbah],
  209. 'n': [nbax + (nbaw)/2, nbay],
  210. 'w': [nbax, nbay + (nbah)/2],
  211. 'e': [nbax + nbaw, nbay + (nbah)/2],
  212. 's': [nbax + (nbaw)/2, nbay + nbah]
  213. };
  214. var dir;
  215. for (dir in this.gripCoords) {
  216. var coords = this.gripCoords[dir];
  217. selectedGrips[dir].setAttribute('cx', coords[0]);
  218. selectedGrips[dir].setAttribute('cy', coords[1]);
  219. }
  220. // we want to go 20 pixels in the negative transformed y direction, ignoring scale
  221. mgr.rotateGripConnector.setAttribute('x1', nbax + (nbaw)/2);
  222. mgr.rotateGripConnector.setAttribute('y1', nbay);
  223. mgr.rotateGripConnector.setAttribute('x2', nbax + (nbaw)/2);
  224. mgr.rotateGripConnector.setAttribute('y2', nbay - (gripRadius*5));
  225. mgr.rotateGrip.setAttribute('cx', nbax + (nbaw)/2);
  226. mgr.rotateGrip.setAttribute('cy', nbay - (gripRadius*5));
  227. // }
  228. svgFactory_.svgRoot().unsuspendRedraw(sr_handle);
  229. };
  230. // Class: svgedit.select.SelectorManager
  231. svgedit.select.SelectorManager = function() {
  232. // this will hold the <g> element that contains all selector rects/grips
  233. this.selectorParentGroup = null;
  234. // this is a special rect that is used for multi-select
  235. this.rubberBandBox = null;
  236. // this will hold objects of type svgedit.select.Selector (see above)
  237. this.selectors = [];
  238. // this holds a map of SVG elements to their Selector object
  239. this.selectorMap = {};
  240. // this holds a reference to the grip elements
  241. this.selectorGrips = {
  242. 'nw': null,
  243. 'n' : null,
  244. 'ne': null,
  245. 'e' : null,
  246. 'se': null,
  247. 's' : null,
  248. 'sw': null,
  249. 'w' : null
  250. };
  251. this.selectorGripsGroup = null;
  252. this.rotateGripConnector = null;
  253. this.rotateGrip = null;
  254. this.initGroup();
  255. };
  256. // Function: svgedit.select.SelectorManager.initGroup
  257. // Resets the parent selector group element
  258. svgedit.select.SelectorManager.prototype.initGroup = function() {
  259. // remove old selector parent group if it existed
  260. if (this.selectorParentGroup && this.selectorParentGroup.parentNode) {
  261. this.selectorParentGroup.parentNode.removeChild(this.selectorParentGroup);
  262. }
  263. // create parent selector group and add it to svgroot
  264. this.selectorParentGroup = svgFactory_.createSVGElement({
  265. 'element': 'g',
  266. 'attr': {'id': 'selectorParentGroup'}
  267. });
  268. this.selectorGripsGroup = svgFactory_.createSVGElement({
  269. 'element': 'g',
  270. 'attr': {'display': 'none'}
  271. });
  272. this.selectorParentGroup.appendChild(this.selectorGripsGroup);
  273. svgFactory_.svgRoot().appendChild(this.selectorParentGroup);
  274. this.selectorMap = {};
  275. this.selectors = [];
  276. this.rubberBandBox = null;
  277. // add the corner grips
  278. var dir;
  279. for (dir in this.selectorGrips) {
  280. var grip = svgFactory_.createSVGElement({
  281. 'element': 'circle',
  282. 'attr': {
  283. 'id': ('selectorGrip_resize_' + dir),
  284. 'fill': '#22C',
  285. 'r': gripRadius,
  286. 'style': ('cursor:' + dir + '-resize'),
  287. // This expands the mouse-able area of the grips making them
  288. // easier to grab with the mouse.
  289. // This works in Opera and WebKit, but does not work in Firefox
  290. // see https://bugzilla.mozilla.org/show_bug.cgi?id=500174
  291. 'stroke-width': 2,
  292. 'pointer-events': 'all'
  293. }
  294. });
  295. $.data(grip, 'dir', dir);
  296. $.data(grip, 'type', 'resize');
  297. this.selectorGrips[dir] = this.selectorGripsGroup.appendChild(grip);
  298. }
  299. // add rotator elems
  300. this.rotateGripConnector = this.selectorGripsGroup.appendChild(
  301. svgFactory_.createSVGElement({
  302. 'element': 'line',
  303. 'attr': {
  304. 'id': ('selectorGrip_rotateconnector'),
  305. 'stroke': '#22C',
  306. 'stroke-width': '1'
  307. }
  308. })
  309. );
  310. this.rotateGrip = this.selectorGripsGroup.appendChild(
  311. svgFactory_.createSVGElement({
  312. 'element': 'circle',
  313. 'attr': {
  314. 'id': 'selectorGrip_rotate',
  315. 'fill': 'lime',
  316. 'r': gripRadius,
  317. 'stroke': '#22C',
  318. 'stroke-width': 2,
  319. 'style': 'cursor:url(' + config_.imgPath + 'rotate.png) 12 12, auto;'
  320. }
  321. })
  322. );
  323. $.data(this.rotateGrip, 'type', 'rotate');
  324. if ($('#canvasBackground').length) {return;}
  325. var dims = config_.dimensions;
  326. var canvasbg = svgFactory_.createSVGElement({
  327. 'element': 'svg',
  328. 'attr': {
  329. 'id': 'canvasBackground',
  330. 'width': dims[0],
  331. 'height': dims[1],
  332. 'x': 0,
  333. 'y': 0,
  334. 'overflow': (svgedit.browser.isWebkit() ? 'none' : 'visible'), // Chrome 7 has a problem with this when zooming out
  335. 'style': 'pointer-events:none'
  336. }
  337. });
  338. var rect = svgFactory_.createSVGElement({
  339. 'element': 'rect',
  340. 'attr': {
  341. 'width': '100%',
  342. 'height': '100%',
  343. 'x': 0,
  344. 'y': 0,
  345. 'stroke-width': 1,
  346. 'stroke': '#000',
  347. 'fill': '#FFF',
  348. 'style': 'pointer-events:none'
  349. }
  350. });
  351. // Both Firefox and WebKit are too slow with this filter region (especially at higher
  352. // zoom levels) and Opera has at least one bug
  353. // if (!svgedit.browser.isOpera()) rect.setAttribute('filter', 'url(#canvashadow)');
  354. canvasbg.appendChild(rect);
  355. svgFactory_.svgRoot().insertBefore(canvasbg, svgFactory_.svgContent());
  356. };
  357. // Function: svgedit.select.SelectorManager.requestSelector
  358. // Returns the selector based on the given element
  359. //
  360. // Parameters:
  361. // elem - DOM element to get the selector for
  362. svgedit.select.SelectorManager.prototype.requestSelector = function(elem) {
  363. if (elem == null) {return null;}
  364. var i,
  365. N = this.selectors.length;
  366. // If we've already acquired one for this element, return it.
  367. if (typeof(this.selectorMap[elem.id]) == 'object') {
  368. this.selectorMap[elem.id].locked = true;
  369. return this.selectorMap[elem.id];
  370. }
  371. for (i = 0; i < N; ++i) {
  372. if (this.selectors[i] && !this.selectors[i].locked) {
  373. this.selectors[i].locked = true;
  374. this.selectors[i].reset(elem);
  375. this.selectorMap[elem.id] = this.selectors[i];
  376. return this.selectors[i];
  377. }
  378. }
  379. // if we reached here, no available selectors were found, we create one
  380. this.selectors[N] = new svgedit.select.Selector(N, elem);
  381. this.selectorParentGroup.appendChild(this.selectors[N].selectorGroup);
  382. this.selectorMap[elem.id] = this.selectors[N];
  383. return this.selectors[N];
  384. };
  385. // Function: svgedit.select.SelectorManager.releaseSelector
  386. // Removes the selector of the given element (hides selection box)
  387. //
  388. // Parameters:
  389. // elem - DOM element to remove the selector for
  390. svgedit.select.SelectorManager.prototype.releaseSelector = function(elem) {
  391. if (elem == null) {return;}
  392. var i,
  393. N = this.selectors.length,
  394. sel = this.selectorMap[elem.id];
  395. for (i = 0; i < N; ++i) {
  396. if (this.selectors[i] && this.selectors[i] == sel) {
  397. if (sel.locked == false) {
  398. // TODO(codedread): Ensure this exists in this module.
  399. console.log('WARNING! selector was released but was already unlocked');
  400. }
  401. delete this.selectorMap[elem.id];
  402. sel.locked = false;
  403. sel.selectedElement = null;
  404. sel.showGrips(false);
  405. // remove from DOM and store reference in JS but only if it exists in the DOM
  406. try {
  407. sel.selectorGroup.setAttribute('display', 'none');
  408. } catch(e) { }
  409. break;
  410. }
  411. }
  412. };
  413. // Function: svgedit.select.SelectorManager.getRubberBandBox
  414. // Returns the rubberBandBox DOM element. This is the rectangle drawn by the user for selecting/zooming
  415. svgedit.select.SelectorManager.prototype.getRubberBandBox = function() {
  416. if (!this.rubberBandBox) {
  417. this.rubberBandBox = this.selectorParentGroup.appendChild(
  418. svgFactory_.createSVGElement({
  419. 'element': 'rect',
  420. 'attr': {
  421. 'id': 'selectorRubberBand',
  422. 'fill': '#22C',
  423. 'fill-opacity': 0.15,
  424. 'stroke': '#22C',
  425. 'stroke-width': 0.5,
  426. 'display': 'none',
  427. 'style': 'pointer-events:none'
  428. }
  429. })
  430. );
  431. }
  432. return this.rubberBandBox;
  433. };
  434. /**
  435. * Interface: svgedit.select.SVGFactory
  436. * An object that creates SVG elements for the canvas.
  437. *
  438. * interface svgedit.select.SVGFactory {
  439. * SVGElement createSVGElement(jsonMap);
  440. * SVGSVGElement svgRoot();
  441. * SVGSVGElement svgContent();
  442. *
  443. * Number currentZoom();
  444. * Object getStrokedBBox(Element[]); // TODO(codedread): Remove when getStrokedBBox() has been put into svgutils.js
  445. * }
  446. */
  447. /**
  448. * Function: svgedit.select.init()
  449. * Initializes this module.
  450. *
  451. * Parameters:
  452. * config - an object containing configurable parameters (imgPath)
  453. * svgFactory - an object implementing the SVGFactory interface (see above).
  454. */
  455. svgedit.select.init = function(config, svgFactory) {
  456. config_ = config;
  457. svgFactory_ = svgFactory;
  458. selectorManager_ = new svgedit.select.SelectorManager();
  459. };
  460. /**
  461. * Function: svgedit.select.getSelectorManager
  462. *
  463. * Returns:
  464. * The SelectorManager instance.
  465. */
  466. svgedit.select.getSelectorManager = function() {
  467. return selectorManager_;
  468. };
  469. }());