dojo.provide("dojo.widget.FisheyeList");
//
// TODO
// fix SVG support, and turn it on only if the browser supports it
// fix really long labels in vertical mode
//
dojo.require("dojo.widget.*");
dojo.require("dojo.widget.HtmlWidget");
dojo.require("dojo.html.style");
dojo.require("dojo.html.selection");
dojo.require("dojo.html.util");
dojo.require("dojo.event.*");
dojo.widget.defineWidget(
"dojo.widget.FisheyeList",
dojo.widget.HtmlWidget,
function(){
/*
* summary
* Menu similar to the fish eye menu on the Mac OS
* usage
* <div dojoType="FisheyeList"
* itemWidth="40" itemHeight="40"
* itemMaxWidth="150" itemMaxHeight="150"
* orientation="horizontal"
* effectUnits="2"
* itemPadding="10"
* attachEdge="center"
* labelEdge="bottom">
*
* <div dojoType="FisheyeListItem"
* id="item1"
* onclick="alert('click on' + this.caption + '(from widget id ' + this.widgetId + ')!');"
* caption="Item 1"
* iconsrc="images/fisheye_1.png">
* </div>
* ...
* </div>
*/
this.pos = {x: -1, y: -1}; // current cursor position, relative to the grid
this.EDGE = {
CENTER: 0,
LEFT: 1,
RIGHT: 2,
TOP: 3,
BOTTOM: 4
};
// for conservative trigger mode, when triggered, timerScale is gradually increased from 0 to 1
this.timerScale = 1.0;
},
{
templateString: '<div class="dojoHtmlFisheyeListBar"></div>',
templateCssPath: dojo.uri.dojoUri("src/widget/templates/FisheyeList.css"),
isContainer: true,
snarfChildDomOutput: true,
// itemWidth: Integer
// width of menu item (in pixels) in it's dormant state (when the mouse is far away)
itemWidth: 40,
// itemHeight: Integer
// height of menu item (in pixels) in it's dormant state (when the mouse is far away)
itemHeight: 40,
// itemMaxWidth: Integer
// width of menu item (in pixels) in it's fully enlarged state (when the mouse is directly over it)
itemMaxWidth: 150,
// itemMaxHeight: Integer
// height of menu item (in pixels) in it's fully enlarged state (when the mouse is directly over it)
itemMaxHeight: 150,
// orientation: String
// orientation of the menu, either "horizontal" or "vertical"
orientation: 'horizontal',
// conservativeTrigger: Boolean
// if true, don't start enlarging menu items until mouse is over an image;
// if false, start enlarging menu items as the mouse moves near them.
conservativeTrigger: false,
// effectUnits: Number
// controls how much reaction the menu makes, relative to the distance of the mouse from the menu
effectUnits: 2,
// itemPadding: Integer
// padding (in pixels) betweeen each menu item
itemPadding: 10,
// attachEdge: String
// controls the border that the menu items don't expand past;
// for example, if set to "top", then the menu items will drop downwards as they expand.
// values
// "center", "left", "right", "top", "bottom".
attachEdge: 'center',
// labelEdge: String
// controls were the labels show up in relation to the menu item icons
// values
// "center", "left", "right", "top", "bottom".
labelEdge: 'bottom',
// enableCrappySvgSupportBoolean
// for browsers that support svg, use the svg image (specified in FisheyeListIem.svgSrc)
// rather than the iconSrc image attribute
enableCrappySvgSupport: false,
fillInTemplate: function() {
dojo.html.disableSelection(this.domNode);
this.isHorizontal = (this.orientation == 'horizontal');
this.selectedNode = -1;
this.isOver = false;
this.hitX1 = -1;
this.hitY1 = -1;
this.hitX2 = -1;
this.hitY2 = -1;
//
// only some edges make sense...
//
this.anchorEdge = this._toEdge(this.attachEdge, this.EDGE.CENTER);
this.labelEdge = this._toEdge(this.labelEdge, this.EDGE.TOP);
if ( this.isHorizontal && (this.anchorEdge == this.EDGE.LEFT )){ this.anchorEdge = this.EDGE.CENTER; }
if ( this.isHorizontal && (this.anchorEdge == this.EDGE.RIGHT )){ this.anchorEdge = this.EDGE.CENTER; }
if (!this.isHorizontal && (this.anchorEdge == this.EDGE.TOP )){ this.anchorEdge = this.EDGE.CENTER; }
if (!this.isHorizontal && (this.anchorEdge == this.EDGE.BOTTOM)){ this.anchorEdge = this.EDGE.CENTER; }
if (this.labelEdge == this.EDGE.CENTER){ this.labelEdge = this.EDGE.TOP; }
if ( this.isHorizontal && (this.labelEdge == this.EDGE.LEFT )){ this.labelEdge = this.EDGE.TOP; }
if ( this.isHorizontal && (this.labelEdge == this.EDGE.RIGHT )){ this.labelEdge = this.EDGE.TOP; }
if (!this.isHorizontal && (this.labelEdge == this.EDGE.TOP )){ this.labelEdge = this.EDGE.LEFT; }
if (!this.isHorizontal && (this.labelEdge == this.EDGE.BOTTOM)){ this.labelEdge = this.EDGE.LEFT; }
//
// figure out the proximity size
//
this.proximityLeft = this.itemWidth * (this.effectUnits - 0.5);
this.proximityRight = this.itemWidth * (this.effectUnits - 0.5);
this.proximityTop = this.itemHeight * (this.effectUnits - 0.5);
this.proximityBottom = this.itemHeight * (this.effectUnits - 0.5);
if (this.anchorEdge == this.EDGE.LEFT){
this.proximityLeft = 0;
}
if (this.anchorEdge == this.EDGE.RIGHT){
this.proximityRight = 0;
}
if (this.anchorEdge == this.EDGE.TOP){
this.proximityTop = 0;
}
if (this.anchorEdge == this.EDGE.BOTTOM){
this.proximityBottom = 0;
}
if (this.anchorEdge == this.EDGE.CENTER){
this.proximityLeft /= 2;
this.proximityRight /= 2;
this.proximityTop /= 2;
this.proximityBottom /= 2;
}
},
postCreate: function() {
this._initializePositioning();
//
// in liberal trigger mode, activate menu whenever mouse is close
//
if( !this.conservativeTrigger ){
dojo.event.connect(document.documentElement, "onmousemove", this, "_onMouseMove");
}
// Deactivate the menu if mouse is moved off screen (doesn't work for FF?)
dojo.event.connect(document.documentElement, "onmouseout", this, "_onBodyOut");
dojo.event.connect(this, "addChild", this, "_initializePositioning");
},
_initializePositioning: function(){
this.itemCount = this.children.length;
this.barWidth = (this.isHorizontal ? this.itemCount : 1) * this.itemWidth;
this.barHeight = (this.isHorizontal ? 1 : this.itemCount) * this.itemHeight;
this.totalWidth = this.proximityLeft + this.proximityRight + this.barWidth;
this.totalHeight = this.proximityTop + this.proximityBottom + this.barHeight;
//
// calculate effect ranges for each item
//
for (var i=0; i<this.children.length; i++){
this.children[i].posX = this.itemWidth * (this.isHorizontal ? i : 0);
this.children[i].posY = this.itemHeight * (this.isHorizontal ? 0 : i);
this.children[i].cenX = this.children[i].posX + (this.itemWidth / 2);
this.children[i].cenY = this.children[i].posY + (this.itemHeight / 2);
var isz = this.isHorizontal ? this.itemWidth : this.itemHeight;
var r = this.effectUnits * isz;
var c = this.isHorizontal ? this.children[i].cenX : this.children[i].cenY;
var lhs = this.isHorizontal ? this.proximityLeft : this.proximityTop;
var rhs = this.isHorizontal ? this.proximityRight : this.proximityBottom;
var siz = this.isHorizontal ? this.barWidth : this.barHeight;
var range_lhs = r;
var range_rhs = r;
if (range_lhs > c+lhs){ range_lhs = c+lhs; }
if (range_rhs > (siz-c+rhs)){ range_rhs = siz-c+rhs; }
this.children[i].effectRangeLeft = range_lhs / isz;
this.children[i].effectRangeRght = range_rhs / isz;
//dojo.debug('effect range for '+i+' is '+range_lhs+'/index.html'+range_rhs);
}
//
// create the bar
//
this.domNode.style.width = this.barWidth + 'px';
this.domNode.style.height = this.barHeight + 'px';
//
// position the items
//
for (var i=0; i<this.children.length; i++){
var itm = this.children[i];
var elm = itm.domNode;
elm.style.left = itm.posX + 'px';
elm.style.top = itm.posY + 'px';
elm.style.width = this.itemWidth + 'px';
elm.style.height = this.itemHeight + 'px';
if ( itm.svgNode ) {
itm.svgNode.style.position = 'absolute';
itm.svgNode.style.left = this.itemPadding+'%';
itm.svgNode.style.top = this.itemPadding+'%';
itm.svgNode.style.width = (100 - 2 * this.itemPadding) + '%';
itm.svgNode.style.height = (100 - 2 * this.itemPadding) + '%';
itm.svgNode.style.zIndex = 1;
itm.svgNode.setSize(this.itemWidth, this.itemHeight);
} else {
itm.imgNode.style.left = this.itemPadding+'%';
itm.imgNode.style.top = this.itemPadding+'%';
itm.imgNode.style.width = (100 - 2 * this.itemPadding) + '%';
itm.imgNode.style.height = (100 - 2 * this.itemPadding) + '%';
}
}
//
// calc the grid
//
this._calcHitGrid();
},
_onBodyOut: function(/*Event*/ e){
// clicking over an object inside of body causes this event to fire; ignore that case
if( dojo.html.overElement(dojo.body(), e) ){
return;
}
this._setDormant(e);
},
_setDormant: function(/*Event*/ e){
// summary: called when mouse moves out of menu's range
if( !this.isOver ){ return; } // already dormant?
this.isOver = false;
if ( this.conservativeTrigger ) {
// user can't re-trigger the menu expansion
// until he mouses over a icon again
dojo.event.disconnect(document.documentElement, "onmousemove", this, "_onMouseMove");
}
this._onGridMouseMove(-1, -1);
},
_setActive: function(/*Event*/ e){
// summary: called when mouse is moved into menu's range
if( this.isOver ){ return; } // already activated?
this.isOver = true;
if ( this.conservativeTrigger ) {
// switch event handlers so that we handle mouse events from anywhere near
// the menu
dojo.event.connect(document.documentElement, "onmousemove", this, "_onMouseMove");
this.timerScale=0.0;
// call mouse handler to do some initial necessary calculations/positioning
this._onMouseMove(e);
// slowly expand the icon size so it isn't jumpy
this._expandSlowly();
}
},
_onMouseMove: function(/*Event*/ e) {
// summary: called when mouse is moved
if ((e.pageX >= this.hitX1) && (e.pageX <= this.hitX2) &&
(e.pageY >= this.hitY1) && (e.pageY <= this.hitY2)){
if( !this.isOver ){
this._setActive(e);
}
this._onGridMouseMove(e.pageX-this.hitX1, e.pageY-this.hitY1);
}else{
if (this.isOver){
this._setDormant(e);
}
}
},
onResized: function() {
this._calcHitGrid();
},
_onGridMouseMove: function(x, y){
// summary: called when mouse is moved in the vicinity of the menu
this.pos = {x:x, y:y};
this._paint();
},
_paint: function(){
var x=this.pos.x;
var y=this.pos.y;
if( this.itemCount <= 0 ){ return; }
//
// figure out our main index
//
var pos = this.isHorizontal ? x : y;
var prx = this.isHorizontal ? this.proximityLeft : this.proximityTop;
var siz = this.isHorizontal ? this.itemWidth : this.itemHeight;
var sim = this.isHorizontal ?
(1.0-this.timerScale)*this.itemWidth + this.timerScale*this.itemMaxWidth :
(1.0-this.timerScale)*this.itemHeight + this.timerScale*this.itemMaxHeight ;
var cen = ((pos - prx) / siz) - 0.5;
var max_off_cen = (sim / siz) - 0.5;
if (max_off_cen > this.effectUnits){ max_off_cen = this.effectUnits; }
//
// figure out our off-axis weighting
//
var off_weight = 0;
if (this.anchorEdge == this.EDGE.BOTTOM){
var cen2 = (y - this.proximityTop) / this.itemHeight;
off_weight = (cen2 > 0.5) ? 1 : y / (this.proximityTop + (this.itemHeight / 2));
}
if (this.anchorEdge == this.EDGE.TOP){
var cen2 = (y - this.proximityTop) / this.itemHeight;
off_weight = (cen2 < 0.5) ? 1 : (this.totalHeight - y) / (this.proximityBottom + (this.itemHeight / 2));
}
if (this.anchorEdge == this.EDGE.RIGHT){
var cen2 = (x - this.proximityLeft) / this.itemWidth;
off_weight = (cen2 > 0.5) ? 1 : x / (this.proximityLeft + (this.itemWidth / 2));
}
if (this.anchorEdge == this.EDGE.LEFT){
var cen2 = (x - this.proximityLeft) / this.itemWidth;
off_weight = (cen2 < 0.5) ? 1 : (this.totalWidth - x) / (this.proximityRight + (this.itemWidth / 2));
}
if (this.anchorEdge == this.EDGE.CENTER){
if (this.isHorizontal){
off_weight = y / (this.totalHeight);
}else{
off_weight = x / (this.totalWidth);
}
if (off_weight > 0.5){
off_weight = 1 - off_weight;
}
off_weight *= 2;
}
//
// set the sizes
//
for(var i=0; i<this.itemCount; i++){
var weight = this._weighAt(cen, i);
if (weight < 0){weight = 0;}
this._setItemSize(i, weight * off_weight);
}
//
// set the positions
//
var main_p = Math.round(cen);
var offset = 0;
if (cen < 0){
main_p = 0;
}else if (cen > this.itemCount - 1){
main_p = this.itemCount -1;
}else{
offset = (cen - main_p) * ((this.isHorizontal ? this.itemWidth : this.itemHeight) - this.children[main_p].sizeMain);
}
this._positionElementsFrom(main_p, offset);
},
_weighAt: function(/*Integer*/ cen, /*Integer*/ i){
var dist = Math.abs(cen - i);
var limit = ((cen - i) > 0) ? this.children[i].effectRangeRght : this.children[i].effectRangeLeft;
return (dist > limit) ? 0 : (1 - dist / limit); // Integer
},
_setItemSize: function(p, scale){
scale *= this.timerScale;
var w = Math.round(this.itemWidth + ((this.itemMaxWidth - this.itemWidth ) * scale));
var h = Math.round(this.itemHeight + ((this.itemMaxHeight - this.itemHeight) * scale));
if (this.isHorizontal){
this.children[p].sizeW = w;
this.children[p].sizeH = h;
this.children[p].sizeMain = w;
this.children[p].sizeOff = h;
var y = 0;
if (this.anchorEdge == this.EDGE.TOP){
y = (this.children[p].cenY - (this.itemHeight / 2));
}else if (this.anchorEdge == this.EDGE.BOTTOM){
y = (this.children[p].cenY - (h - (this.itemHeight / 2)));
}else{
y = (this.children[p].cenY - (h / 2));
}
this.children[p].usualX = Math.round(this.children[p].cenX - (w / 2));
this.children[p].domNode.style.top = y + 'px';
this.children[p].domNode.style.left = this.children[p].usualX + 'px';
}else{
this.children[p].sizeW = w;
this.children[p].sizeH = h;
this.children[p].sizeOff = w;
this.children[p].sizeMain = h;
var x = 0;
if (this.anchorEdge == this.EDGE.LEFT){
x = this.children[p].cenX - (this.itemWidth / 2);
}else if (this.anchorEdge == this.EDGE.RIGHT){
x = this.children[p].cenX - (w - (this.itemWidth / 2));
}else{
x = this.children[p].cenX - (w / 2);
}
this.children[p].domNode.style.left = x + 'px';
this.children[p].usualY = Math.round(this.children[p].cenY - (h / 2));
this.children[p].domNode.style.top = this.children[p].usualY + 'px';
}
this.children[p].domNode.style.width = w + 'px';
this.children[p].domNode.style.height = h + 'px';
if (this.children[p].svgNode){
this.children[p].svgNode.setSize(w, h);
}
},
_positionElementsFrom: function(p, offset){
var pos = 0;
if (this.isHorizontal){
pos = Math.round(this.children[p].usualX + offset);
this.children[p].domNode.style.left = pos + 'px';
}else{
pos = Math.round(this.children[p].usualY + offset);
this.children[p].domNode.style.top = pos + 'px';
}
this._positionLabel(this.children[p]);
//
// position before
//
var bpos = pos;
for(var i=p-1; i>=0; i--){
bpos -= this.children[i].sizeMain;
if (this.isHorizontal){
this.children[i].domNode.style.left = bpos + 'px';
}else{
this.children[i].domNode.style.top = bpos + 'px';
}
this._positionLabel(this.children[i]);
}
//
// position after
//
var apos = pos;
for(var i=p+1; i<this.itemCount; i++){
apos += this.children[i-1].sizeMain;
if (this.isHorizontal){
this.children[i].domNode.style.left = apos + 'px';
}else{
this.children[i].domNode.style.top = apos + 'px';
}
this._positionLabel(this.children[i]);
}
},
_positionLabel: function(itm){
var x = 0;
var y = 0;
var mb = dojo.html.getMarginBox(itm.lblNode);
if (this.labelEdge == this.EDGE.TOP){
x = Math.round((itm.sizeW / 2) - (mb.width / 2));
y = -mb.height;
}
if (this.labelEdge == this.EDGE.BOTTOM){
x = Math.round((itm.sizeW / 2) - (mb.width / 2));
y = itm.sizeH;
}
if (this.labelEdge == this.EDGE.LEFT){
x = -mb.width;
y = Math.round((itm.sizeH / 2) - (mb.height / 2));
}
if (this.labelEdge == this.EDGE.RIGHT){
x = itm.sizeW;
y = Math.round((itm.sizeH / 2) - (mb.height / 2));
}
itm.lblNode.style.left = x + 'px';
itm.lblNode.style.top = y + 'px';
},
_calcHitGrid: function(){
var pos = dojo.html.getAbsolutePosition(this.domNode, true);
this.hitX1 = pos.x - this.proximityLeft;
this.hitY1 = pos.y - this.proximityTop;
this.hitX2 = this.hitX1 + this.totalWidth;
this.hitY2 = this.hitY1 + this.totalHeight;
//dojo.debug(this.hitX1+','+this.hitY1+' // '+this.hitX2+','+this.hitY2);
},
_toEdge: function(inp, def){
return this.EDGE[inp.toUpperCase()] || def;
},
_expandSlowly: function(){
// summary: slowly expand the image to user specified max size
if( !this.isOver ){ return; }
this.timerScale += 0.2;
this._paint();
if ( this.timerScale<1.0 ) {
dojo.lang.setTimeout(this, "_expandSlowly", 10);
}
},
destroy: function(){
// need to disconnect when we destroy
dojo.event.disconnect(document.documentElement, "onmouseout", this, "_onBodyOut");
dojo.event.disconnect(document.documentElement, "onmousemove", this, "_onMouseMove");
dojo.widget.FisheyeList.superclass.destroy.call(this);
}
});
dojo.widget.defineWidget(
"dojo.widget.FisheyeListItem",
dojo.widget.HtmlWidget,
{
/*
* summary
* Menu item inside of a FisheyeList.
* See FisheyeList documentation for details on usage.
*/
// iconSrc: String
// pathname to image file (jpg, gif, png, etc.) of icon for this menu item
iconSrc: "",
// svgSrc: String
// pathname to svg file of icon for this menu item
svgSrc: "",
// caption: String
// label to print next to the icon, when it is moused-over
caption: "",
// id: String
// will be set to the id of the orginal div element
id: "",
_blankImgPath: dojo.uri.dojoUri("src/widget/templates/images/blank.gif"),
templateString:
'<div class="dojoHtmlFisheyeListItem">' +
' <img class="dojoHtmlFisheyeListItemImage" dojoAttachPoint="imgNode" dojoAttachEvent="onMouseOver;onMouseOut;onClick">' +
' <div class="dojoHtmlFisheyeListItemLabel" dojoAttachPoint="lblNode"></div>' +
'</div>',
fillInTemplate: function() {
//
// set image
// TODO: turn on/off SVG support based on browser version.
// this.parent.enableCrappySvgSupport is not available to this function
//
if (this.svgSrc != ""){
this.svgNode = this._createSvgNode(this.svgSrc);
this.domNode.appendChild(this.svgNode);
this.imgNode.style.display = 'none';
} else if((this.iconSrc.toLowerCase().substring(this.iconSrc.length-4)==".png")&&(dojo.render.html.ie)&&(!dojo.render.html.ie70)){
/* we set the id of the new fisheyeListItem to the id of the div defined in the HTML */
if (dojo.dom.hasParent(this.imgNode) && this.id != ""){
var parent = this.imgNode.parentNode;
parent.setAttribute("id", this.id);
}
this.imgNode.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+this.iconSrc+"', sizingMethod='scale')";
this.imgNode.src = this._blankImgPath.toString();
} else {
if (dojo.dom.hasParent(this.imgNode) && this.id != ""){
var parent = this.imgNode.parentNode;
parent.setAttribute("id", this.id);
}
this.imgNode.src = this.iconSrc;
}
//
// Label
//
if ( this.lblNode ) {
this.lblNode.appendChild(document.createTextNode(this.caption));
}
dojo.html.disableSelection(this.domNode);
},
_createSvgNode: function(src){
var elm = document.createElement('embed');
elm.src = src;
elm.type = 'image/svg+xml';
//elm.style.border = '1px solid black';
elm.style.width = '1px';
elm.style.height = '1px';
elm.loaded = 0;
elm.setSizeOnLoad = false;
elm.onload = function(){
this.svgRoot = this.getSVGDocument().rootElement;
this.svgDoc = this.getSVGDocument().documentElement;
this.zeroWidth = this.svgRoot.width.baseVal.value;
this.zeroHeight = this.svgRoot.height.baseVal.value;
this.loaded = true;
if (this.setSizeOnLoad){
this.setSize(this.setWidth, this.setHeight);
}
}
elm.setSize = function(w, h){
if (!this.loaded){
this.setWidth = w;
this.setHeight = h;
this.setSizeOnLoad = true;
return;
}
this.style.width = w+'px';
this.style.height = h+'px';
this.svgRoot.width.baseVal.value = w;
this.svgRoot.height.baseVal.value = h;
var scale_x = w / this.zeroWidth;
var scale_y = h / this.zeroHeight;
for(var i=0; i<this.svgDoc.childNodes.length; i++){
if (this.svgDoc.childNodes[i].setAttribute){
this.svgDoc.childNodes[i].setAttribute( "transform", "scale("+scale_x+","+scale_y+")" );
}
}
}
return elm;
},
onMouseOver: function(/*Event*/ e) {
// summary: callback when user moves mouse over this menu item
// in conservative mode, don't activate the menu until user mouses over an icon
if( !this.parent.isOver ){
this.parent._setActive(e);
}
if ( this.caption != "" ) {
dojo.html.addClass(this.lblNode, "selected");
this.parent._positionLabel(this);
}
},
onMouseOut: function(/*Event*/ e) {
// summary: callback when user moves mouse off of this menu item
dojo.html.removeClass(this.lblNode, "selected");
},
onClick: function(/*Event*/ e) {
// summary: user overridable callback when user clicks this menu item
}
});