select.js 15 KB

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