(function (undefined) { // That’s not IE! Getouttahere. if (!window.attachEvent) { return; } /** * RoundRect. Makes funny looking square boxes into funny looking round * boxes in your funny looking Microsoft browser. * * (Not DD_roundies.) * * Original DD_roundies © 2008 Drew Diller * RoundRect © 2010 Colin Snover * * Released under MIT license. */ /** * The nodeName for the element that is wrapped around the VML, as well as * the name of the globally exposed RoundRect method. * Defaults to RoundRect. You can change it to something else if you don’t * like it (for example, if you are a dork and think CS_undies is a better * name). * @type {string} */ var ns = 'RoundRect', /** * The namespace prefix that VML is bound to. * @type {string} */ xmlns = 'rr', /** * You can change these if you want to use a specific prefix in your * CSS instead of using the unprefixed version of border-radius, though * mostly they are here because these strings are stupidly long. * @type {string} */ br = 'border-radius', /** * @type {string} */ btl = 'border-top-left-radius', /** * @type {string} */ btr = 'border-top-right-radius', /** * @type {string} */ bbr = 'border-bottom-right-radius', /** * @type {string} */ bbl = 'border-bottom-left-radius', /** * @type {string} */ expando = ns + new Date().getTime(), /** * @type {number} */ uuid = 0, /** * Internal collection of all RoundRect objects. Used to ensure all * RoundRects are properly cleaned up so that IE does not leak memory * all over the ground. * @type {Object.} */ collection = {}, /** * @type {boolean} */ ie8 = document.documentMode === 8, /** * @type {boolean} */ isDOMReady = false, /** * @type {Array.} */ readyList = [], /** * A map of images that are used as background-images. This is needed * in order to get the correct size of the original image in order to * clip it for repeat-x and repeat-y. * @type {Object.} */ imageMap = {}, /** * @type {RegExp} */ isPixelString = /^-?\d+(?:px)?$/i, /** * @type {RegExp} */ isNumericString = /^-?\d/, /** * @type {string} */ hoverClass = ns + '-hover'; /** * Proxy function. * @param {Object} obj Object to bind as ‘this’ * @param {Function} fn Function to call * @return {Function} */ function proxy(obj, fn) { return function () { return fn.apply(obj || this, arguments); }; } /** * Registers a function to be executed on DOM ready. * @param {Function} fn */ function ready(fn) { if (isDOMReady) { fn.call(document); return; } readyList.push(fn); } /** * Gets a pixel value for any CSS value string. * @param {Element} e * @param {string} prop */ function getPixelValue(e, prop) { var left = e.style.left, rsLeft = e.runtimeStyle.left, value = e.currentStyle[prop]; if (isPixelString.test(value)) { return parseInt(e.currentStyle[prop], 10); } if (isNumericString.test(value)) { // Put in the new values to get a computed value out e.runtimeStyle.left = e.currentStyle.left; e.style.left = value; value = e.style.pixelLeft; // Revert the changed values e.style.left = left; e.runtimeStyle.left = rsLeft; return value; } return 0; } /** * Determines whether or not an element is a button input. * @param {Element} element * @return {boolean} */ function isButton(element) { var tagName, type; if (!element || !element.nodeName || !element.type) { return false; } tagName = element.nodeName.toUpperCase(); type = element.type.toLowerCase(); return (tagName === 'BUTTON' || (tagName === 'INPUT' && (type === 'image' || type === 'button' || type === 'submit' || type === 'reset'))); } /** * Determines whether or not an element is a text input. * @param {Element} element * @return {boolean} */ function isTextField(element) { var tagName, type; if (!element || !element.nodeName) { return false; } tagName = element.nodeName.toUpperCase(); type = tagName === 'INPUT' ? element.type.toLowerCase() : null; return ((tagName === 'INPUT' && (element.type === 'text' || element.type === 'password')) || tagName === 'TEXTAREA'); } /** * Enables VML on the current page. */ (function enableVml() { var css, rule; // IE will throw confused errors if document.namespaces // is not ready for our sweet sweet lovin’ try { if (ie8) { document.namespaces.add(xmlns, 'urn:schemas-microsoft-com:vml', '#default#VML'); } else { document.namespaces.add(xmlns, 'urn:schemas-microsoft-com:vml'); } } catch (e) { setTimeout(enableVml, 10); return; } // Technically not part of enabling VML, but it is important to // prevent all sorts of havoc try { document.execCommand('BackgroundImageCache', false, true); } catch (e) {} // luckily, IE does not care that styles are going into the body css = document.createElement('style'); document.body.appendChild(css); rule = 'behavior:url(#default#VML);display:inline-block'; css.styleSheet.addRule(xmlns + '\\:shape', rule); css.styleSheet.addRule(xmlns + '\\:group', rule); css.styleSheet.addRule(xmlns + '\\:fill', rule); }()); /** * Executes onReady once the DOM has loaded. */ function onReady() { var fn; if (isDOMReady) { document.detachEvent('onreadystatechange', onReady); return; } if (document.readyState === 'complete') { document.detachEvent('onreadystatechange', onReady); if (!document.body) { setTimeout(onReady, 13); return; } isDOMReady = true; while ((fn = readyList.shift())) { fn.call(document); } } } document.attachEvent('onreadystatechange', onReady); /** * Poll for early document ready state. */ (function scrollCheck() { if (isDOMReady) { return; } try { document.documentElement.doScroll('left'); } catch (e) { setTimeout(scrollCheck); return; } onReady(); }()); /** * Only you can prevent horrible memory leaks in IE—because Microsoft * doesn’t. (j/k guys i am sure ie9 will be leak-free.) */ window.attachEvent('onunload', function () { for (var i in collection) { if (collection.hasOwnProperty(i)) { try { collection[i].destroy(); } catch (e) {} } } collection = null; }); /** * Creates a new RoundRect object. * @class RoundRect * @constructor * @param {Element} element * @param {boolean} dynamic */ function RoundRect(element, dynamic) { if (element[expando] && collection[element[expando]]) { throw new Error('Can’t round already rounded rectangles (use RoundRect.create)'); } collection[element[expando] = (++uuid)] = this; this.element = element; this.onPropertyChangeProxy = proxy(this, function () { var self = this, property = window.event.propertyName; setTimeout(function () { self.onPropertyChange.call(self, property); }); }); this.onStateChangeProxy = proxy(this, function () { var self = this, eventType = window.event.type; setTimeout(function () { self.onStateChange.call(self, eventType); }); }); this.onVmlStateChangeProxy = proxy(this, function () { // IE seems to sometimes ditch properties from the event object // if we do not create references to them here before passing to // the statechange function var eventType = window.event.type; this.onVmlStateChange(eventType); }); this.events = { element: {}, container: {} }; ready(proxy(this, function () { this.render(); if (dynamic) { this.start(); } })); } /** * @type {string} */ RoundRect.expando = expando; /** * A hash map of nodeNames that will fail if we try to round them. What a * bummer. * @type {Object.} */ RoundRect.disallowed = { BODY: true, TABLE: true, TR: true, TD: true, SELECT: !ie8, OPTION: true }; /** * Creates a new RoundRect object, or returns the one that already exists * in the collection for the given element. This is the preferred method of * RoundRect instantiation. * @param {Element} element * @param {boolean} dynamic * @return {RoundRect} */ RoundRect.create = function (element, dynamic) { var id = element[expando], obj; if (id && (obj = collection[id])) { if (dynamic !== undefined && obj.dynamic !== dynamic) { if (obj.dynamic) { obj.start(); } else { obj.stop(); } } return obj; } return new RoundRect(element, dynamic); }; /** * Manually destroy references of all elements not currently in the DOM in * order to allow IE to garbage collect and free memory. If you need to * call this, you are failing to destroy RoundRect objects, which is bad! * Call the destroy method on objects you remove instead, whenever * possible. */ RoundRect.gc = function () { for (var i in collection) { if (collection.hasOwnProperty(i)) { if (!collection[i].element.parentNode) { collection[i].destroy(); } } } }; /** * Because sometimes faking hover events is necessary, we need to pull * rules from stylesheets that contain :hover pseudo-elements and add some * new rules to generate pseudo-classes. */ RoundRect.processStyleSheets = function () { var i, j, k, l, sheet; /** * :hover -> ns + -hover * @param {Object} sheet CSSStyleSheet, IE style. */ function processStyleSheet(sheet) { var i, rule; for (i = sheet.rules.length - 1; i >= 0; --i) { rule = sheet.rules[i]; // Remove rules that were added previously, in case // processStyleSheets is being executed to refresh them if (rule.selectorText.indexOf('.' + ns + '-hover') !== -1) { sheet.removeRule(i); continue; } if (rule.selectorText.indexOf(':hover') !== -1) { sheet.addRule(rule.selectorText.replace(/:hover/g, '.' + ns + '-hover'), rule.style.cssText, i + 1); } } } for (i = 0, j = document.styleSheets.length; i < j; ++i) { try { sheet = document.styleSheets[i]; if (sheet.imports) { for (k = 0, l = sheet.imports.length; k < l; ++k) { try { processStyleSheet(sheet.imports[k]); } // ignore ‘Permission Denied’ errors // with as little collateral damage as possible catch (e) {} } } processStyleSheet(sheet); } // ignore ‘Permission Denied’ errors catch (e) {} } }; /** * Search the DOM for any elements that should have rounded rectangles * (based on CSS rules) and apply them. * @param {boolean=} dynamic Whether to watch for changed styles. Defaults * to TRUE. * @param {boolean=} watchAll If TRUE, even elements that don’t have * border-radius right now will be watched for changes. dynamic must also * be TRUE, or this will do nothing. */ RoundRect.run = function (dynamic, watchAll) { if (dynamic === undefined) { dynamic = true; } if (dynamic) { RoundRect.processStyleSheets(); } ready(function () { var elements = document.getElementsByTagName('*'), i, e, sucks, cs, tagName, nsUpper = ns.toUpperCase(); // NodeLists are live; when elements are added, the length changes, // so don’t you dare try to optimize this loop unless you want to // break stuff for (i = 0; i < elements.length; ++i) { e = elements[i]; cs = e.currentStyle; tagName = e.nodeName.toUpperCase(); // Skip non-Element, RoundRect, VML, and disallowed elements if (e.nodeType !== 1 || tagName === nsUpper || RoundRect.disallowed[tagName] || e.scopeName === xmlns) { continue; } if ((cs[br] || cs[btl] || cs[btr] || cs[bbr] || cs[bbl]) !== undefined || (dynamic && watchAll)) { RoundRect.create(e, dynamic); } } }); }; RoundRect.prototype = { /** * The element referenced by this object. * @type {Element} * @private */ element: undefined, /** * A VML container for the VML content. What could be better?! * @type {?} * @private */ container: undefined, /** * Cached values for the element’s width, height, top, and left offset. * @type {Object.} * @private */ dimensions: undefined, /** * Caches values for the four border-radius values, starting from the * top-left. * @type {?Array.} */ radii: null, /** * Cached values for the top, right, bottom, and left border widths. * @type {Object.} * @private */ borderWidths: undefined, /** * VML elements used to draw the border and background. * @type {Object.} */ vml: undefined, /** * Whether or not the element responds to dynamic property updates. * @type {boolean} */ dynamic: false, /** * The URL of the background image of the element. * @type {?string} */ backgroundImage: null, /** * @type {Object.} * @private */ originalStyles: undefined, /** * References to event handlers for this element and its container. * Required in order to prevent memory leaks. * Defined in the constructor. * @type {Object.>>} * @private */ events: undefined, /** * Adds events to DOM elements in a manner such that they can be safely * removed for garbage collection, since IE is incapable of doing this * on its own. * @param {string} elementType Either ‘element’ or ‘container’, * depending upon which we are adding an event to. * @param {string} eventType The type of event, excluding ‘on’. * @param {Function} fn The event handler. */ addEvent: function (elementType, eventType, fn) { if (!this.events[elementType][eventType]) { this.events[elementType][eventType] = [ fn ]; } else { this.events[elementType][eventType].push(fn); } this[elementType].attachEvent('on' + eventType, fn); }, /** * Removes events from DOM elements in a manner such that they can be * safely garbage collected, since IE is incapable of doing this on its * own. * @param {string=} element Either ‘element’ or ‘container’. If * undefined, all events will be removed. * @param {string=} event The type of event, excluding ‘on’. If * undefined, all events for the specified elementType will be removed. * @param {Function=} fn The function to unbind. If undefined, all * events for the specified eventType will be removed. */ removeEvent: function (element, event, fn) { var elementTypes, eventTypes, elementEvents, elementType, eventType, i, j; if (element !== undefined) { elementTypes = {}; elementTypes[element] = 1; } else { elementTypes = this.events; } for (elementType in elementTypes) { if (elementTypes.hasOwnProperty(elementType)) { if (event !== undefined) { eventTypes = {}; eventTypes[event] = 1; } else { eventTypes = this.events[elementType]; } for (eventType in eventTypes) { if (eventTypes.hasOwnProperty(eventType)) { elementEvents = this.events[elementType][eventType]; for (i = 0, j = elementEvents.length; i < j; ++i) { if (elementEvents[i] === fn) { this[elementType].detachEvent('on' + eventType, elementEvents.splice(i, 1)[0]); return; } else if (fn === undefined) { this[elementType].detachEvent('on' + eventType, elementEvents[i]); } } if (fn === undefined) { delete this.events[elementType][eventType]; } } } } } }, /** * Breaks references to the DOM to allow Microsoft’s crap GC to GC. * (I am not entirely sure how many of this is actually necessary, * since 1. who cares about IE6, 2. sIEve is actually incredibly * unreliable at determining leaks, and 3. leaks are fixed in IE8 * and will get collected when navigating to another page in IE7. * Expert advice is appreciated.) * @param {boolean=} restoreStyles Whether or not to restore inline * styles from when the element was first run through RoundRect. */ destroy: function (restoreStyles) { var id = this.element[expando], i; this.removeEvent(); this.element.removeAttribute(expando); if (this.vml) { for (i in this.vml) { if (this.vml.hasOwnProperty(i)) { this.vml[i].filler = null; this.vml[i] = null; } } } if (this.container) { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } } if (restoreStyles && this.originalStyles) { for (i in this.originalStyles) { if (this.originalStyles.hasOwnProperty(i)) { this.element.style[i] = this.originalStyles[i]; } } this.element.parentNode.style.width = ''; } this.container = null; this.element = null; delete collection[id]; }, /** * Start watching for dynamic property changes and events. */ start: function () { if (!this.dynamic) { this.modifyEvents(true); this.dynamic = true; } }, /** * Stop watching for dynamic property changes and events. */ stop: function () { if (this.dynamic) { this.modifyEvents(false); this.dynamic = false; } }, /** * Modifies the event listeners on the element. * @param {boolean} append * @private */ modifyEvents: function (append) { var e = 'element', c = 'container', scp = this.onStateChangeProxy, vcp = this.onVmlStateChangeProxy, method = append ? 'addEvent' : 'removeEvent'; this[method](e, 'propertychange', this.onPropertyChangeProxy); // events that may have corresponding changes within stylesheets this[method](e, 'mouseenter', scp); this[method](e, 'mouseleave', scp); this[method](e, 'focus', scp); this[method](e, 'blur', scp); // onresize fires whenever the original element is resized this[method](e, 'resize', scp); // move fires whenever the original element changes positions this[method](e, 'move', scp); this[method](c, 'mouseover', vcp); this[method](c, 'mouseout', vcp); this[method](c, 'click', vcp); }, /** * Add a class to the element referenced by this RoundRect object. * @type {string} className */ addClass: function (className) { var oldClassName = ' ' + this.element.className + ' '; if (oldClassName.indexOf(' ' + className + ' ') === -1) { this.element.className += ' ' + className; } }, /** * Remove a class from the element referenced by this RoundRect object. * @type {string} className */ removeClass: function (className) { var oldClassName = ' ' + this.element.className + ' '; if (oldClassName.indexOf(' ' + className + ' ') !== -1) { this.element.className = oldClassName.replace(' ' + ns + '-hover ', ' ').replace(/^\s+|\s+$/g, ''); } }, /** * Proxy for onVmlStateChange, binds ‘this’. Defined in the * constructor. * @type {Function} * @private */ onVmlStateChangeProxy: undefined, /** * Attaches a -hover class to the element when its VML container is * hovered over, since the mouse passes right through any areas of the * rounded element that aren’t taken up by child elements. * @param {string} eventType */ onVmlStateChange: function (eventType) { if (eventType === 'click' && isTextField(this.element)) { // With RoundRect applied, text inputs can only be // clicked on where text has already been written. This // partially works around this issue. It is not perfect: // clicking empty lines in textareas, for instance, puts the // carat in the wrong place, but it works in most common cases // and is much better than the default behaviour. var range = this.element.createTextRange(); range.moveStart('textedit'); range.select(); } else if (eventType === 'click' && isButton(this.element)) { // Much like text fields, clicking on the VML part of a button // will not trigger the button click this.element.click(); } else if (eventType === 'click' && document.activeElement !== this.element && document.activeElement !== document.body) { document.activeElement.blur(); } else if (eventType === 'mouseover') { this.addClass(hoverClass); } else if (eventType === 'mouseout') { this.removeClass(hoverClass); } }, /** * Proxy for onPropertyChange, binds ‘this’ and implements a timeout * when necessary. Defined in the constructor. * @type {Function} * @private */ onPropertyChangeProxy: undefined, /** * Adjusts the VML in response to changes to the DOM to the original * element. * @param {string} property The name of the property that changed. * @private */ onPropertyChange: function (property) { var es = this.element.style, cs = this.container.style; switch (property) { case 'style.display': cs.display = (es.display === 'none') ? 'none' : 'block'; // fall through case 'style': case 'className': case 'style.cssText': this.dimensions = this.calculateDimensions(); // fall through case 'style.border': case 'style.borderTop': case 'style.borderRight': case 'style.borderBottom': case 'style.borderLeft': case 'style.borderTopWidth': case 'style.borderRightWidth': case 'style.borderBottomWidth': case 'style.borderLeftWidth': case 'style.borderWidth': this.borderWidths = this.calculateBorderWidths(); // fall through case 'style.border-radius': case 'style.border-top-left-radius': case 'style.border-top-right-radius': case 'style.border-bottom-right-radius': case 'style.border-bottom-left-radius': this.radii = this.calculateRadii(); // fall through case 'style.padding': case 'style.background': case 'style.backgroundImage': case 'style.backgroundColor': case 'style.backgroundPosition': case 'style.backgroundRepeat': this.applyVML(); break; case 'style.borderColor': this.vmlStrokeColor(); break; case 'style.visibility': cs.visibility = es.visibility; break; case 'style.filter': this.vmlOpacity(); break; case 'style.zIndex': cs.zIndex = es.zIndex; break; } }, /** * Proxy for onStateChange, binds ‘this’ and implements a timeout. * Defined in the constructor. * @type {Function} * @private */ onStateChangeProxy: undefined, /** * Reapplies VML styles in response to state change event, such as a * mouseover. * @param {string} eventType * @private */ onStateChange: function (eventType) { if (eventType === 'resize' || eventType === 'move') { var oldDimensions = this.dimensions; this.dimensions = this.calculateDimensions(); if (this.dimensions.width !== oldDimensions.width || this.dimensions.height !== oldDimensions.height || this.dimensions.top !== oldDimensions.top || this.dimensions.left !== oldDimensions.left) { this.vmlOffsets(); this.vmlPath(); } } else { // Buttons always fail to change their hover states properly; // though maybe it is just because it is really slow? // TODO: Borders width/colour doesn’t seem to update properly // even with this change for some reason. if (isButton(this.element)) { if (eventType === 'mouseenter') { this.addClass(hoverClass); } else if (eventType === 'mouseleave') { this.removeClass(hoverClass); } } this.element.runtimeStyle.cssText = ''; this.dimensions = this.calculateDimensions(); this.borderWidths = this.calculateBorderWidths(); this.radii = this.calculateRadii(); this.applyVML(); } }, /** * Calculates the appropriate radii for all corners of an element. * @return {?Array.} * @private */ calculateRadii: function () { var e = this.element, cs = e.currentStyle, defaultRadius = cs[br] || '0 0 0 0', radii, i; if ((cs[br] || cs[btl] || cs[btr] || cs[bbr] || cs[bbl]) === undefined) { // No border radius set return null; } // The first split gets rid of any vertical radii, which are not // supported. We also assume in a really naïve manner that we are // always dealing with pixels. Pixels pixels pixels pixels pixels. radii = defaultRadius.split(/\s+\//)[0].replace(/[^0-9\s]/g, '').split(/\s+/); radii[0] = (cs[btl] || '').replace(/[^0-9]/g, '') || radii[0]; radii[1] = (cs[btr] || '').replace(/[^0-9]/g, '') || radii[1]; radii[2] = (cs[bbr] || '').replace(/[^0-9]/g, '') || radii[2]; radii[3] = (cs[bbl] || '').replace(/[^0-9]/g, '') || radii[3]; // Normalize as per the css3 spec so we always have four radii for (i = 0; i < 4; ++i) { radii[i] = radii[i] === undefined ? (+radii[Math.max((i - 2), 0)]) : (+radii[i]); } // Make sure we aren’t drawing zero-radiuses because // someone decided to try to be clever and set everything to 0 // in CSS if (radii[0] + radii[1] + radii[2] + radii[3] === 0) { return null; } return radii; }, /** * Calculates the width, height, top, and left offset of the element. * @return {Object.} Object with four keys: width, * height, top, left. * @private */ calculateDimensions: function () { return { width: this.element.offsetWidth, height: this.element.offsetHeight, left: this.element.offsetLeft, top: this.element.offsetTop }; }, /** * Calculates the element’s border widths. * @return {Object.} Object with four keys: top, * right, bottom, left. * @private */ calculateBorderWidths: function () { var cs = this.element.currentStyle; return { top: parseInt(cs.borderTopWidth, 10) || 0, right: parseInt(cs.borderRightWidth, 10) || 0, bottom: parseInt(cs.borderBottomWidth, 10) || 0, left: parseInt(cs.borderLeftWidth, 10) || 0 }; }, /** * Draws VML for an element and puts it on the DOM. * @private */ render: function () { var e = this.element, cs = e.currentStyle, tagName = e.nodeName.toUpperCase(), i; /** * Forces an element to have layout. * @param {...Element} var_args */ function forceLayout(var_args) { for (var e, i = 0, j = arguments.length; i < j; ++i) { e = arguments[i]; e.style.zoom = 1; if (e.currentStyle.position === 'static') { e.style.position = 'relative'; // We reset these just in case the element has been flipped // from positioned to static at some point in the past; // don’t want it to suddenly reposition itself somewhere // else e.style.top = 'auto'; e.style.right = 'auto'; e.style.bottom = 'auto'; e.style.left = 'auto'; } } } // Not sure why we check if currentStyle doesn’t exist, since it // always should at this point, but it was in the original code // (which I assume is more widely tested…) if (!cs || RoundRect.disallowed[tagName] || (this.radii = this.calculateRadii()) === null) { return; } e.style.behavior = 'none'; // hasLayout is required on the element and its parent in order to // provide accurate positioning and to prevent VML layout bugs // (like having the VML render itself over the content). If your // everything breaks, uh, sorry! this.originalStyles = { zoom: e.style.zoom, position: e.style.position, top: e.style.top, right: e.style.right, bottom: e.style.bottom, left: e.style.left }; // According to Drew, if “something” accidentally matches this, // you'll get infinitely-created elements and a frozen browser. // No, I don’t know what that means, either. this.container = document.createElement(ns); this.container.runtimeStyle.cssText = 'behavior:none;position:absolute;margin:0;padding:0;border:0;background:none;'; this.container.style.zIndex = cs.zIndex; // build elements corresponding to parts of the background this.vml = { color: null, image: null, stroke: null }; for (i in this.vml) { if (this.vml.hasOwnProperty(i)) { this.vml[i] = document.createElement(xmlns + ':shape'); this.vml[i].filler = document.createElement(xmlns + ':fill'); this.vml[i].appendChild(this.vml[i].filler); this.vml[i].stroked = false; this.vml[i].style.position = 'absolute'; this.vml[i].style.zIndex = cs.zIndex; this.vml[i].coordorigin = '1,1'; this.container.appendChild(this.vml[i]); } } this.vml.image.fillcolor = 'none'; this.vml.image.filler.type = 'tile'; e.parentNode.insertBefore(this.container, e); forceLayout(e.offsetParent, e, this.container); if (tagName === 'IMG') { e.style.visibility = 'hidden'; } else if (isTextField(e)) { this.container.style.cursor = cs.cursor === 'auto' ? 'text' : cs.cursor; } else if (isButton(e)) { this.container.style.cursor = cs.cursor === 'auto' ? 'pointer' : cs.cursor; } // Without a timeout, IE will throw “unspecified error”s setTimeout(proxy(this, function () { this.addEvent('container', 'mouseenter', proxy(this, function () { var fakeEvent = document.createEventObject(window.event); fakeEvent.toElement = fakeEvent.srcElement = this.element; this.element.fireEvent('on' + window.event.type, fakeEvent); })); this.addEvent('container', 'mouseleave', proxy(this, function () { var fakeEvent = document.createEventObject(window.event); fakeEvent.fromElement = fakeEvent.srcElement = this.element; this.element.fireEvent('on' + window.event.type, fakeEvent); })); this.dimensions = this.calculateDimensions(); this.borderWidths = this.calculateBorderWidths(); this.applyVML(); })); }, /** * Applies all changes to the VML elements for an element. You can call * this if you are not using RoundRect’s dynamic properties * functionality and need to update the style, or if it doesn’t work * properly for some reason. */ applyVML: function () { // If the element thinks it is invisible, chances are it was // created back when it was inside a container with display: none, // so let’s just refresh it now. if (this.dimensions.width === 0 || this.dimensions.height === 0) { this.dimensions = this.calculateDimensions(); } // Nope, still invisible. if (this.dimensions.width === 0 || this.dimensions.height === 0) { return; } this.element.runtimeStyle.cssText = ''; this.vmlFill(); this.vmlStrokeColor(); this.vmlOffsets(); this.vmlPath(); this.padBorder(); this.vmlOpacity(); }, /** * Updates the opacity of the VML elements that belong to the element. * @private */ vmlOpacity: function () { var e = this.element, cs = e.currentStyle, opacity, vml = this.vml; if ((opacity = /Opacity=([0-9]+)/i.exec(cs.filter))) { opacity = (+opacity[1]) * 0.01; for (var i in vml) { if (vml.hasOwnProperty(i)) { vml[i].filler.opacity = opacity; } } } }, /** * Updates the element’s border-color. * @private */ vmlStrokeColor: function () { this.vml.stroke.fillcolor = this.element.currentStyle.borderColor; }, /** * Moves VML elements to correspond with an change to the element’s * width, height, top, or left offsets. * @private */ vmlOffsets: function () { var dimensions = this.dimensions, i, vml = this.vml, multiplier, parent = this.element.parentNode; /** * Copies style properties from the dimensions hash to another * element. * @param {Element} e * @param {boolean} topLeft Whether to copy top/left dimensions. */ function assign(e, topLeft) { e.style.left = (topLeft ? 0 : dimensions.left) + 'px'; e.style.top = (topLeft ? 0 : dimensions.top) + 'px'; e.style.width = dimensions.width + 'px'; e.style.height = dimensions.height + 'px'; } for (i in vml) { if (vml.hasOwnProperty(i)) { multiplier = (i === 'image') ? 1 : 2; vml[i].coordsize = (dimensions.width * multiplier) + ',' + (dimensions.height * multiplier); assign(vml[i], true); } } assign(this.container, false); // IE7 inappropriately collapses table cells and gives outlandish // values for offsetWidth if (!ie8 && (parent.nodeName.toUpperCase() === 'TD' || parent.nodeName.toUpperCase() === 'TH')) { parent.style.width = ''; if (parent.currentStyle.width === 'auto') { parent.style.width = dimensions.width + 'px'; } } // I don’t know what this was *supposed* to do, but it seems to // just fuck up the borders. /*if (ie8) { vml.stroke.style.margin = '-1px'; borderWidths = this.calculateBorderWidths(); vml.color.style.margin = (borderWidths.top - 1) + 'px ' + (borderWidths.left - 1) + 'px'; }*/ }, /** * Draws some fucking VML to some fucking VML elements. Fuck yeah! * Needs a little love. * @private */ vmlPath: function () { var borderWidths = this.borderWidths, dimensions = this.dimensions, radii = this.radii.slice(), vml = this.vml, i; /** * Generates a VML path string for a given set of coordinates. * @param {boolean} direction Whether or not to draw clockwise * @param {number} w Width * @param {number} h Height * @param {Array.} r Radii * @param {number} aL Left offset * @param {number} aT Top offset * @param {number} mult Multiplier * @return {string} A VML path string. */ function coords(direction, w, h, r, aL, aT, mult) { var cmd = direction ? ['m', 'qy', 'l', 'qx', 'l', 'qy', 'l', 'qx', 'l'] : ['qx', 'l', 'qy', 'l', 'qx', 'l', 'qy', 'l', 'm'], R = r.slice(), // clone of array i, cmdCoords; aL *= mult; aT *= mult; w *= mult; h *= mult; for (i = 0; i < 4; ++i) { R[i] *= mult; // Avoid funky corner shapes caused by too large radii R[i] = Math.min(w * 0.5, h * 0.5, R[i]); } cmdCoords = [ cmd[0] + Math.floor(0 + aL) + ',' + Math.floor(R[0] + aT), cmd[1] + Math.floor(R[0] + aL) + ',' + Math.floor(0 + aT), cmd[2] + Math.ceil(w - R[1] + aL) + ',' + Math.floor(0 + aT), cmd[3] + Math.ceil(w + aL) + ',' + Math.floor(R[1] + aT), cmd[4] + Math.ceil(w + aL) + ',' + Math.ceil(h - R[2] + aT), cmd[5] + Math.ceil(w - R[2] + aL) + ',' + Math.ceil(h + aT), cmd[6] + Math.floor(R[3] + aL) + ',' + Math.ceil(h + aT), cmd[7] + Math.floor(0 + aL) + ',' + Math.ceil(h - R[3] + aT), cmd[8] + Math.floor(0 + aL) + ',' + Math.floor(R[0] + aT) ]; if (!direction) { cmdCoords.reverse(); } var path = cmdCoords.join(''); return path; } if (borderWidths === undefined) { borderWidths = this.calculateBorderWidths(); } /* determine outer curves */ var outer = coords(true, dimensions.width, dimensions.height, radii, 0, 0, 2); /* determine inner curves */ radii[0] -= Math.max(borderWidths.left, borderWidths.top); radii[1] -= Math.max(borderWidths.top, borderWidths.right); radii[2] -= Math.max(borderWidths.right, borderWidths.bottom); radii[3] -= Math.max(borderWidths.bottom, borderWidths.left); for (i = 0; i < 4; ++i) { radii[i] = Math.max(radii[i], 0); } var inner = coords( false, dimensions.width - borderWidths.left - borderWidths.right, dimensions.height - borderWidths.top - borderWidths.bottom, radii, borderWidths.left, borderWidths.top, 2); var image = coords( true, dimensions.width - borderWidths.left - borderWidths.right + 1, dimensions.height - borderWidths.top - borderWidths.bottom + 1, radii, borderWidths.left, borderWidths.top, 1); vml.color.path = inner; vml.image.path = image; vml.stroke.path = outer + inner; this.clipImage(); }, /** * Replaces borders on the original element with more padding, because * the border is redrawn in VML. * @private */ padBorder: function () { var e = this.element, props = [ 'Top', 'Right', 'Bottom', 'Left' ], i; for (i = 0; i < 4; ++i) { e.runtimeStyle['padding' + props[i]] = (getPixelValue(e, 'padding' + props[i]) + getPixelValue(e, 'border' + props[i] + 'Width')) + 'px'; } e.runtimeStyle.border = 'none'; }, /** * Updates the background of the element. * Needs some fixing up. * @private */ vmlFill: function () { var e = this.element, cs = e.currentStyle, isImg = this.element.tagName.toUpperCase() === 'IMG', vml = this.vml, vmlBg, img; e.runtimeStyle.backgroundColor = ''; e.runtimeStyle.backgroundImage = ''; if (isImg || cs.backgroundImage !== 'none') { // if the element is an image element, use the src; otherwise, // use the backgroundImage. this.backgroundImage = vmlBg = isImg ? e.src : /^url\(["']?\s*(.+?)\s*["']?\)$/.exec(cs.backgroundImage)[1]; // Determine the size of the loaded image if (imageMap[vmlBg] === undefined) { img = new Image(); img.attachEvent('onload', proxy(this, function () { // Replace the Image object in the map with something // more primitive to save memory imageMap[vmlBg] = { width: this.width, height: this.height }; this.vmlOffsets(); })); img.src = vmlBg; imageMap[vmlBg] = img; } vml.image.filler.src = vmlBg; isImg = true; } vml.image.filled = isImg; vml.image.fillcolor = 'none'; vml.color.filled = cs.backgroundColor !== 'transparent'; vml.color.fillcolor = cs.backgroundColor; e.runtimeStyle.backgroundImage = 'none'; e.runtimeStyle.backgroundColor = 'transparent'; }, /** * Clips background image to the size of the container. * Needs overhauling. * @private */ clipImage: function () { var e = this.element, cs = e.currentStyle, vmlBg = this.backgroundImage, dimensions = this.calculateDimensions(), borderWidths = this.calculateBorderWidths(), vml = this.vml; if (vmlBg === undefined || imageMap[vmlBg] === undefined) { return; } var bg = {'X' : 0, 'Y' : 0 }; /** * Determine the position of the background, given the percentage * where it is supposed to be placed. * Abusive function. Abusive. Horrible. Fucking awful. * @param {string} axis X or Y. * @param {(string|number)} position */ function figurePercentage(axis, position) { var fraction = true; switch (position) { case 'left': case 'top': bg[axis] = 0; break; case 'center': bg[axis] = 0.5; break; case 'right': case 'bottom': bg[axis] = 1; break; default: if (position.indexOf('%') !== -1) { bg[axis] = parseInt(position, 10) * 0.01; } else { fraction = false; } break; } var horz = (axis === 'X'); bg[axis] = Math.ceil(fraction ? ((dimensions[horz ? 'width' : 'height'] - (borderWidths[horz ? 'left' : 'top'] + borderWidths[horz ? 'right' : 'bottom'])) * bg[axis]) - (imageMap[vmlBg][horz ? 'width' : 'height'] * bg[axis]) : parseInt(position, 10)); bg[axis] += 1; } for (var b in bg) { if (bg.hasOwnProperty(b)) { // TODO: yes let us call a function instead of just inlining it, // that makes much more sense :| figurePercentage(b, cs['backgroundPosition' + b]); } } vml.image.filler.position = (bg.X / (dimensions.width - borderWidths.left - borderWidths.right + 1)) + ',' + (bg.Y / (dimensions.height - borderWidths.top - borderWidths.bottom + 1)); // defaults. named c! makes perfect sense. var c = {'T' : 1, 'R' : dimensions.width + 1, 'B' : dimensions.height + 1, 'L' : 1}; var altC = { 'X': {'b1' : 'L', 'b2' : 'R', 'd' : 'Width'}, 'Y': {'b1': 'T', 'b2': 'B', 'd': 'Height'} }; // non-repeating, or repeat in one direction only if (cs.backgroundRepeat !== 'repeat') { // defaults for no-repeat // race condition! hottt!! c = {'T' : bg.Y, 'R' : (bg.X + imageMap[vmlBg].width), 'B' : (bg.Y + imageMap[vmlBg].height), 'L' : bg.X }; // repeat-x or repeat-y /* now let's revert to dC for repeat-x or repeat-y */ if (cs.backgroundRepeat.indexOf('repeat-') !== -1) { var v = cs.backgroundRepeat.split('repeat-')[1].toUpperCase(); c[altC[v].b1] = 1; c[altC[v].b2] = dimensions[altC[v].d.toLowerCase()] + 1; } // This seems to cause badness when negative positioning is involved // if (c.B > dimensions.height) { // c.B = dimensions.height + 1; // } } vml.image.style.clip = 'rect(' + c.T + 'px ' + c.R + 'px ' + c.B + 'px ' + c.L + 'px)'; } }; // Expose RoundRect to the world! window[ns] = RoundRect; }());