646 lines
16 KiB
JavaScript
646 lines
16 KiB
JavaScript
//>>built
|
|
// wrapped by build app
|
|
define("dojox/drawing/manager/Stencil", ["dijit","dojo","dojox"], function(dijit,dojo,dojox){
|
|
dojo.provide("dojox.drawing.manager.Stencil");
|
|
|
|
(function(){
|
|
var surface, surfaceNode;
|
|
dojox.drawing.manager.Stencil = dojox.drawing.util.oo.declare(
|
|
// summary:
|
|
// The main class for tracking Stencils that are cretaed, added,
|
|
// selected, or deleted. Also handles selections, multiple
|
|
// selections, adding and removing from selections, and dragging
|
|
// selections. It's this class that triggers the anchors to
|
|
// appear on a Stencil and whther there are anchor on a multiple
|
|
// select or not (currently not)
|
|
//
|
|
function(options){
|
|
//
|
|
// TODO: mixin props
|
|
//
|
|
surface = options.surface;
|
|
this.canvas = options.canvas;
|
|
|
|
this.defaults = dojox.drawing.defaults.copy();
|
|
this.undo = options.undo;
|
|
this.mouse = options.mouse;
|
|
this.keys = options.keys;
|
|
this.anchors = options.anchors;
|
|
this.stencils = {};
|
|
this.selectedStencils = {};
|
|
this._mouseHandle = this.mouse.register(this);
|
|
|
|
dojo.connect(this.keys, "onArrow", this, "onArrow");
|
|
dojo.connect(this.keys, "onEsc", this, "deselect");
|
|
dojo.connect(this.keys, "onDelete", this, "onDelete");
|
|
|
|
},
|
|
{
|
|
_dragBegun: false,
|
|
_wasDragged:false,
|
|
_secondClick:false,
|
|
_isBusy:false,
|
|
|
|
setRecentStencil: function(stencil){
|
|
// summary:
|
|
// Keeps track of the most recent stencil interacted
|
|
// with, whether created or selected.
|
|
this.recent = stencil;
|
|
},
|
|
|
|
getRecentStencil: function(){
|
|
// summary:
|
|
// Returns the stencil most recently interacted
|
|
// with whether it's last created or last selected
|
|
return this.recent;
|
|
},
|
|
|
|
register: function(/*Object*/stencil){
|
|
// summary:
|
|
// Key method for adding Stencils. Stencils
|
|
// can be added to the canvas without adding
|
|
// them to this, but they won't have selection
|
|
// or drag ability.
|
|
//
|
|
console.log("Selection.register ::::::", stencil.id);
|
|
if(stencil.isText && !stencil.editMode && stencil.deleteEmptyCreate && !stencil.getText()){
|
|
// created empty text field
|
|
// defaults say to delete
|
|
console.warn("EMPTY CREATE DELETE", stencil);
|
|
stencil.destroy();
|
|
return false;
|
|
}
|
|
|
|
this.stencils[stencil.id] = stencil;
|
|
this.setRecentStencil(stencil);
|
|
|
|
if(stencil.execText){
|
|
if(stencil._text && !stencil.editMode){
|
|
console.log("select text");
|
|
this.selectItem(stencil);
|
|
}
|
|
stencil.connect("execText", this, function(){
|
|
if(stencil.isText && stencil.deleteEmptyModify && !stencil.getText()){
|
|
console.warn("EMPTY MOD DELETE", stencil);
|
|
// text deleted
|
|
// defaults say to delete
|
|
this.deleteItem(stencil);
|
|
}else if(stencil.selectOnExec){
|
|
this.selectItem(stencil);
|
|
}
|
|
});
|
|
}
|
|
|
|
stencil.connect("deselect", this, function(){
|
|
if(!this._isBusy && this.isSelected(stencil)){
|
|
// called from within stencil. do action.
|
|
this.deselectItem(stencil);
|
|
}
|
|
});
|
|
|
|
stencil.connect("select", this, function(){
|
|
if(!this._isBusy && !this.isSelected(stencil)){
|
|
// called from within stencil. do action.
|
|
this.selectItem(stencil);
|
|
}
|
|
});
|
|
|
|
return stencil;
|
|
},
|
|
unregister: function(/*Object*/stencil){
|
|
// summary:
|
|
// Method for removing Stencils from the manager.
|
|
// This doesn't delete them, only removes them from
|
|
// the list.
|
|
//
|
|
console.log("Selection.unregister ::::::", stencil.id, "sel:", stencil.selected);
|
|
if(stencil){
|
|
stencil.selected && this.onDeselect(stencil);
|
|
delete this.stencils[stencil.id];
|
|
}
|
|
},
|
|
|
|
onArrow: function(/*Key Event*/evt){
|
|
// summary:
|
|
// Moves selection based on keyboard arrow keys
|
|
//
|
|
// FIXME: Check constraints
|
|
if(this.hasSelected()){
|
|
this.saveThrottledState();
|
|
this.group.applyTransform({dx:evt.x, dy: evt.y});
|
|
}
|
|
},
|
|
|
|
_throttleVrl:null,
|
|
_throttle: false,
|
|
throttleTime:400,
|
|
_lastmxx:-1,
|
|
_lastmxy:-1,
|
|
saveMoveState: function(){
|
|
// summary:
|
|
// Internal. Used for the prototype undo stack.
|
|
// Saves selection position.
|
|
//
|
|
var mx = this.group.getTransform();
|
|
if(mx.dx == this._lastmxx && mx.dy == this._lastmxy){ return; }
|
|
this._lastmxx = mx.dx;
|
|
this._lastmxy = mx.dy;
|
|
//console.warn("SAVE MOVE!", mx.dx, mx.dy);
|
|
this.undo.add({
|
|
before:dojo.hitch(this.group, "setTransform", mx)
|
|
});
|
|
},
|
|
|
|
saveThrottledState: function(){
|
|
// summary:
|
|
// Internal. Used for the prototype undo stack.
|
|
// Prevents an undo point on every mouse move.
|
|
// Only does a point when the mouse hesitates.
|
|
//
|
|
clearTimeout(this._throttleVrl);
|
|
clearInterval(this._throttleVrl);
|
|
this._throttleVrl = setTimeout(dojo.hitch(this, function(){
|
|
this._throttle = false;
|
|
this.saveMoveState();
|
|
}), this.throttleTime);
|
|
if(this._throttle){ return; }
|
|
this._throttle = true;
|
|
|
|
this.saveMoveState();
|
|
|
|
},
|
|
unDelete: function(/*Array*/stencils){
|
|
// summary:
|
|
// Undeletes a stencil. Used in undo stack.
|
|
//
|
|
console.log("unDelete:", stencils);
|
|
for(var s in stencils){
|
|
stencils[s].render();
|
|
this.onSelect(stencils[s]);
|
|
}
|
|
},
|
|
onDelete: function(/*Boolean*/noundo){
|
|
// summary:
|
|
// Event fired on deletion of a stencil
|
|
//
|
|
console.log("Stencil onDelete", noundo);
|
|
if(noundo!==true){
|
|
this.undo.add({
|
|
before:dojo.hitch(this, "unDelete", this.selectedStencils),
|
|
after:dojo.hitch(this, "onDelete", true)
|
|
});
|
|
}
|
|
this.withSelected(function(m){
|
|
this.anchors.remove(m);
|
|
var id = m.id;
|
|
console.log("delete:", m);
|
|
m.destroy();
|
|
delete this.stencils[id];
|
|
});
|
|
this.selectedStencils = {};
|
|
},
|
|
|
|
deleteItem: function(/*Object*/stencil){
|
|
// summary:
|
|
// Deletes a stencil.
|
|
// NOTE: supports limited undo.
|
|
//
|
|
// manipulating the selection to fire onDelete properly
|
|
if(this.hasSelected()){
|
|
// there is a selection
|
|
var sids = [];
|
|
for(var m in this.selectedStencils){
|
|
if(this.selectedStencils.id == stencil.id){
|
|
if(this.hasSelected()==1){
|
|
// the deleting stencil is the only one selected
|
|
this.onDelete();
|
|
return;
|
|
}
|
|
}else{
|
|
sids.push(this.selectedStencils.id);
|
|
}
|
|
}
|
|
// remove selection, delete, restore selection
|
|
this.deselect();
|
|
this.selectItem(stencil);
|
|
this.onDelete();
|
|
dojo.forEach(sids, function(id){
|
|
this.selectItem(id);
|
|
}, this);
|
|
}else{
|
|
// there is not a selection. select it, delete it
|
|
this.selectItem(stencil);
|
|
// now delete selection
|
|
this.onDelete();
|
|
}
|
|
},
|
|
|
|
removeAll: function(){
|
|
// summary:
|
|
// Deletes all Stencils on the canvas.
|
|
|
|
this.selectAll();
|
|
this._isBusy = true;
|
|
this.onDelete();
|
|
this.stencils = {};
|
|
this._isBusy = false;
|
|
},
|
|
|
|
setSelectionGroup: function(){
|
|
// summary:
|
|
// Internal. Creates a new selection group
|
|
// used to hold selected stencils.
|
|
//
|
|
this.withSelected(function(m){
|
|
this.onDeselect(m, true);
|
|
});
|
|
|
|
if(this.group){
|
|
surface.remove(this.group);
|
|
this.group.removeShape();
|
|
}
|
|
this.group = surface.createGroup();
|
|
this.group.setTransform({dx:0, dy: 0});
|
|
|
|
this.withSelected(function(m){
|
|
this.group.add(m.container);
|
|
m.select();
|
|
});
|
|
},
|
|
|
|
setConstraint: function(){
|
|
// summary:
|
|
// Internal. Gets all selected stencils' coordinates
|
|
// and determines how far left and up the selection
|
|
// can go without going below zero
|
|
//
|
|
var t = Infinity, l = Infinity;
|
|
this.withSelected(function(m){
|
|
var o = m.getBounds();
|
|
t = Math.min(o.y1, t);
|
|
l = Math.min(o.x1, l);
|
|
});
|
|
this.constrain = {l:-l, t:-t};
|
|
},
|
|
|
|
|
|
|
|
onDeselect: function(stencil, keepObject){
|
|
// summary:
|
|
// Event fired on deselection of a stencil
|
|
//
|
|
if(!keepObject){
|
|
delete this.selectedStencils[stencil.id];
|
|
}
|
|
//console.log('onDeselect, keep:', keepObject, "stencil:", stencil.type)
|
|
|
|
this.anchors.remove(stencil);
|
|
|
|
surface.add(stencil.container);
|
|
stencil.selected && stencil.deselect();
|
|
stencil.applyTransform(this.group.getTransform());
|
|
},
|
|
|
|
deselectItem: function(/*Object*/stencil){
|
|
// summary:
|
|
// Deselect passed stencil
|
|
//
|
|
// note: just keeping with standardized methods
|
|
this.onDeselect(stencil);
|
|
},
|
|
|
|
deselect: function(){ // all stencils
|
|
// summary:
|
|
// Deselect all stencils
|
|
//
|
|
this.withSelected(function(m){
|
|
this.onDeselect(m);
|
|
});
|
|
this._dragBegun = false;
|
|
this._wasDragged = false;
|
|
},
|
|
|
|
onSelect: function(/*Object*/stencil){
|
|
// summary:
|
|
// Event fired on selection of a stencil
|
|
//
|
|
//console.log("stencil.onSelect", stencil);
|
|
if(!stencil){
|
|
console.error("null stencil is not selected:", this.stencils)
|
|
}
|
|
if(this.selectedStencils[stencil.id]){ return; }
|
|
this.selectedStencils[stencil.id] = stencil;
|
|
this.group.add(stencil.container);
|
|
stencil.select();
|
|
if(this.hasSelected()==1){
|
|
this.anchors.add(stencil, this.group);
|
|
}
|
|
},
|
|
|
|
selectAll: function(){
|
|
// summary:
|
|
// Selects all items
|
|
this._isBusy = true;
|
|
for(var m in this.stencils){
|
|
//if(!this.stencils[m].selected){
|
|
this.selectItem(m);
|
|
//}
|
|
}
|
|
this._isBusy = false;
|
|
},
|
|
|
|
selectItem: function(/*String|Object*/ idOrItem){
|
|
// summary:
|
|
// Method used to select a stencil.
|
|
//
|
|
var id = typeof(idOrItem)=="string" ? idOrItem : idOrItem.id;
|
|
var stencil = this.stencils[id];
|
|
this.setSelectionGroup();
|
|
this.onSelect(stencil);
|
|
this.group.moveToFront();
|
|
this.setConstraint();
|
|
},
|
|
|
|
onLabelDoubleClick: function(/*EventObject*/obj){
|
|
// summary:
|
|
// Event to connect a textbox to
|
|
// for label edits
|
|
console.info("mgr.onLabelDoubleClick:", obj);
|
|
if(this.selectedStencils[obj.id]){
|
|
this.deselect();
|
|
}
|
|
},
|
|
|
|
onStencilDoubleClick: function(/*EventObject*/obj){
|
|
// summary:
|
|
// Event fired on the double-click of a stencil
|
|
//
|
|
console.info("mgr.onStencilDoubleClick:", obj);
|
|
if(this.selectedStencils[obj.id]){
|
|
if(this.selectedStencils[obj.id].edit){
|
|
console.info("Mgr Stencil Edit -> ", this.selectedStencils[obj.id]);
|
|
var m = this.selectedStencils[obj.id];
|
|
// deselect must happen first to set the transform
|
|
// then edit knows where to set the text box
|
|
m.editMode = true;
|
|
this.deselect();
|
|
m.edit();
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
onAnchorUp: function(){
|
|
// summary:
|
|
// Event fire on mouseup off of an anchor point
|
|
this.setConstraint();
|
|
},
|
|
|
|
onStencilDown: function(/*EventObject*/obj, evt){
|
|
// summary:
|
|
// Event fired on mousedown on a stencil
|
|
//
|
|
console.info(" >>> onStencilDown:", obj.id, this.keys.meta);
|
|
if(!this.stencils[obj.id]){ return; }
|
|
this.setRecentStencil(this.stencils[obj.id]);
|
|
this._isBusy = true;
|
|
|
|
|
|
if(this.selectedStencils[obj.id] && this.keys.meta){
|
|
if(dojo.isMac && this.keys.cmmd){
|
|
// block context menu
|
|
|
|
}
|
|
console.log(" shift remove");
|
|
this.onDeselect(this.selectedStencils[obj.id]);
|
|
if(this.hasSelected()==1){
|
|
this.withSelected(function(m){
|
|
this.anchors.add(m, this.group);
|
|
});
|
|
}
|
|
this.group.moveToFront();
|
|
this.setConstraint();
|
|
return;
|
|
|
|
}else if(this.selectedStencils[obj.id]){
|
|
console.log(" clicked on selected");
|
|
// clicking on same selected item(s)
|
|
// RESET OFFSETS
|
|
var mx = this.group.getTransform();
|
|
this._offx = obj.x - mx.dx;
|
|
this._offy = obj.y - mx.dy;
|
|
return;
|
|
|
|
}else if(!this.keys.meta){
|
|
|
|
console.log(" deselect all");
|
|
this.deselect();
|
|
|
|
}else{
|
|
// meta-key add
|
|
//console.log("reset sel and add stencil")
|
|
}
|
|
console.log(" add stencil to selection");
|
|
// add a stencil
|
|
this.selectItem(obj.id);
|
|
|
|
mx = this.group.getTransform();
|
|
this._offx = obj.x - mx.dx;
|
|
this._offy = obj.y - mx.dx;
|
|
|
|
this.orgx = obj.x;
|
|
this.orgy = obj.y;
|
|
|
|
this._isBusy = false;
|
|
|
|
// TODO:
|
|
// dojo.style(surfaceNode, "cursor", "pointer");
|
|
|
|
// TODO:
|
|
this.undo.add({
|
|
before:function(){
|
|
|
|
},
|
|
after: function(){
|
|
|
|
}
|
|
});
|
|
},
|
|
|
|
onLabelDown: function(/*EventObject*/obj, evt){
|
|
// summary:
|
|
// Event fired on mousedown of a stencil's label
|
|
// Because it's an annotation the id will be the
|
|
// master stencil.
|
|
//console.info("===============>>>Label click: ",obj, " evt: ",evt);
|
|
this.onStencilDown(obj,evt);
|
|
},
|
|
|
|
onStencilUp: function(/*EventObject*/obj){
|
|
// summary:
|
|
// Event fired on mouseup off of a stencil
|
|
//
|
|
},
|
|
|
|
onLabelUp: function(/*EventObject*/obj){
|
|
this.onStencilUp(obj);
|
|
},
|
|
|
|
onStencilDrag: function(/*EventObject*/obj){
|
|
// summary:
|
|
// Event fired on every mousemove of a stencil drag
|
|
//
|
|
if(!this._dragBegun){
|
|
// bug, in FF anyway - first mouse move shows x=0
|
|
// the 'else' fixes it
|
|
this.onBeginDrag(obj);
|
|
this._dragBegun = true;
|
|
}else{
|
|
this.saveThrottledState();
|
|
|
|
var x = obj.x - obj.last.x,
|
|
y = obj.y - obj.last.y,
|
|
c = this.constrain,
|
|
mz = this.defaults.anchors.marginZero;
|
|
|
|
|
|
x = obj.x - this._offx;
|
|
y = obj.y - this._offy;
|
|
|
|
if(x < c.l + mz){
|
|
x = c.l + mz;
|
|
}
|
|
if(y < c.t + mz){
|
|
y = c.t + mz;
|
|
}
|
|
|
|
this.group.setTransform({
|
|
dx: x,
|
|
dy: y
|
|
});
|
|
|
|
|
|
}
|
|
},
|
|
|
|
onLabelDrag: function(/*EventObject*/obj){
|
|
this.onStencilDrag(obj);
|
|
},
|
|
|
|
onDragEnd: function(/*EventObject*/obj){
|
|
// summary:
|
|
// Event fired at the end of a stencil drag
|
|
//
|
|
this._dragBegun = false;
|
|
},
|
|
onBeginDrag: function(/*EventObject*/obj){
|
|
// summary:
|
|
// Event fired at the beginning of a stencil drag
|
|
//
|
|
this._wasDragged = true;
|
|
},
|
|
|
|
onDown: function(/*EventObject*/obj){
|
|
// summary:
|
|
// Event fired on mousedown on the canvas
|
|
//
|
|
this.deselect();
|
|
},
|
|
|
|
|
|
onStencilOver: function(obj){
|
|
// summary:
|
|
// This changes the cursor when hovering over
|
|
// a selectable stencil.
|
|
//console.log("OVER")
|
|
dojo.style(obj.id, "cursor", "move");
|
|
},
|
|
|
|
onStencilOut: function(obj){
|
|
// summary:
|
|
// This restores the cursor.
|
|
//console.log("OUT")
|
|
dojo.style(obj.id, "cursor", "crosshair");
|
|
},
|
|
|
|
exporter: function(){
|
|
// summary:
|
|
// Collects all Stencil data and returns an
|
|
// Array of objects.
|
|
var items = [];
|
|
for(var m in this.stencils){
|
|
this.stencils[m].enabled && items.push(this.stencils[m].exporter());
|
|
}
|
|
return items; // Array
|
|
},
|
|
|
|
listStencils: function(){
|
|
return this.stencils;
|
|
},
|
|
|
|
toSelected: function(/*String*/func){
|
|
// summary:
|
|
// Convenience function calls function *within*
|
|
// all selected stencils
|
|
var args = Array.prototype.slice.call(arguments).splice(1);
|
|
for(var m in this.selectedStencils){
|
|
var item = this.selectedStencils[m];
|
|
item[func].apply(item, args);
|
|
}
|
|
},
|
|
|
|
withSelected: function(/*Function*/func){
|
|
// summary:
|
|
// Convenience function calls function on
|
|
// all selected stencils
|
|
var f = dojo.hitch(this, func);
|
|
for(var m in this.selectedStencils){
|
|
f(this.selectedStencils[m]);
|
|
}
|
|
},
|
|
|
|
withUnselected: function(/*Function*/func){
|
|
// summary:
|
|
// Convenience function calls function on
|
|
// all stencils that are not selected
|
|
var f = dojo.hitch(this, func);
|
|
for(var m in this.stencils){
|
|
!this.stencils[m].selected && f(this.stencils[m]);
|
|
}
|
|
},
|
|
|
|
withStencils: function(/*Function*/func){
|
|
// summary:
|
|
// Convenience function calls function on
|
|
// all stencils
|
|
var f = dojo.hitch(this, func);
|
|
for(var m in this.stencils){
|
|
f(this.stencils[m]);
|
|
}
|
|
},
|
|
|
|
hasSelected: function(){
|
|
// summary:
|
|
// Returns number of selected (generally used
|
|
// as truthy or falsey)
|
|
//
|
|
// FIXME: should be areSelected?
|
|
var ln = 0;
|
|
for(var m in this.selectedStencils){ ln++; }
|
|
return ln; // Number
|
|
},
|
|
|
|
isSelected: function(/*Object*/stencil){
|
|
// summary:
|
|
// Returns if passed stencil is selected or not
|
|
// based on internal collection, not on stencil
|
|
// boolean
|
|
return !!this.selectedStencils[stencil.id]; // Boolean
|
|
}
|
|
}
|
|
|
|
);
|
|
})();
|
|
|
|
});
|