Stencil.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. // wrapped by build app
  2. define("dojox/drawing/manager/Stencil", ["dijit","dojo","dojox"], function(dijit,dojo,dojox){
  3. dojo.provide("dojox.drawing.manager.Stencil");
  4. (function(){
  5. var surface, surfaceNode;
  6. dojox.drawing.manager.Stencil = dojox.drawing.util.oo.declare(
  7. // summary:
  8. // The main class for tracking Stencils that are cretaed, added,
  9. // selected, or deleted. Also handles selections, multiple
  10. // selections, adding and removing from selections, and dragging
  11. // selections. It's this class that triggers the anchors to
  12. // appear on a Stencil and whther there are anchor on a multiple
  13. // select or not (currently not)
  14. //
  15. function(options){
  16. //
  17. // TODO: mixin props
  18. //
  19. surface = options.surface;
  20. this.canvas = options.canvas;
  21. this.defaults = dojox.drawing.defaults.copy();
  22. this.undo = options.undo;
  23. this.mouse = options.mouse;
  24. this.keys = options.keys;
  25. this.anchors = options.anchors;
  26. this.stencils = {};
  27. this.selectedStencils = {};
  28. this._mouseHandle = this.mouse.register(this);
  29. dojo.connect(this.keys, "onArrow", this, "onArrow");
  30. dojo.connect(this.keys, "onEsc", this, "deselect");
  31. dojo.connect(this.keys, "onDelete", this, "onDelete");
  32. },
  33. {
  34. _dragBegun: false,
  35. _wasDragged:false,
  36. _secondClick:false,
  37. _isBusy:false,
  38. setRecentStencil: function(stencil){
  39. // summary:
  40. // Keeps track of the most recent stencil interacted
  41. // with, whether created or selected.
  42. this.recent = stencil;
  43. },
  44. getRecentStencil: function(){
  45. // summary:
  46. // Returns the stencil most recently interacted
  47. // with whether it's last created or last selected
  48. return this.recent;
  49. },
  50. register: function(/*Object*/stencil){
  51. // summary:
  52. // Key method for adding Stencils. Stencils
  53. // can be added to the canvas without adding
  54. // them to this, but they won't have selection
  55. // or drag ability.
  56. //
  57. console.log("Selection.register ::::::", stencil.id);
  58. if(stencil.isText && !stencil.editMode && stencil.deleteEmptyCreate && !stencil.getText()){
  59. // created empty text field
  60. // defaults say to delete
  61. console.warn("EMPTY CREATE DELETE", stencil);
  62. stencil.destroy();
  63. return false;
  64. }
  65. this.stencils[stencil.id] = stencil;
  66. this.setRecentStencil(stencil);
  67. if(stencil.execText){
  68. if(stencil._text && !stencil.editMode){
  69. console.log("select text");
  70. this.selectItem(stencil);
  71. }
  72. stencil.connect("execText", this, function(){
  73. if(stencil.isText && stencil.deleteEmptyModify && !stencil.getText()){
  74. console.warn("EMPTY MOD DELETE", stencil);
  75. // text deleted
  76. // defaults say to delete
  77. this.deleteItem(stencil);
  78. }else if(stencil.selectOnExec){
  79. this.selectItem(stencil);
  80. }
  81. });
  82. }
  83. stencil.connect("deselect", this, function(){
  84. if(!this._isBusy && this.isSelected(stencil)){
  85. // called from within stencil. do action.
  86. this.deselectItem(stencil);
  87. }
  88. });
  89. stencil.connect("select", this, function(){
  90. if(!this._isBusy && !this.isSelected(stencil)){
  91. // called from within stencil. do action.
  92. this.selectItem(stencil);
  93. }
  94. });
  95. return stencil;
  96. },
  97. unregister: function(/*Object*/stencil){
  98. // summary:
  99. // Method for removing Stencils from the manager.
  100. // This doesn't delete them, only removes them from
  101. // the list.
  102. //
  103. console.log("Selection.unregister ::::::", stencil.id, "sel:", stencil.selected);
  104. if(stencil){
  105. stencil.selected && this.onDeselect(stencil);
  106. delete this.stencils[stencil.id];
  107. }
  108. },
  109. onArrow: function(/*Key Event*/evt){
  110. // summary:
  111. // Moves selection based on keyboard arrow keys
  112. //
  113. // FIXME: Check constraints
  114. if(this.hasSelected()){
  115. this.saveThrottledState();
  116. this.group.applyTransform({dx:evt.x, dy: evt.y});
  117. }
  118. },
  119. _throttleVrl:null,
  120. _throttle: false,
  121. throttleTime:400,
  122. _lastmxx:-1,
  123. _lastmxy:-1,
  124. saveMoveState: function(){
  125. // summary:
  126. // Internal. Used for the prototype undo stack.
  127. // Saves selection position.
  128. //
  129. var mx = this.group.getTransform();
  130. if(mx.dx == this._lastmxx && mx.dy == this._lastmxy){ return; }
  131. this._lastmxx = mx.dx;
  132. this._lastmxy = mx.dy;
  133. //console.warn("SAVE MOVE!", mx.dx, mx.dy);
  134. this.undo.add({
  135. before:dojo.hitch(this.group, "setTransform", mx)
  136. });
  137. },
  138. saveThrottledState: function(){
  139. // summary:
  140. // Internal. Used for the prototype undo stack.
  141. // Prevents an undo point on every mouse move.
  142. // Only does a point when the mouse hesitates.
  143. //
  144. clearTimeout(this._throttleVrl);
  145. clearInterval(this._throttleVrl);
  146. this._throttleVrl = setTimeout(dojo.hitch(this, function(){
  147. this._throttle = false;
  148. this.saveMoveState();
  149. }), this.throttleTime);
  150. if(this._throttle){ return; }
  151. this._throttle = true;
  152. this.saveMoveState();
  153. },
  154. unDelete: function(/*Array*/stencils){
  155. // summary:
  156. // Undeletes a stencil. Used in undo stack.
  157. //
  158. console.log("unDelete:", stencils);
  159. for(var s in stencils){
  160. stencils[s].render();
  161. this.onSelect(stencils[s]);
  162. }
  163. },
  164. onDelete: function(/*Boolean*/noundo){
  165. // summary:
  166. // Event fired on deletion of a stencil
  167. //
  168. console.log("Stencil onDelete", noundo);
  169. if(noundo!==true){
  170. this.undo.add({
  171. before:dojo.hitch(this, "unDelete", this.selectedStencils),
  172. after:dojo.hitch(this, "onDelete", true)
  173. });
  174. }
  175. this.withSelected(function(m){
  176. this.anchors.remove(m);
  177. var id = m.id;
  178. console.log("delete:", m);
  179. m.destroy();
  180. delete this.stencils[id];
  181. });
  182. this.selectedStencils = {};
  183. },
  184. deleteItem: function(/*Object*/stencil){
  185. // summary:
  186. // Deletes a stencil.
  187. // NOTE: supports limited undo.
  188. //
  189. // manipulating the selection to fire onDelete properly
  190. if(this.hasSelected()){
  191. // there is a selection
  192. var sids = [];
  193. for(var m in this.selectedStencils){
  194. if(this.selectedStencils.id == stencil.id){
  195. if(this.hasSelected()==1){
  196. // the deleting stencil is the only one selected
  197. this.onDelete();
  198. return;
  199. }
  200. }else{
  201. sids.push(this.selectedStencils.id);
  202. }
  203. }
  204. // remove selection, delete, restore selection
  205. this.deselect();
  206. this.selectItem(stencil);
  207. this.onDelete();
  208. dojo.forEach(sids, function(id){
  209. this.selectItem(id);
  210. }, this);
  211. }else{
  212. // there is not a selection. select it, delete it
  213. this.selectItem(stencil);
  214. // now delete selection
  215. this.onDelete();
  216. }
  217. },
  218. removeAll: function(){
  219. // summary:
  220. // Deletes all Stencils on the canvas.
  221. this.selectAll();
  222. this._isBusy = true;
  223. this.onDelete();
  224. this.stencils = {};
  225. this._isBusy = false;
  226. },
  227. setSelectionGroup: function(){
  228. // summary:
  229. // Internal. Creates a new selection group
  230. // used to hold selected stencils.
  231. //
  232. this.withSelected(function(m){
  233. this.onDeselect(m, true);
  234. });
  235. if(this.group){
  236. surface.remove(this.group);
  237. this.group.removeShape();
  238. }
  239. this.group = surface.createGroup();
  240. this.group.setTransform({dx:0, dy: 0});
  241. this.withSelected(function(m){
  242. this.group.add(m.container);
  243. m.select();
  244. });
  245. },
  246. setConstraint: function(){
  247. // summary:
  248. // Internal. Gets all selected stencils' coordinates
  249. // and determines how far left and up the selection
  250. // can go without going below zero
  251. //
  252. var t = Infinity, l = Infinity;
  253. this.withSelected(function(m){
  254. var o = m.getBounds();
  255. t = Math.min(o.y1, t);
  256. l = Math.min(o.x1, l);
  257. });
  258. this.constrain = {l:-l, t:-t};
  259. },
  260. onDeselect: function(stencil, keepObject){
  261. // summary:
  262. // Event fired on deselection of a stencil
  263. //
  264. if(!keepObject){
  265. delete this.selectedStencils[stencil.id];
  266. }
  267. //console.log('onDeselect, keep:', keepObject, "stencil:", stencil.type)
  268. this.anchors.remove(stencil);
  269. surface.add(stencil.container);
  270. stencil.selected && stencil.deselect();
  271. stencil.applyTransform(this.group.getTransform());
  272. },
  273. deselectItem: function(/*Object*/stencil){
  274. // summary:
  275. // Deselect passed stencil
  276. //
  277. // note: just keeping with standardized methods
  278. this.onDeselect(stencil);
  279. },
  280. deselect: function(){ // all stencils
  281. // summary:
  282. // Deselect all stencils
  283. //
  284. this.withSelected(function(m){
  285. this.onDeselect(m);
  286. });
  287. this._dragBegun = false;
  288. this._wasDragged = false;
  289. },
  290. onSelect: function(/*Object*/stencil){
  291. // summary:
  292. // Event fired on selection of a stencil
  293. //
  294. //console.log("stencil.onSelect", stencil);
  295. if(!stencil){
  296. console.error("null stencil is not selected:", this.stencils)
  297. }
  298. if(this.selectedStencils[stencil.id]){ return; }
  299. this.selectedStencils[stencil.id] = stencil;
  300. this.group.add(stencil.container);
  301. stencil.select();
  302. if(this.hasSelected()==1){
  303. this.anchors.add(stencil, this.group);
  304. }
  305. },
  306. selectAll: function(){
  307. // summary:
  308. // Selects all items
  309. this._isBusy = true;
  310. for(var m in this.stencils){
  311. //if(!this.stencils[m].selected){
  312. this.selectItem(m);
  313. //}
  314. }
  315. this._isBusy = false;
  316. },
  317. selectItem: function(/*String|Object*/ idOrItem){
  318. // summary:
  319. // Method used to select a stencil.
  320. //
  321. var id = typeof(idOrItem)=="string" ? idOrItem : idOrItem.id;
  322. var stencil = this.stencils[id];
  323. this.setSelectionGroup();
  324. this.onSelect(stencil);
  325. this.group.moveToFront();
  326. this.setConstraint();
  327. },
  328. onLabelDoubleClick: function(/*EventObject*/obj){
  329. // summary:
  330. // Event to connect a textbox to
  331. // for label edits
  332. console.info("mgr.onLabelDoubleClick:", obj);
  333. if(this.selectedStencils[obj.id]){
  334. this.deselect();
  335. }
  336. },
  337. onStencilDoubleClick: function(/*EventObject*/obj){
  338. // summary:
  339. // Event fired on the double-click of a stencil
  340. //
  341. console.info("mgr.onStencilDoubleClick:", obj);
  342. if(this.selectedStencils[obj.id]){
  343. if(this.selectedStencils[obj.id].edit){
  344. console.info("Mgr Stencil Edit -> ", this.selectedStencils[obj.id]);
  345. var m = this.selectedStencils[obj.id];
  346. // deselect must happen first to set the transform
  347. // then edit knows where to set the text box
  348. m.editMode = true;
  349. this.deselect();
  350. m.edit();
  351. }
  352. }
  353. },
  354. onAnchorUp: function(){
  355. // summary:
  356. // Event fire on mouseup off of an anchor point
  357. this.setConstraint();
  358. },
  359. onStencilDown: function(/*EventObject*/obj, evt){
  360. // summary:
  361. // Event fired on mousedown on a stencil
  362. //
  363. console.info(" >>> onStencilDown:", obj.id, this.keys.meta);
  364. if(!this.stencils[obj.id]){ return; }
  365. this.setRecentStencil(this.stencils[obj.id]);
  366. this._isBusy = true;
  367. if(this.selectedStencils[obj.id] && this.keys.meta){
  368. if(dojo.isMac && this.keys.cmmd){
  369. // block context menu
  370. }
  371. console.log(" shift remove");
  372. this.onDeselect(this.selectedStencils[obj.id]);
  373. if(this.hasSelected()==1){
  374. this.withSelected(function(m){
  375. this.anchors.add(m, this.group);
  376. });
  377. }
  378. this.group.moveToFront();
  379. this.setConstraint();
  380. return;
  381. }else if(this.selectedStencils[obj.id]){
  382. console.log(" clicked on selected");
  383. // clicking on same selected item(s)
  384. // RESET OFFSETS
  385. var mx = this.group.getTransform();
  386. this._offx = obj.x - mx.dx;
  387. this._offy = obj.y - mx.dy;
  388. return;
  389. }else if(!this.keys.meta){
  390. console.log(" deselect all");
  391. this.deselect();
  392. }else{
  393. // meta-key add
  394. //console.log("reset sel and add stencil")
  395. }
  396. console.log(" add stencil to selection");
  397. // add a stencil
  398. this.selectItem(obj.id);
  399. mx = this.group.getTransform();
  400. this._offx = obj.x - mx.dx;
  401. this._offy = obj.y - mx.dx;
  402. this.orgx = obj.x;
  403. this.orgy = obj.y;
  404. this._isBusy = false;
  405. // TODO:
  406. // dojo.style(surfaceNode, "cursor", "pointer");
  407. // TODO:
  408. this.undo.add({
  409. before:function(){
  410. },
  411. after: function(){
  412. }
  413. });
  414. },
  415. onLabelDown: function(/*EventObject*/obj, evt){
  416. // summary:
  417. // Event fired on mousedown of a stencil's label
  418. // Because it's an annotation the id will be the
  419. // master stencil.
  420. //console.info("===============>>>Label click: ",obj, " evt: ",evt);
  421. this.onStencilDown(obj,evt);
  422. },
  423. onStencilUp: function(/*EventObject*/obj){
  424. // summary:
  425. // Event fired on mouseup off of a stencil
  426. //
  427. },
  428. onLabelUp: function(/*EventObject*/obj){
  429. this.onStencilUp(obj);
  430. },
  431. onStencilDrag: function(/*EventObject*/obj){
  432. // summary:
  433. // Event fired on every mousemove of a stencil drag
  434. //
  435. if(!this._dragBegun){
  436. // bug, in FF anyway - first mouse move shows x=0
  437. // the 'else' fixes it
  438. this.onBeginDrag(obj);
  439. this._dragBegun = true;
  440. }else{
  441. this.saveThrottledState();
  442. var x = obj.x - obj.last.x,
  443. y = obj.y - obj.last.y,
  444. c = this.constrain,
  445. mz = this.defaults.anchors.marginZero;
  446. x = obj.x - this._offx;
  447. y = obj.y - this._offy;
  448. if(x < c.l + mz){
  449. x = c.l + mz;
  450. }
  451. if(y < c.t + mz){
  452. y = c.t + mz;
  453. }
  454. this.group.setTransform({
  455. dx: x,
  456. dy: y
  457. });
  458. }
  459. },
  460. onLabelDrag: function(/*EventObject*/obj){
  461. this.onStencilDrag(obj);
  462. },
  463. onDragEnd: function(/*EventObject*/obj){
  464. // summary:
  465. // Event fired at the end of a stencil drag
  466. //
  467. this._dragBegun = false;
  468. },
  469. onBeginDrag: function(/*EventObject*/obj){
  470. // summary:
  471. // Event fired at the beginning of a stencil drag
  472. //
  473. this._wasDragged = true;
  474. },
  475. onDown: function(/*EventObject*/obj){
  476. // summary:
  477. // Event fired on mousedown on the canvas
  478. //
  479. this.deselect();
  480. },
  481. onStencilOver: function(obj){
  482. // summary:
  483. // This changes the cursor when hovering over
  484. // a selectable stencil.
  485. //console.log("OVER")
  486. dojo.style(obj.id, "cursor", "move");
  487. },
  488. onStencilOut: function(obj){
  489. // summary:
  490. // This restores the cursor.
  491. //console.log("OUT")
  492. dojo.style(obj.id, "cursor", "crosshair");
  493. },
  494. exporter: function(){
  495. // summary:
  496. // Collects all Stencil data and returns an
  497. // Array of objects.
  498. var items = [];
  499. for(var m in this.stencils){
  500. this.stencils[m].enabled && items.push(this.stencils[m].exporter());
  501. }
  502. return items; // Array
  503. },
  504. listStencils: function(){
  505. return this.stencils;
  506. },
  507. toSelected: function(/*String*/func){
  508. // summary:
  509. // Convenience function calls function *within*
  510. // all selected stencils
  511. var args = Array.prototype.slice.call(arguments).splice(1);
  512. for(var m in this.selectedStencils){
  513. var item = this.selectedStencils[m];
  514. item[func].apply(item, args);
  515. }
  516. },
  517. withSelected: function(/*Function*/func){
  518. // summary:
  519. // Convenience function calls function on
  520. // all selected stencils
  521. var f = dojo.hitch(this, func);
  522. for(var m in this.selectedStencils){
  523. f(this.selectedStencils[m]);
  524. }
  525. },
  526. withUnselected: function(/*Function*/func){
  527. // summary:
  528. // Convenience function calls function on
  529. // all stencils that are not selected
  530. var f = dojo.hitch(this, func);
  531. for(var m in this.stencils){
  532. !this.stencils[m].selected && f(this.stencils[m]);
  533. }
  534. },
  535. withStencils: function(/*Function*/func){
  536. // summary:
  537. // Convenience function calls function on
  538. // all stencils
  539. var f = dojo.hitch(this, func);
  540. for(var m in this.stencils){
  541. f(this.stencils[m]);
  542. }
  543. },
  544. hasSelected: function(){
  545. // summary:
  546. // Returns number of selected (generally used
  547. // as truthy or falsey)
  548. //
  549. // FIXME: should be areSelected?
  550. var ln = 0;
  551. for(var m in this.selectedStencils){ ln++; }
  552. return ln; // Number
  553. },
  554. isSelected: function(/*Object*/stencil){
  555. // summary:
  556. // Returns if passed stencil is selected or not
  557. // based on internal collection, not on stencil
  558. // boolean
  559. return !!this.selectedStencils[stencil.id]; // Boolean
  560. }
  561. }
  562. );
  563. })();
  564. });