draw.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /**
  2. * Package: svgedit.draw
  3. *
  4. * Licensed under the Apache License, Version 2
  5. *
  6. * Copyright(c) 2011 Jeff Schiller
  7. */
  8. // Dependencies:
  9. // 1) jQuery
  10. // 2) browser.js
  11. // 3) svgutils.js
  12. var svgedit = svgedit || {};
  13. (function() {
  14. if (!svgedit.draw) {
  15. svgedit.draw = {};
  16. }
  17. var svg_ns = "http://www.w3.org/2000/svg";
  18. var se_ns = "http://svg-edit.googlecode.com";
  19. var xmlns_ns = "http://www.w3.org/2000/xmlns/";
  20. var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use';
  21. var visElems_arr = visElems.split(',');
  22. var RandomizeModes = {
  23. LET_DOCUMENT_DECIDE: 0,
  24. ALWAYS_RANDOMIZE: 1,
  25. NEVER_RANDOMIZE: 2
  26. };
  27. var randomize_ids = RandomizeModes.LET_DOCUMENT_DECIDE;
  28. /**
  29. * This class encapsulates the concept of a layer in the drawing
  30. * @param name {String} Layer name
  31. * @param child {SVGGElement} Layer SVG group.
  32. */
  33. svgedit.draw.Layer = function(name, group) {
  34. this.name_ = name;
  35. this.group_ = group;
  36. };
  37. svgedit.draw.Layer.prototype.getName = function() {
  38. return this.name_;
  39. };
  40. svgedit.draw.Layer.prototype.getGroup = function() {
  41. return this.group_;
  42. };
  43. // Called to ensure that drawings will or will not have randomized ids.
  44. // The current_drawing will have its nonce set if it doesn't already.
  45. //
  46. // Params:
  47. // enableRandomization - flag indicating if documents should have randomized ids
  48. svgedit.draw.randomizeIds = function(enableRandomization, current_drawing) {
  49. randomize_ids = enableRandomization == false ?
  50. RandomizeModes.NEVER_RANDOMIZE :
  51. RandomizeModes.ALWAYS_RANDOMIZE;
  52. if (randomize_ids == RandomizeModes.ALWAYS_RANDOMIZE && !current_drawing.getNonce()) {
  53. current_drawing.setNonce(Math.floor(Math.random() * 100001));
  54. } else if (randomize_ids == RandomizeModes.NEVER_RANDOMIZE && current_drawing.getNonce()) {
  55. current_drawing.clearNonce();
  56. }
  57. };
  58. /**
  59. * This class encapsulates the concept of a SVG-edit drawing
  60. *
  61. * @param svgElem {SVGSVGElement} The SVG DOM Element that this JS object
  62. * encapsulates. If the svgElem has a se:nonce attribute on it, then
  63. * IDs will use the nonce as they are generated.
  64. * @param opt_idPrefix {String} The ID prefix to use. Defaults to "svg_"
  65. * if not specified.
  66. */
  67. svgedit.draw.Drawing = function(svgElem, opt_idPrefix) {
  68. if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI ||
  69. svgElem.tagName != 'svg' || svgElem.namespaceURI != svg_ns) {
  70. throw "Error: svgedit.draw.Drawing instance initialized without a <svg> element";
  71. }
  72. /**
  73. * The SVG DOM Element that represents this drawing.
  74. * @type {SVGSVGElement}
  75. */
  76. this.svgElem_ = svgElem;
  77. /**
  78. * The latest object number used in this drawing.
  79. * @type {number}
  80. */
  81. this.obj_num = 0;
  82. /**
  83. * The prefix to prepend to each element id in the drawing.
  84. * @type {String}
  85. */
  86. this.idPrefix = opt_idPrefix || "svg_";
  87. /**
  88. * An array of released element ids to immediately reuse.
  89. * @type {Array.<number>}
  90. */
  91. this.releasedNums = [];
  92. /**
  93. * The z-ordered array of tuples containing layer names and <g> elements.
  94. * The first layer is the one at the bottom of the rendering.
  95. * TODO: Turn this into an Array.<Layer>
  96. * @type {Array.<Array.<String, SVGGElement>>}
  97. */
  98. this.all_layers = [];
  99. /**
  100. * The current layer being used.
  101. * TODO: Make this a {Layer}.
  102. * @type {SVGGElement}
  103. */
  104. this.current_layer = null;
  105. /**
  106. * The nonce to use to uniquely identify elements across drawings.
  107. * @type {!String}
  108. */
  109. this.nonce_ = "";
  110. var n = this.svgElem_.getAttributeNS(se_ns, 'nonce');
  111. // If already set in the DOM, use the nonce throughout the document
  112. // else, if randomizeIds(true) has been called, create and set the nonce.
  113. if (!!n && randomize_ids != RandomizeModes.NEVER_RANDOMIZE) {
  114. this.nonce_ = n;
  115. } else if (randomize_ids == RandomizeModes.ALWAYS_RANDOMIZE) {
  116. this.setNonce(Math.floor(Math.random() * 100001));
  117. }
  118. };
  119. svgedit.draw.Drawing.prototype.getElem_ = function(id) {
  120. if(this.svgElem_.querySelector) {
  121. // querySelector lookup
  122. return this.svgElem_.querySelector('#'+id);
  123. } else {
  124. // jQuery lookup: twice as slow as xpath in FF
  125. return $(this.svgElem_).find('[id=' + id + ']')[0];
  126. }
  127. };
  128. svgedit.draw.Drawing.prototype.getSvgElem = function() {
  129. return this.svgElem_;
  130. };
  131. svgedit.draw.Drawing.prototype.getNonce = function() {
  132. return this.nonce_;
  133. };
  134. svgedit.draw.Drawing.prototype.setNonce = function(n) {
  135. this.svgElem_.setAttributeNS(xmlns_ns, 'xmlns:se', se_ns);
  136. this.svgElem_.setAttributeNS(se_ns, 'se:nonce', n);
  137. this.nonce_ = n;
  138. };
  139. svgedit.draw.Drawing.prototype.clearNonce = function() {
  140. // We deliberately leave any se:nonce attributes alone,
  141. // we just don't use it to randomize ids.
  142. this.nonce_ = "";
  143. };
  144. /**
  145. * Returns the latest object id as a string.
  146. * @return {String} The latest object Id.
  147. */
  148. svgedit.draw.Drawing.prototype.getId = function() {
  149. return this.nonce_ ?
  150. this.idPrefix + this.nonce_ +'_' + this.obj_num :
  151. this.idPrefix + this.obj_num;
  152. };
  153. /**
  154. * Returns the next object Id as a string.
  155. * @return {String} The next object Id to use.
  156. */
  157. svgedit.draw.Drawing.prototype.getNextId = function() {
  158. var oldObjNum = this.obj_num;
  159. var restoreOldObjNum = false;
  160. // If there are any released numbers in the release stack,
  161. // use the last one instead of the next obj_num.
  162. // We need to temporarily use obj_num as that is what getId() depends on.
  163. if (this.releasedNums.length > 0) {
  164. this.obj_num = this.releasedNums.pop();
  165. restoreOldObjNum = true;
  166. } else {
  167. // If we are not using a released id, then increment the obj_num.
  168. this.obj_num++;
  169. }
  170. // Ensure the ID does not exist.
  171. var id = this.getId();
  172. while (this.getElem_(id)) {
  173. if (restoreOldObjNum) {
  174. this.obj_num = oldObjNum;
  175. restoreOldObjNum = false;
  176. }
  177. this.obj_num++;
  178. id = this.getId();
  179. }
  180. // Restore the old object number if required.
  181. if (restoreOldObjNum) {
  182. this.obj_num = oldObjNum;
  183. }
  184. return id;
  185. };
  186. // Function: svgedit.draw.Drawing.releaseId
  187. // Releases the object Id, letting it be used as the next id in getNextId().
  188. // This method DOES NOT remove any elements from the DOM, it is expected
  189. // that client code will do this.
  190. //
  191. // Parameters:
  192. // id - The id to release.
  193. //
  194. // Returns:
  195. // True if the id was valid to be released, false otherwise.
  196. svgedit.draw.Drawing.prototype.releaseId = function(id) {
  197. // confirm if this is a valid id for this Document, else return false
  198. var front = this.idPrefix + (this.nonce_ ? this.nonce_ +'_' : '');
  199. if (typeof id != typeof '' || id.indexOf(front) != 0) {
  200. return false;
  201. }
  202. // extract the obj_num of this id
  203. var num = parseInt(id.substr(front.length));
  204. // if we didn't get a positive number or we already released this number
  205. // then return false.
  206. if (typeof num != typeof 1 || num <= 0 || this.releasedNums.indexOf(num) != -1) {
  207. return false;
  208. }
  209. // push the released number into the released queue
  210. this.releasedNums.push(num);
  211. return true;
  212. };
  213. // Function: svgedit.draw.Drawing.getNumLayers
  214. // Returns the number of layers in the current drawing.
  215. //
  216. // Returns:
  217. // The number of layers in the current drawing.
  218. svgedit.draw.Drawing.prototype.getNumLayers = function() {
  219. return this.all_layers.length;
  220. };
  221. // Function: svgedit.draw.Drawing.hasLayer
  222. // Check if layer with given name already exists
  223. svgedit.draw.Drawing.prototype.hasLayer = function(name) {
  224. for(var i = 0; i < this.getNumLayers(); i++) {
  225. if(this.all_layers[i][0] == name) return true;
  226. }
  227. return false;
  228. };
  229. // Function: svgedit.draw.Drawing.getLayerName
  230. // Returns the name of the ith layer. If the index is out of range, an empty string is returned.
  231. //
  232. // Parameters:
  233. // i - the zero-based index of the layer you are querying.
  234. //
  235. // Returns:
  236. // The name of the ith layer
  237. svgedit.draw.Drawing.prototype.getLayerName = function(i) {
  238. if (i >= 0 && i < this.getNumLayers()) {
  239. return this.all_layers[i][0];
  240. }
  241. return "";
  242. };
  243. // Function: svgedit.draw.Drawing.getCurrentLayer
  244. // Returns:
  245. // The SVGGElement representing the current layer.
  246. svgedit.draw.Drawing.prototype.getCurrentLayer = function() {
  247. return this.current_layer;
  248. };
  249. // Function: getCurrentLayerName
  250. // Returns the name of the currently selected layer. If an error occurs, an empty string
  251. // is returned.
  252. //
  253. // Returns:
  254. // The name of the currently active layer.
  255. svgedit.draw.Drawing.prototype.getCurrentLayerName = function() {
  256. for (var i = 0; i < this.getNumLayers(); ++i) {
  257. if (this.all_layers[i][1] == this.current_layer) {
  258. return this.getLayerName(i);
  259. }
  260. }
  261. return "";
  262. };
  263. // Function: setCurrentLayer
  264. // Sets the current layer. If the name is not a valid layer name, then this function returns
  265. // false. Otherwise it returns true. This is not an undo-able action.
  266. //
  267. // Parameters:
  268. // name - the name of the layer you want to switch to.
  269. //
  270. // Returns:
  271. // true if the current layer was switched, otherwise false
  272. svgedit.draw.Drawing.prototype.setCurrentLayer = function(name) {
  273. for (var i = 0; i < this.getNumLayers(); ++i) {
  274. if (name == this.getLayerName(i)) {
  275. if (this.current_layer != this.all_layers[i][1]) {
  276. this.current_layer.setAttribute("style", "pointer-events:none");
  277. this.current_layer = this.all_layers[i][1];
  278. this.current_layer.setAttribute("style", "pointer-events:all");
  279. }
  280. return true;
  281. }
  282. }
  283. return false;
  284. };
  285. // Function: svgedit.draw.Drawing.deleteCurrentLayer
  286. // Deletes the current layer from the drawing and then clears the selection. This function
  287. // then calls the 'changed' handler. This is an undoable action.
  288. // Returns:
  289. // The SVGGElement of the layer removed or null.
  290. svgedit.draw.Drawing.prototype.deleteCurrentLayer = function() {
  291. if (this.current_layer && this.getNumLayers() > 1) {
  292. // actually delete from the DOM and return it
  293. var parent = this.current_layer.parentNode;
  294. var nextSibling = this.current_layer.nextSibling;
  295. var oldLayerGroup = parent.removeChild(this.current_layer);
  296. this.identifyLayers();
  297. return oldLayerGroup;
  298. }
  299. return null;
  300. };
  301. // Function: svgedit.draw.Drawing.identifyLayers
  302. // Updates layer system and sets the current layer to the
  303. // top-most layer (last <g> child of this drawing).
  304. svgedit.draw.Drawing.prototype.identifyLayers = function() {
  305. this.all_layers = [];
  306. var numchildren = this.svgElem_.childNodes.length;
  307. // loop through all children of SVG element
  308. var orphans = [], layernames = [];
  309. var a_layer = null;
  310. var childgroups = false;
  311. for (var i = 0; i < numchildren; ++i) {
  312. var child = this.svgElem_.childNodes.item(i);
  313. // for each g, find its layer name
  314. if (child && child.nodeType == 1) {
  315. if (child.tagName == "g") {
  316. childgroups = true;
  317. var name = $("title",child).text();
  318. // Hack for Opera 10.60
  319. if(!name && svgedit.browser.isOpera() && child.querySelectorAll) {
  320. name = $(child.querySelectorAll('title')).text();
  321. }
  322. // store layer and name in global variable
  323. if (name) {
  324. layernames.push(name);
  325. this.all_layers.push( [name,child] );
  326. a_layer = child;
  327. svgedit.utilities.walkTree(child, function(e){e.setAttribute("style", "pointer-events:inherit");});
  328. a_layer.setAttribute("style", "pointer-events:none");
  329. }
  330. // if group did not have a name, it is an orphan
  331. else {
  332. orphans.push(child);
  333. }
  334. }
  335. // if child has is "visible" (i.e. not a <title> or <defs> element), then it is an orphan
  336. else if(~visElems_arr.indexOf(child.nodeName)) {
  337. var bb = svgedit.utilities.getBBox(child);
  338. orphans.push(child);
  339. }
  340. }
  341. }
  342. // create a new layer and add all the orphans to it
  343. var svgdoc = this.svgElem_.ownerDocument;
  344. if (orphans.length > 0 || !childgroups) {
  345. var i = 1;
  346. // TODO(codedread): What about internationalization of "Layer"?
  347. while (layernames.indexOf(("Layer " + i)) >= 0) { i++; }
  348. var newname = "Layer " + i;
  349. a_layer = svgdoc.createElementNS(svg_ns, "g");
  350. var layer_title = svgdoc.createElementNS(svg_ns, "title");
  351. layer_title.textContent = newname;
  352. a_layer.appendChild(layer_title);
  353. for (var j = 0; j < orphans.length; ++j) {
  354. a_layer.appendChild(orphans[j]);
  355. }
  356. this.svgElem_.appendChild(a_layer);
  357. this.all_layers.push( [newname, a_layer] );
  358. }
  359. svgedit.utilities.walkTree(a_layer, function(e){e.setAttribute("style","pointer-events:inherit");});
  360. this.current_layer = a_layer;
  361. this.current_layer.setAttribute("style","pointer-events:all");
  362. };
  363. // Function: svgedit.draw.Drawing.createLayer
  364. // Creates a new top-level layer in the drawing with the given name and
  365. // sets the current layer to it.
  366. //
  367. // Parameters:
  368. // name - The given name
  369. //
  370. // Returns:
  371. // The SVGGElement of the new layer, which is also the current layer
  372. // of this drawing.
  373. svgedit.draw.Drawing.prototype.createLayer = function(name) {
  374. var svgdoc = this.svgElem_.ownerDocument;
  375. var new_layer = svgdoc.createElementNS(svg_ns, "g");
  376. var layer_title = svgdoc.createElementNS(svg_ns, "title");
  377. layer_title.textContent = name;
  378. new_layer.appendChild(layer_title);
  379. this.svgElem_.appendChild(new_layer);
  380. this.identifyLayers();
  381. return new_layer;
  382. };
  383. // Function: svgedit.draw.Drawing.getLayerVisibility
  384. // Returns whether the layer is visible. If the layer name is not valid, then this function
  385. // returns false.
  386. //
  387. // Parameters:
  388. // layername - the name of the layer which you want to query.
  389. //
  390. // Returns:
  391. // The visibility state of the layer, or false if the layer name was invalid.
  392. svgedit.draw.Drawing.prototype.getLayerVisibility = function(layername) {
  393. // find the layer
  394. var layer = null;
  395. for (var i = 0; i < this.getNumLayers(); ++i) {
  396. if (this.getLayerName(i) == layername) {
  397. layer = this.all_layers[i][1];
  398. break;
  399. }
  400. }
  401. if (!layer) return false;
  402. return (layer.getAttribute('display') != 'none');
  403. };
  404. // Function: svgedit.draw.Drawing.setLayerVisibility
  405. // Sets the visibility of the layer. If the layer name is not valid, this function return
  406. // false, otherwise it returns true. This is an undo-able action.
  407. //
  408. // Parameters:
  409. // layername - the name of the layer to change the visibility
  410. // bVisible - true/false, whether the layer should be visible
  411. //
  412. // Returns:
  413. // The SVGGElement representing the layer if the layername was valid, otherwise null.
  414. svgedit.draw.Drawing.prototype.setLayerVisibility = function(layername, bVisible) {
  415. if (typeof bVisible != typeof true) {
  416. return null;
  417. }
  418. // find the layer
  419. var layer = null;
  420. for (var i = 0; i < this.getNumLayers(); ++i) {
  421. if (this.getLayerName(i) == layername) {
  422. layer = this.all_layers[i][1];
  423. break;
  424. }
  425. }
  426. if (!layer) return null;
  427. var oldDisplay = layer.getAttribute("display");
  428. if (!oldDisplay) oldDisplay = "inline";
  429. layer.setAttribute("display", bVisible ? "inline" : "none");
  430. return layer;
  431. };
  432. // Function: svgedit.draw.Drawing.getLayerOpacity
  433. // Returns the opacity of the given layer. If the input name is not a layer, null is returned.
  434. //
  435. // Parameters:
  436. // layername - name of the layer on which to get the opacity
  437. //
  438. // Returns:
  439. // The opacity value of the given layer. This will be a value between 0.0 and 1.0, or null
  440. // if layername is not a valid layer
  441. svgedit.draw.Drawing.prototype.getLayerOpacity = function(layername) {
  442. for (var i = 0; i < this.getNumLayers(); ++i) {
  443. if (this.getLayerName(i) == layername) {
  444. var g = this.all_layers[i][1];
  445. var opacity = g.getAttribute('opacity');
  446. if (!opacity) {
  447. opacity = '1.0';
  448. }
  449. return parseFloat(opacity);
  450. }
  451. }
  452. return null;
  453. };
  454. // Function: svgedit.draw.Drawing.setLayerOpacity
  455. // Sets the opacity of the given layer. If the input name is not a layer, nothing happens.
  456. // If opacity is not a value between 0.0 and 1.0, then nothing happens.
  457. //
  458. // Parameters:
  459. // layername - name of the layer on which to set the opacity
  460. // opacity - a float value in the range 0.0-1.0
  461. svgedit.draw.Drawing.prototype.setLayerOpacity = function(layername, opacity) {
  462. if (typeof opacity != typeof 1.0 || opacity < 0.0 || opacity > 1.0) {
  463. return;
  464. }
  465. for (var i = 0; i < this.getNumLayers(); ++i) {
  466. if (this.getLayerName(i) == layername) {
  467. var g = this.all_layers[i][1];
  468. g.setAttribute("opacity", opacity);
  469. break;
  470. }
  471. }
  472. };
  473. })();