Mercurial > caja-test
diff js/ext/bridal.js @ 0:633c9cb05555
Origination.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Sun, 07 Jun 2009 19:29:10 -0700 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/js/ext/bridal.js Sun Jun 07 19:29:10 2009 -0700 @@ -0,0 +1,550 @@ +// Copyright (C) 2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * @fileoverview + * A set of utility functions that implement browser feature testing to unify + * certain DOM behaviors, and a set of recommendations about when to use these + * functions as opposed to the native DOM functions. + * + * @author ihab.awad@gmail.com + * @author jasvir@gmail.com + * @provides bridal + * @requires ___, cajita, document, html, html4, navigator + */ + +var bridal = (function() { + + //////////////////////////////////////////////////////////////////////////// + // Private section + //////////////////////////////////////////////////////////////////////////// + + var isOpera = navigator.userAgent.indexOf('Opera') === 0; + var isIE = !isOpera && navigator.userAgent.indexOf('MSIE') !== -1; + var isWebkit = !isOpera && navigator.userAgent.indexOf('WebKit') !== -1; + + var features = { + attachEvent: !!(document.createElement('div').attachEvent), + setAttributeExtraParam: isIE, + /** + * Does the extended form of extendedCreateElement work? + * From http://msdn.microsoft.com/en-us/library/ms536389.aspx :<blockquote> + * You can also specify all the attributes inside the createElement + * method by using an HTML string for the method argument. + * The following example demonstrates how to dynamically create two + * radio buttons utilizing this technique. + * <pre> + * ... + * var newRadioButton = document.createElement( + * "<INPUT TYPE='RADIO' NAME='RADIOTEST' VALUE='First Choice'>") + * </pre> + * </blockquote> + */ + extendedCreateElement: ( + function () { + try { + var inp = document.createElement('<input name="x" type="radio">'); + return inp.name === 'x' && inp.type === 'radio'; + } catch (ex) { + return false; + } + })() + }; + + var CUSTOM_EVENT_TYPE_SUFFIX = '_custom___'; + function tameEventType(type, opt_isCustom) { + type = String(type); + if (endsWith__.test(type)) { + throw new Error('Invalid event type ' + type); + } + if (opt_isCustom + || html4.atype.SCRIPT !== html4.ATTRIBS['*:on' + type]) { + type = type + CUSTOM_EVENT_TYPE_SUFFIX; + } + return type; + } + + function eventHandlerTypeFilter(handler, tameType) { + // This does not need to check that handler is callable by untrusted code + // since the handler will invoke plugin_dispatchEvent which will do that + // check on the untrusted function reference. + return function (event) { + if (tameType === event.eventType___) { + return handler.call(this, event); + } + }; + } + + var endsWith__ = /__$/; + function constructClone(node, deep) { + var clone; + if (node.nodeType === 1) { + // From http://blog.pengoworks.com/index.cfm/2007/7/16/IE6--IE7-quirks-with-cloneNode-and-form-elements + // It turns out IE 6/7 doesn't properly clone some form elements + // when you use the cloneNode(true) and the form element is a + // checkbox, radio or select element. + // JQuery provides a clone method which attempts to fix this and an issue + // with event listeners. According to the source code for JQuery's clone + // method ( http://docs.jquery.com/Manipulation/clone#true ): + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // We do not need to deal with XHTML DOMs and so can skip the clean step + // that jQuery does. + var tagDesc = node.tagName; + // Copying form state is not strictly mentioned in DOM2's spec of + // cloneNode, but all implementations do it. The value copying + // can be interpreted as fixing implementations' failure to have + // the value attribute "reflect" the input's value as determined by the + // value property. + switch (node.tagName) { + case 'INPUT': + tagDesc = '<input name="' + html.escapeAttrib(node.name) + + '" type="' + html.escapeAttrib(node.type) + + '" value="' + html.escapeAttrib(node.defaultValue) + '"' + + (node.defaultChecked ? ' checked="checked">' : '>'); + break; + case 'OPTION': + tagDesc = '<option ' + + (node.defaultSelected ? ' selected="selected">' : '>'); + break; + case 'TEXTAREA': + tagDesc = '<textarea value="' + + html.escapeAttrib(node.defaultValue) + '">'; + break; + } + + clone = document.createElement(tagDesc); + + var attrs = node.attributes; + for (var i = 0, attr; (attr = attrs[i]); ++i) { + if (attr.specified && !endsWith__.test(attr.name)) { + clone.setAttribute(attr.nodeName, attr.nodeValue); + } + } + } else { + clone = node.cloneNode(false); + } + if (deep) { + // TODO(mikesamuel): should we whitelist nodes here, to e.g. prevent + // untrusted code from reloading an already loaded script by cloning + // a script node that somehow exists in a tree accessible to it? + for (var child = node.firstChild; child; child = child.nextSibling) { + var cloneChild = constructClone(child, deep); + clone.appendChild(cloneChild); + } + } + return clone; + } + + function fixupClone(node, clone) { + for (var child = node.firstChild, cloneChild = clone.firstChild; cloneChild; + child = child.nextSibling, cloneChild = cloneChild.nextSibling) { + fixupClone(child, cloneChild); + } + if (node.nodeType === 1) { + switch (node.tagName) { + case 'INPUT': + clone.value = node.value; + clone.checked = node.checked; + break; + case 'OPTION': + clone.selected = node.selected; + clone.value = node.value; + break; + case 'TEXTAREA': + clone.value = node.value; + break; + } + } + + // Do not copy listeners since DOM2 specifies that only attributes and + // children are copied, and that children should only be copied if the + // deep flag is set. + // The children are handled in constructClone. + var originalAttribs = node.attributes___; + if (originalAttribs) { + var attribs = {}; + clone.attributes___ = attribs; + cajita.forOwnKeys(originalAttribs, ___.func(function (k, v) { + switch (typeof v) { + case 'string': case 'number': case 'boolean': + attribs[k] = v; + break; + } + })); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Public section + //////////////////////////////////////////////////////////////////////////// + + function untameEventType(type) { + var suffix = CUSTOM_EVENT_TYPE_SUFFIX; + var tlen = type.length, slen = suffix.length; + var end = tlen - slen; + if (end >= 0 && suffix === type.substring(end)) { + type = type.substring(0, end); + } + return type; + } + + function initEvent(event, type, bubbles, cancelable) { + type = tameEventType(type, true); + bubbles = Boolean(bubbles); + cancelable = Boolean(cancelable); + + if (event.initEvent) { // Non-IE + event.initEvent(type, bubbles, cancelable); + } else if (bubbles && cancelable) { // IE + event.eventType___ = type; + } else { + // TODO(mikesamuel): can bubbling and cancelable on events be simulated + // via http://msdn.microsoft.com/en-us/library/ms533545(VS.85).aspx + throw new Error( + 'Browser does not support non-bubbling/uncanceleable events'); + } + } + + function dispatchEvent(element, event) { + // TODO(mikesamuel): when we change event dispatching to happen + // asynchronously, we should exempt custom events since those + // need to return a useful value, and there may be code bracketing + // them which could observe asynchronous dispatch. + + // "The return value of dispatchEvent indicates whether any of + // the listeners which handled the event called + // preventDefault. If preventDefault was called the value is + // false, else the value is true." + if (element.dispatchEvent) { + return Boolean(element.dispatchEvent(event)); + } else { + // Only dispatches custom events as when tameEventType(t) !== t. + element.fireEvent('ondataavailable', event); + return Boolean(event.returnValue); + } + } + + /** + * Add an event listener function to an element. + * + * <p>Replaces + * W3C <code>Element::addEventListener</code> and + * IE <code>Element::attachEvent</code>. + * + * @param {HTMLElement} element a native DOM element. + * @param {string} type a string identifying the event type. + * @param {boolean Element::function (event)} handler an event handler. + * @param {boolean} useCapture whether the user wishes to initiate capture. + * @return {boolean Element::function (event)} the handler added. May be + * a wrapper around the input. + */ + function addEventListener(element, type, handler, useCapture) { + type = String(type); + var tameType = tameEventType(type); + if (features.attachEvent) { + // TODO(ihab.awad): How do we emulate 'useCapture' here? + if (type !== tameType) { + var wrapper = eventHandlerTypeFilter(handler, tameType); + element.attachEvent('ondataavailable', wrapper); + return wrapper; + } else { + element.attachEvent('on' + type, handler); + return handler; + } + } else { + // FF2 fails if useCapture not passed or is not a boolean. + element.addEventListener(tameType, handler, useCapture); + return handler; + } + } + + /** + * Remove an event listener function from an element. + * + * <p>Replaces + * W3C <code>Element::removeEventListener</code> and + * IE <code>Element::detachEvent</code>. + * + * @param element a native DOM element. + * @param type a string identifying the event type. + * @param handler a function acting as an event handler. + * @param useCapture whether the user wishes to initiate capture. + */ + function removeEventListener(element, type, handler, useCapture) { + type = String(type); + var tameType = tameEventType(type); + if (features.attachEvent) { + // TODO(ihab.awad): How do we emulate 'useCapture' here? + if (tameType !== type) { + element.detachEvent('ondataavailable', handler); + } else { + element.detachEvent('on' + type, handler); + } + } else { + element.removeEventListener(tameType, handler, useCapture); + } + } + + /** + * Clones a node per {@code Node.clone()}. + * <p> + * Returns a duplicate of this node, i.e., serves as a generic copy + * constructor for nodes. The duplicate node has no parent; + * (parentNode is null.). + * <p> + * Cloning an Element copies all attributes and their values, + * including those generated by the XML processor to represent + * defaulted attributes, but this method does not copy any text it + * contains unless it is a deep clone, since the text is contained + * in a child Text node. Cloning an Attribute directly, as opposed + * to be cloned as part of an Element cloning operation, returns a + * specified attribute (specified is true). Cloning any other type + * of node simply returns a copy of this node. + * <p> + * Note that cloning an immutable subtree results in a mutable copy, + * but the children of an EntityReference clone are readonly. In + * addition, clones of unspecified Attr nodes are specified. And, + * cloning Document, DocumentType, Entity, and Notation nodes is + * implementation dependent. + * + * @param {boolean} deep If true, recursively clone the subtree + * under the specified node; if false, clone only the node itself + * (and its attributes, if it is an Element). + * + * @return {Node} The duplicate node. + */ + function cloneNode(node, deep) { + var clone; + if (!document.all) { // Not IE 6 or IE 7 + clone = node.cloneNode(deep); + } else { + clone = constructClone(node, deep); + } + fixupClone(node, clone); + return clone; + } + + /** + * Create a <code>style</code> element for a document containing some + * specified CSS text. Does not add the element to the document: the client + * may do this separately if desired. + * + * <p>Replaces directly creating the <code>style</code> element and + * populating its contents. + * + * @param document a DOM document. + * @param cssText a string containing a well-formed stylesheet production. + * @return a <code>style</code> element for the specified document. + */ + function createStylesheet(document, cssText) { + // Courtesy Stoyan Stefanov who documents the derivation of this at + // http://www.phpied.com/dynamic-script-and-style-elements-in-ie/ and + // http://yuiblog.com/blog/2007/06/07/style/ + var styleSheet = document.createElement('style'); + styleSheet.setAttribute('type', 'text/css'); + if (styleSheet.styleSheet) { // IE + styleSheet.styleSheet.cssText = cssText; + } else { // the world + styleSheet.appendChild(document.createTextNode(cssText)); + } + return styleSheet; + } + + /** + * Set an attribute on a DOM node. + * + * <p>Replaces DOM <code>Node::setAttribute</code>. + * + * @param {HTMLElement} element a DOM element. + * @param {string} name the name of an attribute. + * @param {string} value the value of an attribute. + */ + function setAttribute(element, name, value) { + switch (name) { + case 'style': + if ((typeof element.style.cssText) === 'string') { + // Setting the 'style' attribute does not work for IE, but + // setting cssText works on IE 6, Firefox, and IE 7. + element.style.cssText = value; + return value; + } + break; + case 'class': + element.className = value; + return value; + case 'for': + element.htmlFor = value; + return value; + } + if (features.setAttributeExtraParam) { + element.setAttribute(name, value, 0); + } else { + element.setAttribute(name, value); + } + return value; + } + + /** + * See <a href="http://www.w3.org/TR/cssom-view/#the-getclientrects" + * >ElementView.getBoundingClientRect()</a>. + * @return {Object} duck types as a TextRectangle with numeric fields + * {@code left}, {@code right}, {@code top}, and {@code bottom}. + */ + function getBoundingClientRect(el) { + var doc = el.ownerDocument; + // Use the native method if present. + if (el.getBoundingClientRect) { + var cRect = el.getBoundingClientRect(); + if (isIE) { + // IE has an unnecessary border, which can be mucked with by styles, so + // the amount of border is not predictable. + // Depending on whether the document is in quirks or standards mode, + // the border will be present on either the HTML or BODY elements. + var fixupLeft = doc.documentElement.clientLeft + doc.body.clientLeft; + cRect.left -= fixupLeft; + cRect.right -= fixupLeft; + var fixupTop = doc.documentElement.clientTop + doc.body.clientTop; + cRect.top -= fixupTop; + cRect.bottom -= fixupTop; + } + return ({ + top: +cRect.top, + left: +cRect.left, + right: +cRect.right, + bottom: +cRect.bottom + }); + } + + // Otherwise, try using the deprecated gecko method, or emulate it in + // horribly inefficient ways. + + // http://code.google.com/p/doctype/wiki/ArticleClientViewportElement + var viewport = (isIE && doc.compatMode === 'CSS1Compat') + ? doc.body : doc.documentElement; + + // Figure out the position relative to the viewport. + // From http://code.google.com/p/doctype/wiki/ArticlePageOffset + var pageX = 0, pageY = 0; + if (el === viewport) { + // The viewport is the origin. + } else if (doc.getBoxObjectFor) { // Handles Firefox < 3 + var elBoxObject = doc.getBoxObjectFor(el); + var viewPortBoxObject = doc.getBoxObjectFor(viewport); + pageX = elBoxObject.screenX - viewPortBoxObject.screenX; + pageY = elBoxObject.screenY - viewPortBoxObject.screenY; + } else { + // Walk the offsetParent chain adding up offsets. + for (var op = el; (op && op !== el); op = op.offsetParent) { + pageX += op.offsetLeft; + pageY += op.offsetTop; + if (op !== el) { + pageX += op.clientLeft || 0; + pageY += op.clientTop || 0; + } + if (isWebkit) { + // On webkit the offsets for position:fixed elements are off by the + // scroll offset. + var opPosition = doc.defaultView.getComputedStyle(op, 'position'); + if (opPosition === 'fixed') { + pageX += doc.body.scrollLeft; + pageY += doc.body.scrollTop; + } + break; + } + } + + // Opera & (safari absolute) incorrectly account for body offsetTop + if ((isWebkit + && doc.defaultView.getComputedStyle(el, 'position') === 'absolute') + || isOpera) { + pageY -= doc.body.offsetTop; + } + + // Accumulate the scroll positions for everything but the body element + for (var op = el; (op = op.offsetParent) && op !== doc.body;) { + pageX -= op.scrollLeft; + // see https://bugs.opera.com/show_bug.cgi?id=249965 + if (!isOpera || op.tagName !== 'TR') { + pageY -= op.scrollTop; + } + } + } + + // Figure out the viewport container so we can subtract the window's + // scroll offsets. + var scrollEl = !isWebkit && doc.compatMode === 'CSS1Compat' + ? doc.documentElement + : doc.body; + + var left = pageX - scrollEl.scrollLeft, top = pageY - scrollEl.scrollTop; + return ({ + top: top, + left: left, + right: left + el.clientWidth, + bottom: top + el.clientHeight + }); + } + + /** + * Returns the value of the named attribute on element. + * + * @param {HTMLElement} element a DOM element. + * @param {string} name the name of an attribute. + */ + function getAttribute(element, name) { + switch (name) { + case 'style': + if ((typeof element.style.cssText) === 'string') { + return element.style.cssText; + } + break; + case 'class': + return element.className; + case 'for': + return element.htmlFor; + } + return element.getAttribute(name); + } + + function getAttributeNode(element, name) { + return element.getAttributeNode(name); + } + + function hasAttribute(element, name) { + if (element.hasAttribute) { // Non IE + return element.hasAttribute(name); + } else { + var attr = getAttributeNode(element, name); + return attr !== null && attr.specified; + } + } + + return { + addEventListener: addEventListener, + removeEventListener: removeEventListener, + initEvent: initEvent, + dispatchEvent: dispatchEvent, + cloneNode: cloneNode, + createStylesheet: createStylesheet, + setAttribute: setAttribute, + getAttribute: getAttribute, + getAttributeNode: getAttributeNode, + hasAttribute: hasAttribute, + getBoundingClientRect: getBoundingClientRect, + untameEventType: untameEventType, + extendedCreateElementFeature: features.extendedCreateElement + }; +})();