/**
* <a href="http://www.w3.org/TR/NOTE-VML">VML</a> implementation of the <a href="Graphic.html">`Graphic`</a> class.
* `VMLGraphic` is not intended to be used directly. Instead, use the <a href="Graphic.html">`Graphic`</a> class.
* If the browser lacks <a href="http://www.w3.org/TR/SVG/">SVG</a> and <a href="http://www.w3.org/TR/html5/the-canvas-element.html">Canvas</a>
* capabilities, the <a href="Graphic.html">`Graphic`</a> class will point to the `VMLGraphic` class.
*
* @module graphics
* @class VMLGraphic
* @constructor
*/
VMLGraphic = function() {
VMLGraphic.superclass.constructor.apply(this, arguments);
};
VMLGraphic.NAME = "vmlGraphic";
VMLGraphic.ATTRS = {
/**
* Whether or not to render the `Graphic` automatically after to a specified parent node after init. This can be a Node
* instance or a CSS selector string.
*
* @config render
* @type Node | String
*/
render: {},
/**
* Unique id for class instance.
*
* @config id
* @type String
*/
id: {
valueFn: function()
{
return Y.guid();
},
setter: function(val)
{
var node = this._node;
if(node)
{
node.setAttribute("id", val);
}
return val;
}
},
/**
* Key value pairs in which a shape instance is associated with its id.
*
* @config shapes
* @type Object
* @readOnly
*/
shapes: {
readOnly: true,
getter: function()
{
return this._shapes;
}
},
/**
* Object containing size and coordinate data for the content of a Graphic in relation to the coordSpace node.
*
* @config contentBounds
* @type Object
*/
contentBounds: {
readOnly: true,
getter: function()
{
return this._contentBounds;
}
},
/**
* The html element that represents to coordinate system of the Graphic instance.
*
* @config node
* @type HTMLElement
*/
node: {
readOnly: true,
getter: function()
{
return this._node;
}
},
/**
* Indicates the width of the `Graphic`.
*
* @config width
* @type Number
*/
width: {
setter: function(val)
{
if(this._node)
{
this._node.style.width = val + "px";
}
return val;
}
},
/**
* Indicates the height of the `Graphic`.
*
* @config height
* @type Number
*/
height: {
setter: function(val)
{
if(this._node)
{
this._node.style.height = val + "px";
}
return val;
}
},
/**
* Determines the sizing of the Graphic.
*
* <dl>
* <dt>sizeContentToGraphic</dt><dd>The Graphic's width and height attributes are, either explicitly set through the
* <code>width</code> and <code>height</code> attributes or are determined by the dimensions of the parent element. The
* content contained in the Graphic will be sized to fit with in the Graphic instance's dimensions. When using this
* setting, the <code>preserveAspectRatio</code> attribute will determine how the contents are sized.</dd>
* <dt>sizeGraphicToContent</dt><dd>(Also accepts a value of true) The Graphic's width and height are determined by the
* size and positioning of the content.</dd>
* <dt>false</dt><dd>The Graphic's width and height attributes are, either explicitly set through the <code>width</code>
* and <code>height</code> attributes or are determined by the dimensions of the parent element. The contents of the
* Graphic instance are not affected by this setting.</dd>
* </dl>
*
*
* @config autoSize
* @type Boolean | String
* @default false
*/
autoSize: {
value: false
},
/**
* Determines how content is sized when <code>autoSize</code> is set to <code>sizeContentToGraphic</code>.
*
* <dl>
* <dt>none<dt><dd>Do not force uniform scaling. Scale the graphic content of the given element non-uniformly if necessary
* such that the element's bounding box exactly matches the viewport rectangle.</dd>
* <dt>xMinYMin</dt><dd>Force uniform scaling position along the top left of the Graphic's node.</dd>
* <dt>xMidYMin</dt><dd>Force uniform scaling horizontally centered and positioned at the top of the Graphic's node.<dd>
* <dt>xMaxYMin</dt><dd>Force uniform scaling positioned horizontally from the right and vertically from the top.</dd>
* <dt>xMinYMid</dt>Force uniform scaling positioned horizontally from the left and vertically centered.</dd>
* <dt>xMidYMid (the default)</dt><dd>Force uniform scaling with the content centered.</dd>
* <dt>xMaxYMid</dt><dd>Force uniform scaling positioned horizontally from the right and vertically centered.</dd>
* <dt>xMinYMax</dt><dd>Force uniform scaling positioned horizontally from the left and vertically from the bottom.</dd>
* <dt>xMidYMax</dt><dd>Force uniform scaling horizontally centered and position vertically from the bottom.</dd>
* <dt>xMaxYMax</dt><dd>Force uniform scaling positioned horizontally from the right and vertically from the bottom.</dd>
* </dl>
*
* @config preserveAspectRatio
* @type String
* @default xMidYMid
*/
preserveAspectRatio: {
value: "xMidYMid"
},
/**
* The contentBounds will resize to greater values but not values. (for performance)
* When resizing the contentBounds down is desirable, set the resizeDown value to true.
*
* @config resizeDown
* @type Boolean
*/
resizeDown: {
resizeDown: false
},
/**
* Indicates the x-coordinate for the instance.
*
* @config x
* @type Number
*/
x: {
getter: function()
{
return this._x;
},
setter: function(val)
{
this._x = val;
if(this._node)
{
this._node.style.left = val + "px";
}
return val;
}
},
/**
* Indicates the y-coordinate for the instance.
*
* @config y
* @type Number
*/
y: {
getter: function()
{
return this._y;
},
setter: function(val)
{
this._y = val;
if(this._node)
{
this._node.style.top = val + "px";
}
return val;
}
},
/**
* Indicates whether or not the instance will automatically redraw after a change is made to a shape.
* This property will get set to false when batching operations.
*
* @config autoDraw
* @type Boolean
* @default true
* @private
*/
autoDraw: {
value: true
},
visible: {
value: true,
setter: function(val)
{
this._toggleVisible(val);
return val;
}
}
};
Y.extend(VMLGraphic, Y.GraphicBase, {
/**
* Sets the value of an attribute.
*
* @method set
* @param {String|Object} name The name of the attribute. Alternatively, an object of key value pairs can
* be passed in to set multiple attributes at once.
* @param {Any} value The value to set the attribute to. This value is ignored if an object is received as
* the name param.
*/
set: function()
{
var host = this,
attr = arguments[0],
redrawAttrs = {
autoDraw: true,
autoSize: true,
preserveAspectRatio: true,
resizeDown: true
},
key,
forceRedraw = false;
AttributeLite.prototype.set.apply(host, arguments);
if(host._state.autoDraw === true && Y.Object.size(this._shapes) > 0)
{
if(Y_LANG.isString && redrawAttrs[attr])
{
forceRedraw = true;
}
else if(Y_LANG.isObject(attr))
{
for(key in redrawAttrs)
{
if(redrawAttrs.hasOwnProperty(key) && attr[key])
{
forceRedraw = true;
break;
}
}
}
}
if(forceRedraw)
{
host._redraw();
}
},
/**
* Storage for `x` attribute.
*
* @property _x
* @type Number
* @private
*/
_x: 0,
/**
* Storage for `y` attribute.
*
* @property _y
* @type Number
* @private
*/
_y: 0,
/**
* Gets the current position of the graphic instance in page coordinates.
*
* @method getXY
* @return Array The XY position of the shape.
*/
getXY: function()
{
var node = this.parentNode,
x = this.get("x"),
y = this.get("y"),
xy;
if(node)
{
xy = Y.DOM.getXY(node);
xy[0] += x;
xy[1] += y;
}
else
{
xy = Y.DOM._getOffset(this._node);
}
return xy;
},
/**
* Initializes the class.
*
* @method initializer
* @private
*/
initializer: function() {
var render = this.get("render"),
visibility = this.get("visible") ? "visible" : "hidden";
this._shapes = {};
this._contentBounds = {
left: 0,
top: 0,
right: 0,
bottom: 0
};
this._node = this._createGraphic();
this._node.style.left = this.get("x") + "px";
this._node.style.top = this.get("y") + "px";
this._node.style.visibility = visibility;
this._node.setAttribute("id", this.get("id"));
if(render)
{
this.render(render);
}
},
/**
* Adds the graphics node to the dom.
*
* @method render
* @param {HTMLElement} parentNode node in which to render the graphics node into.
*/
render: function(render) {
var parentNode = render || DOCUMENT.body,
node = this._node,
w,
h;
if(render instanceof Y.Node)
{
parentNode = render._node;
}
else if(Y.Lang.isString(render))
{
parentNode = Y.Selector.query(render, DOCUMENT.body, true);
}
w = this.get("width") || parseInt(Y.DOM.getComputedStyle(parentNode, "width"), 10);
h = this.get("height") || parseInt(Y.DOM.getComputedStyle(parentNode, "height"), 10);
parentNode.appendChild(node);
this.parentNode = parentNode;
this.set("width", w);
this.set("height", h);
return this;
},
/**
* Removes all nodes.
*
* @method destroy
*/
destroy: function()
{
this.removeAllShapes();
if(this._node)
{
this._removeChildren(this._node);
if(this._node.parentNode)
{
this._node.parentNode.removeChild(this._node);
}
this._node = null;
}
},
/**
* Generates a shape instance by type.
*
* @method addShape
* @param {Object} cfg attributes for the shape
* @return Shape
*/
addShape: function(cfg)
{
cfg.graphic = this;
if(!this.get("visible"))
{
cfg.visible = false;
}
var ShapeClass = this._getShapeClass(cfg.type),
shape = new ShapeClass(cfg);
this._appendShape(shape);
shape._appendStrokeAndFill();
return shape;
},
/**
* Adds a shape instance to the graphic instance.
*
* @method _appendShape
* @param {Shape} shape The shape instance to be added to the graphic.
* @private
*/
_appendShape: function(shape)
{
var node = shape.node,
parentNode = this._frag || this._node;
if(this.get("autoDraw") || this.get("autoSize") === "sizeContentToGraphic")
{
parentNode.appendChild(node);
}
else
{
this._getDocFrag().appendChild(node);
}
},
/**
* Removes a shape instance from from the graphic instance.
*
* @method removeShape
* @param {Shape|String} shape The instance or id of the shape to be removed.
*/
removeShape: function(shape)
{
if(!(shape instanceof VMLShape))
{
if(Y_LANG.isString(shape))
{
shape = this._shapes[shape];
}
}
if(shape && (shape instanceof VMLShape))
{
shape._destroy();
this._shapes[shape.get("id")] = null;
delete this._shapes[shape.get("id")];
}
if(this.get("autoDraw"))
{
this._redraw();
}
},
/**
* Removes all shape instances from the dom.
*
* @method removeAllShapes
*/
removeAllShapes: function()
{
var shapes = this._shapes,
i;
for(i in shapes)
{
if(shapes.hasOwnProperty(i))
{
shapes[i].destroy();
}
}
this._shapes = {};
},
/**
* Removes all child nodes.
*
* @method _removeChildren
* @param node
* @private
*/
_removeChildren: function(node)
{
if(node.hasChildNodes())
{
var child;
while(node.firstChild)
{
child = node.firstChild;
this._removeChildren(child);
node.removeChild(child);
}
}
},
/**
* Clears the graphics object.
*
* @method clear
*/
clear: function() {
this.removeAllShapes();
this._removeChildren(this._node);
},
/**
* Toggles visibility
*
* @method _toggleVisible
* @param {Boolean} val indicates visibilitye
* @private
*/
_toggleVisible: function(val)
{
var i,
shapes = this._shapes,
visibility = val ? "visible" : "hidden";
if(shapes)
{
for(i in shapes)
{
if(shapes.hasOwnProperty(i))
{
shapes[i].set("visible", val);
}
}
}
if(this._node)
{
this._node.style.visibility = visibility;
}
if(this._node)
{
this._node.style.visibility = visibility;
}
},
/**
* Sets the size of the graphics object.
*
* @method setSize
* @param w {Number} width to set for the instance.
* @param h {Number} height to set for the instance.
*/
setSize: function(w, h) {
w = Math.round(w);
h = Math.round(h);
this._node.style.width = w + 'px';
this._node.style.height = h + 'px';
},
/**
* Sets the positon of the graphics object.
*
* @method setPosition
* @param {Number} x x-coordinate for the object.
* @param {Number} y y-coordinate for the object.
*/
setPosition: function(x, y)
{
x = Math.round(x);
y = Math.round(y);
this._node.style.left = x + "px";
this._node.style.top = y + "px";
},
/**
* Creates a group element
*
* @method _createGraphic
* @private
*/
_createGraphic: function() {
var group = DOCUMENT.createElement(
'<group xmlns="urn:schemas-microsft.com:vml"' +
' style="behavior:url(#default#VML);padding:0px 0px 0px 0px;display:block;position:absolute;top:0px;left:0px;zoom:1;"' +
'/>'
);
return group;
},
/**
* Creates a graphic node
*
* @method _createGraphicNode
* @param {String} type node type to create
* @param {String} pe specified pointer-events value
* @return HTMLElement
* @private
*/
_createGraphicNode: function(type)
{
return DOCUMENT.createElement(
'<' +
type +
' xmlns="urn:schemas-microsft.com:vml"' +
' style="behavior:url(#default#VML);display:inline-block;zoom:1;"' +
'/>'
);
},
/**
* Returns a shape based on the id of its dom node.
*
* @method getShapeById
* @param {String} id Dom id of the shape's node attribute.
* @return Shape
*/
getShapeById: function(id)
{
return this._shapes[id];
},
/**
* Returns a shape class. Used by `addShape`.
*
* @method _getShapeClass
* @param {Shape | String} val Indicates which shape class.
* @return Function
* @private
*/
_getShapeClass: function(val)
{
var shape = this._shapeClass[val];
if(shape)
{
return shape;
}
return val;
},
/**
* Look up for shape classes. Used by `addShape` to retrieve a class for instantiation.
*
* @property _shapeClass
* @type Object
* @private
*/
_shapeClass: {
circle: Y.VMLCircle,
rect: Y.VMLRect,
path: Y.VMLPath,
ellipse: Y.VMLEllipse,
pieslice: Y.VMLPieSlice
},
/**
* Allows for creating multiple shapes in order to batch appending and redraw operations.
*
* @method batch
* @param {Function} method Method to execute.
*/
batch: function(method)
{
var autoDraw = this.get("autoDraw");
this.set("autoDraw", false);
method.apply();
this.set("autoDraw", autoDraw);
},
/**
* Returns a document fragment to for attaching shapes.
*
* @method _getDocFrag
* @return DocumentFragment
* @private
*/
_getDocFrag: function()
{
if(!this._frag)
{
this._frag = DOCUMENT.createDocumentFragment();
}
return this._frag;
},
/**
* Adds a shape to the redraw queue and calculates the contentBounds.
*
* @method addToRedrawQueue
* @param shape {VMLShape}
* @protected
*/
addToRedrawQueue: function(shape)
{
var shapeBox,
box;
this._shapes[shape.get("id")] = shape;
if(!this.get("resizeDown"))
{
shapeBox = shape.getBounds();
box = this._contentBounds;
box.left = box.left < shapeBox.left ? box.left : shapeBox.left;
box.top = box.top < shapeBox.top ? box.top : shapeBox.top;
box.right = box.right > shapeBox.right ? box.right : shapeBox.right;
box.bottom = box.bottom > shapeBox.bottom ? box.bottom : shapeBox.bottom;
box.width = box.right - box.left;
box.height = box.bottom - box.top;
this._contentBounds = box;
}
if(this.get("autoDraw"))
{
this._redraw();
}
},
/**
* Redraws all shapes.
*
* @method _redraw
* @private
*/
_redraw: function()
{
var autoSize = this.get("autoSize"),
preserveAspectRatio,
node = this.parentNode,
nodeWidth = parseFloat(Y.DOM.getComputedStyle(node, "width")),
nodeHeight = parseFloat(Y.DOM.getComputedStyle(node, "height")),
xCoordOrigin = 0,
yCoordOrigin = 0,
box = this.get("resizeDown") ? this._getUpdatedContentBounds() : this._contentBounds,
left = box.left,
right = box.right,
top = box.top,
bottom = box.bottom,
contentWidth = right - left,
contentHeight = bottom - top,
aspectRatio,
xCoordSize,
yCoordSize,
scaledWidth,
scaledHeight,
visible = this.get("visible");
this._node.style.visibility = "hidden";
if(autoSize)
{
if(autoSize === "sizeContentToGraphic")
{
preserveAspectRatio = this.get("preserveAspectRatio");
if(preserveAspectRatio === "none" || contentWidth/contentHeight === nodeWidth/nodeHeight)
{
xCoordOrigin = left;
yCoordOrigin = top;
xCoordSize = contentWidth;
yCoordSize = contentHeight;
}
else
{
if(contentWidth * nodeHeight/contentHeight > nodeWidth)
{
aspectRatio = nodeHeight/nodeWidth;
xCoordSize = contentWidth;
yCoordSize = contentWidth * aspectRatio;
scaledHeight = (nodeWidth * (contentHeight/contentWidth)) * (yCoordSize/nodeHeight);
yCoordOrigin = this._calculateCoordOrigin(preserveAspectRatio.slice(5).toLowerCase(), scaledHeight, yCoordSize);
yCoordOrigin = top + yCoordOrigin;
xCoordOrigin = left;
}
else
{
aspectRatio = nodeWidth/nodeHeight;
xCoordSize = contentHeight * aspectRatio;
yCoordSize = contentHeight;
scaledWidth = (nodeHeight * (contentWidth/contentHeight)) * (xCoordSize/nodeWidth);
xCoordOrigin = this._calculateCoordOrigin(preserveAspectRatio.slice(1, 4).toLowerCase(), scaledWidth, xCoordSize);
xCoordOrigin = xCoordOrigin + left;
yCoordOrigin = top;
}
}
this._node.style.width = nodeWidth + "px";
this._node.style.height = nodeHeight + "px";
this._node.coordOrigin = xCoordOrigin + ", " + yCoordOrigin;
}
else
{
xCoordSize = contentWidth;
yCoordSize = contentHeight;
this._node.style.width = contentWidth + "px";
this._node.style.height = contentHeight + "px";
this._state.width = contentWidth;
this._state.height = contentHeight;
}
this._node.coordSize = xCoordSize + ", " + yCoordSize;
}
else
{
this._node.style.width = nodeWidth + "px";
this._node.style.height = nodeHeight + "px";
this._node.coordSize = nodeWidth + ", " + nodeHeight;
}
if(this._frag)
{
this._node.appendChild(this._frag);
this._frag = null;
}
if(visible)
{
this._node.style.visibility = "visible";
}
},
/**
* Determines the value for either an x or y coordinate to be used for the <code>coordOrigin</code> of the Graphic.
*
* @method _calculateCoordOrigin
* @param {String} position The position for placement. Possible values are min, mid and max.
* @param {Number} size The total scaled size of the content.
* @param {Number} coordsSize The coordsSize for the Graphic.
* @return Number
* @private
*/
_calculateCoordOrigin: function(position, size, coordsSize)
{
var coord;
switch(position)
{
case "min" :
coord = 0;
break;
case "mid" :
coord = (size - coordsSize)/2;
break;
case "max" :
coord = (size - coordsSize);
break;
}
return coord;
},
/**
* Recalculates and returns the `contentBounds` for the `Graphic` instance.
*
* @method _getUpdatedContentBounds
* @return {Object}
* @private
*/
_getUpdatedContentBounds: function()
{
var bounds,
i,
shape,
queue = this._shapes,
box = {};
for(i in queue)
{
if(queue.hasOwnProperty(i))
{
shape = queue[i];
bounds = shape.getBounds();
box.left = Y_LANG.isNumber(box.left) ? Math.min(box.left, bounds.left) : bounds.left;
box.top = Y_LANG.isNumber(box.top) ? Math.min(box.top, bounds.top) : bounds.top;
box.right = Y_LANG.isNumber(box.right) ? Math.max(box.right, bounds.right) : bounds.right;
box.bottom = Y_LANG.isNumber(box.bottom) ? Math.max(box.bottom, bounds.bottom) : bounds.bottom;
}
}
box.left = Y_LANG.isNumber(box.left) ? box.left : 0;
box.top = Y_LANG.isNumber(box.top) ? box.top : 0;
box.right = Y_LANG.isNumber(box.right) ? box.right : 0;
box.bottom = Y_LANG.isNumber(box.bottom) ? box.bottom : 0;
this._contentBounds = box;
return box;
},
/**
* Inserts shape on the top of the tree.
*
* @method _toFront
* @param {VMLShape} Shape to add.
* @private
*/
_toFront: function(shape)
{
var contentNode = this._node;
if(shape instanceof Y.VMLShape)
{
shape = shape.get("node");
}
if(contentNode && shape)
{
contentNode.appendChild(shape);
}
},
/**
* Inserts shape as the first child of the content node.
*
* @method _toBack
* @param {VMLShape} Shape to add.
* @private
*/
_toBack: function(shape)
{
var contentNode = this._node,
targetNode;
if(shape instanceof Y.VMLShape)
{
shape = shape.get("node");
}
if(contentNode && shape)
{
targetNode = contentNode.firstChild;
if(targetNode)
{
contentNode.insertBefore(shape, targetNode);
}
else
{
contentNode.appendChild(shape);
}
}
}
});
Y.VMLGraphic = VMLGraphic;