/** * <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;