comparison js/ext/bridal.js @ 0:633c9cb05555

Origination.
author Atul Varma <varmaa@toolness.com>
date Sun, 07 Jun 2009 19:29:10 -0700
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:633c9cb05555
1 // Copyright (C) 2008 Google Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 /**
16 * @fileoverview
17 * A set of utility functions that implement browser feature testing to unify
18 * certain DOM behaviors, and a set of recommendations about when to use these
19 * functions as opposed to the native DOM functions.
20 *
21 * @author ihab.awad@gmail.com
22 * @author jasvir@gmail.com
23 * @provides bridal
24 * @requires ___, cajita, document, html, html4, navigator
25 */
26
27 var bridal = (function() {
28
29 ////////////////////////////////////////////////////////////////////////////
30 // Private section
31 ////////////////////////////////////////////////////////////////////////////
32
33 var isOpera = navigator.userAgent.indexOf('Opera') === 0;
34 var isIE = !isOpera && navigator.userAgent.indexOf('MSIE') !== -1;
35 var isWebkit = !isOpera && navigator.userAgent.indexOf('WebKit') !== -1;
36
37 var features = {
38 attachEvent: !!(document.createElement('div').attachEvent),
39 setAttributeExtraParam: isIE,
40 /**
41 * Does the extended form of extendedCreateElement work?
42 * From http://msdn.microsoft.com/en-us/library/ms536389.aspx :<blockquote>
43 * You can also specify all the attributes inside the createElement
44 * method by using an HTML string for the method argument.
45 * The following example demonstrates how to dynamically create two
46 * radio buttons utilizing this technique.
47 * <pre>
48 * ...
49 * var newRadioButton = document.createElement(
50 * "&lt;INPUT TYPE='RADIO' NAME='RADIOTEST' VALUE='First Choice'>")
51 * </pre>
52 * </blockquote>
53 */
54 extendedCreateElement: (
55 function () {
56 try {
57 var inp = document.createElement('<input name="x" type="radio">');
58 return inp.name === 'x' && inp.type === 'radio';
59 } catch (ex) {
60 return false;
61 }
62 })()
63 };
64
65 var CUSTOM_EVENT_TYPE_SUFFIX = '_custom___';
66 function tameEventType(type, opt_isCustom) {
67 type = String(type);
68 if (endsWith__.test(type)) {
69 throw new Error('Invalid event type ' + type);
70 }
71 if (opt_isCustom
72 || html4.atype.SCRIPT !== html4.ATTRIBS['*:on' + type]) {
73 type = type + CUSTOM_EVENT_TYPE_SUFFIX;
74 }
75 return type;
76 }
77
78 function eventHandlerTypeFilter(handler, tameType) {
79 // This does not need to check that handler is callable by untrusted code
80 // since the handler will invoke plugin_dispatchEvent which will do that
81 // check on the untrusted function reference.
82 return function (event) {
83 if (tameType === event.eventType___) {
84 return handler.call(this, event);
85 }
86 };
87 }
88
89 var endsWith__ = /__$/;
90 function constructClone(node, deep) {
91 var clone;
92 if (node.nodeType === 1) {
93 // From http://blog.pengoworks.com/index.cfm/2007/7/16/IE6--IE7-quirks-with-cloneNode-and-form-elements
94 // It turns out IE 6/7 doesn't properly clone some form elements
95 // when you use the cloneNode(true) and the form element is a
96 // checkbox, radio or select element.
97 // JQuery provides a clone method which attempts to fix this and an issue
98 // with event listeners. According to the source code for JQuery's clone
99 // method ( http://docs.jquery.com/Manipulation/clone#true ):
100 // IE copies events bound via attachEvent when
101 // using cloneNode. Calling detachEvent on the
102 // clone will also remove the events from the orignal
103 // We do not need to deal with XHTML DOMs and so can skip the clean step
104 // that jQuery does.
105 var tagDesc = node.tagName;
106 // Copying form state is not strictly mentioned in DOM2's spec of
107 // cloneNode, but all implementations do it. The value copying
108 // can be interpreted as fixing implementations' failure to have
109 // the value attribute "reflect" the input's value as determined by the
110 // value property.
111 switch (node.tagName) {
112 case 'INPUT':
113 tagDesc = '<input name="' + html.escapeAttrib(node.name)
114 + '" type="' + html.escapeAttrib(node.type)
115 + '" value="' + html.escapeAttrib(node.defaultValue) + '"'
116 + (node.defaultChecked ? ' checked="checked">' : '>');
117 break;
118 case 'OPTION':
119 tagDesc = '<option '
120 + (node.defaultSelected ? ' selected="selected">' : '>');
121 break;
122 case 'TEXTAREA':
123 tagDesc = '<textarea value="'
124 + html.escapeAttrib(node.defaultValue) + '">';
125 break;
126 }
127
128 clone = document.createElement(tagDesc);
129
130 var attrs = node.attributes;
131 for (var i = 0, attr; (attr = attrs[i]); ++i) {
132 if (attr.specified && !endsWith__.test(attr.name)) {
133 clone.setAttribute(attr.nodeName, attr.nodeValue);
134 }
135 }
136 } else {
137 clone = node.cloneNode(false);
138 }
139 if (deep) {
140 // TODO(mikesamuel): should we whitelist nodes here, to e.g. prevent
141 // untrusted code from reloading an already loaded script by cloning
142 // a script node that somehow exists in a tree accessible to it?
143 for (var child = node.firstChild; child; child = child.nextSibling) {
144 var cloneChild = constructClone(child, deep);
145 clone.appendChild(cloneChild);
146 }
147 }
148 return clone;
149 }
150
151 function fixupClone(node, clone) {
152 for (var child = node.firstChild, cloneChild = clone.firstChild; cloneChild;
153 child = child.nextSibling, cloneChild = cloneChild.nextSibling) {
154 fixupClone(child, cloneChild);
155 }
156 if (node.nodeType === 1) {
157 switch (node.tagName) {
158 case 'INPUT':
159 clone.value = node.value;
160 clone.checked = node.checked;
161 break;
162 case 'OPTION':
163 clone.selected = node.selected;
164 clone.value = node.value;
165 break;
166 case 'TEXTAREA':
167 clone.value = node.value;
168 break;
169 }
170 }
171
172 // Do not copy listeners since DOM2 specifies that only attributes and
173 // children are copied, and that children should only be copied if the
174 // deep flag is set.
175 // The children are handled in constructClone.
176 var originalAttribs = node.attributes___;
177 if (originalAttribs) {
178 var attribs = {};
179 clone.attributes___ = attribs;
180 cajita.forOwnKeys(originalAttribs, ___.func(function (k, v) {
181 switch (typeof v) {
182 case 'string': case 'number': case 'boolean':
183 attribs[k] = v;
184 break;
185 }
186 }));
187 }
188 }
189
190 ////////////////////////////////////////////////////////////////////////////
191 // Public section
192 ////////////////////////////////////////////////////////////////////////////
193
194 function untameEventType(type) {
195 var suffix = CUSTOM_EVENT_TYPE_SUFFIX;
196 var tlen = type.length, slen = suffix.length;
197 var end = tlen - slen;
198 if (end >= 0 && suffix === type.substring(end)) {
199 type = type.substring(0, end);
200 }
201 return type;
202 }
203
204 function initEvent(event, type, bubbles, cancelable) {
205 type = tameEventType(type, true);
206 bubbles = Boolean(bubbles);
207 cancelable = Boolean(cancelable);
208
209 if (event.initEvent) { // Non-IE
210 event.initEvent(type, bubbles, cancelable);
211 } else if (bubbles && cancelable) { // IE
212 event.eventType___ = type;
213 } else {
214 // TODO(mikesamuel): can bubbling and cancelable on events be simulated
215 // via http://msdn.microsoft.com/en-us/library/ms533545(VS.85).aspx
216 throw new Error(
217 'Browser does not support non-bubbling/uncanceleable events');
218 }
219 }
220
221 function dispatchEvent(element, event) {
222 // TODO(mikesamuel): when we change event dispatching to happen
223 // asynchronously, we should exempt custom events since those
224 // need to return a useful value, and there may be code bracketing
225 // them which could observe asynchronous dispatch.
226
227 // "The return value of dispatchEvent indicates whether any of
228 // the listeners which handled the event called
229 // preventDefault. If preventDefault was called the value is
230 // false, else the value is true."
231 if (element.dispatchEvent) {
232 return Boolean(element.dispatchEvent(event));
233 } else {
234 // Only dispatches custom events as when tameEventType(t) !== t.
235 element.fireEvent('ondataavailable', event);
236 return Boolean(event.returnValue);
237 }
238 }
239
240 /**
241 * Add an event listener function to an element.
242 *
243 * <p>Replaces
244 * W3C <code>Element::addEventListener</code> and
245 * IE <code>Element::attachEvent</code>.
246 *
247 * @param {HTMLElement} element a native DOM element.
248 * @param {string} type a string identifying the event type.
249 * @param {boolean Element::function (event)} handler an event handler.
250 * @param {boolean} useCapture whether the user wishes to initiate capture.
251 * @return {boolean Element::function (event)} the handler added. May be
252 * a wrapper around the input.
253 */
254 function addEventListener(element, type, handler, useCapture) {
255 type = String(type);
256 var tameType = tameEventType(type);
257 if (features.attachEvent) {
258 // TODO(ihab.awad): How do we emulate 'useCapture' here?
259 if (type !== tameType) {
260 var wrapper = eventHandlerTypeFilter(handler, tameType);
261 element.attachEvent('ondataavailable', wrapper);
262 return wrapper;
263 } else {
264 element.attachEvent('on' + type, handler);
265 return handler;
266 }
267 } else {
268 // FF2 fails if useCapture not passed or is not a boolean.
269 element.addEventListener(tameType, handler, useCapture);
270 return handler;
271 }
272 }
273
274 /**
275 * Remove an event listener function from an element.
276 *
277 * <p>Replaces
278 * W3C <code>Element::removeEventListener</code> and
279 * IE <code>Element::detachEvent</code>.
280 *
281 * @param element a native DOM element.
282 * @param type a string identifying the event type.
283 * @param handler a function acting as an event handler.
284 * @param useCapture whether the user wishes to initiate capture.
285 */
286 function removeEventListener(element, type, handler, useCapture) {
287 type = String(type);
288 var tameType = tameEventType(type);
289 if (features.attachEvent) {
290 // TODO(ihab.awad): How do we emulate 'useCapture' here?
291 if (tameType !== type) {
292 element.detachEvent('ondataavailable', handler);
293 } else {
294 element.detachEvent('on' + type, handler);
295 }
296 } else {
297 element.removeEventListener(tameType, handler, useCapture);
298 }
299 }
300
301 /**
302 * Clones a node per {@code Node.clone()}.
303 * <p>
304 * Returns a duplicate of this node, i.e., serves as a generic copy
305 * constructor for nodes. The duplicate node has no parent;
306 * (parentNode is null.).
307 * <p>
308 * Cloning an Element copies all attributes and their values,
309 * including those generated by the XML processor to represent
310 * defaulted attributes, but this method does not copy any text it
311 * contains unless it is a deep clone, since the text is contained
312 * in a child Text node. Cloning an Attribute directly, as opposed
313 * to be cloned as part of an Element cloning operation, returns a
314 * specified attribute (specified is true). Cloning any other type
315 * of node simply returns a copy of this node.
316 * <p>
317 * Note that cloning an immutable subtree results in a mutable copy,
318 * but the children of an EntityReference clone are readonly. In
319 * addition, clones of unspecified Attr nodes are specified. And,
320 * cloning Document, DocumentType, Entity, and Notation nodes is
321 * implementation dependent.
322 *
323 * @param {boolean} deep If true, recursively clone the subtree
324 * under the specified node; if false, clone only the node itself
325 * (and its attributes, if it is an Element).
326 *
327 * @return {Node} The duplicate node.
328 */
329 function cloneNode(node, deep) {
330 var clone;
331 if (!document.all) { // Not IE 6 or IE 7
332 clone = node.cloneNode(deep);
333 } else {
334 clone = constructClone(node, deep);
335 }
336 fixupClone(node, clone);
337 return clone;
338 }
339
340 /**
341 * Create a <code>style</code> element for a document containing some
342 * specified CSS text. Does not add the element to the document: the client
343 * may do this separately if desired.
344 *
345 * <p>Replaces directly creating the <code>style</code> element and
346 * populating its contents.
347 *
348 * @param document a DOM document.
349 * @param cssText a string containing a well-formed stylesheet production.
350 * @return a <code>style</code> element for the specified document.
351 */
352 function createStylesheet(document, cssText) {
353 // Courtesy Stoyan Stefanov who documents the derivation of this at
354 // http://www.phpied.com/dynamic-script-and-style-elements-in-ie/ and
355 // http://yuiblog.com/blog/2007/06/07/style/
356 var styleSheet = document.createElement('style');
357 styleSheet.setAttribute('type', 'text/css');
358 if (styleSheet.styleSheet) { // IE
359 styleSheet.styleSheet.cssText = cssText;
360 } else { // the world
361 styleSheet.appendChild(document.createTextNode(cssText));
362 }
363 return styleSheet;
364 }
365
366 /**
367 * Set an attribute on a DOM node.
368 *
369 * <p>Replaces DOM <code>Node::setAttribute</code>.
370 *
371 * @param {HTMLElement} element a DOM element.
372 * @param {string} name the name of an attribute.
373 * @param {string} value the value of an attribute.
374 */
375 function setAttribute(element, name, value) {
376 switch (name) {
377 case 'style':
378 if ((typeof element.style.cssText) === 'string') {
379 // Setting the 'style' attribute does not work for IE, but
380 // setting cssText works on IE 6, Firefox, and IE 7.
381 element.style.cssText = value;
382 return value;
383 }
384 break;
385 case 'class':
386 element.className = value;
387 return value;
388 case 'for':
389 element.htmlFor = value;
390 return value;
391 }
392 if (features.setAttributeExtraParam) {
393 element.setAttribute(name, value, 0);
394 } else {
395 element.setAttribute(name, value);
396 }
397 return value;
398 }
399
400 /**
401 * See <a href="http://www.w3.org/TR/cssom-view/#the-getclientrects"
402 * >ElementView.getBoundingClientRect()</a>.
403 * @return {Object} duck types as a TextRectangle with numeric fields
404 * {@code left}, {@code right}, {@code top}, and {@code bottom}.
405 */
406 function getBoundingClientRect(el) {
407 var doc = el.ownerDocument;
408 // Use the native method if present.
409 if (el.getBoundingClientRect) {
410 var cRect = el.getBoundingClientRect();
411 if (isIE) {
412 // IE has an unnecessary border, which can be mucked with by styles, so
413 // the amount of border is not predictable.
414 // Depending on whether the document is in quirks or standards mode,
415 // the border will be present on either the HTML or BODY elements.
416 var fixupLeft = doc.documentElement.clientLeft + doc.body.clientLeft;
417 cRect.left -= fixupLeft;
418 cRect.right -= fixupLeft;
419 var fixupTop = doc.documentElement.clientTop + doc.body.clientTop;
420 cRect.top -= fixupTop;
421 cRect.bottom -= fixupTop;
422 }
423 return ({
424 top: +cRect.top,
425 left: +cRect.left,
426 right: +cRect.right,
427 bottom: +cRect.bottom
428 });
429 }
430
431 // Otherwise, try using the deprecated gecko method, or emulate it in
432 // horribly inefficient ways.
433
434 // http://code.google.com/p/doctype/wiki/ArticleClientViewportElement
435 var viewport = (isIE && doc.compatMode === 'CSS1Compat')
436 ? doc.body : doc.documentElement;
437
438 // Figure out the position relative to the viewport.
439 // From http://code.google.com/p/doctype/wiki/ArticlePageOffset
440 var pageX = 0, pageY = 0;
441 if (el === viewport) {
442 // The viewport is the origin.
443 } else if (doc.getBoxObjectFor) { // Handles Firefox < 3
444 var elBoxObject = doc.getBoxObjectFor(el);
445 var viewPortBoxObject = doc.getBoxObjectFor(viewport);
446 pageX = elBoxObject.screenX - viewPortBoxObject.screenX;
447 pageY = elBoxObject.screenY - viewPortBoxObject.screenY;
448 } else {
449 // Walk the offsetParent chain adding up offsets.
450 for (var op = el; (op && op !== el); op = op.offsetParent) {
451 pageX += op.offsetLeft;
452 pageY += op.offsetTop;
453 if (op !== el) {
454 pageX += op.clientLeft || 0;
455 pageY += op.clientTop || 0;
456 }
457 if (isWebkit) {
458 // On webkit the offsets for position:fixed elements are off by the
459 // scroll offset.
460 var opPosition = doc.defaultView.getComputedStyle(op, 'position');
461 if (opPosition === 'fixed') {
462 pageX += doc.body.scrollLeft;
463 pageY += doc.body.scrollTop;
464 }
465 break;
466 }
467 }
468
469 // Opera & (safari absolute) incorrectly account for body offsetTop
470 if ((isWebkit
471 && doc.defaultView.getComputedStyle(el, 'position') === 'absolute')
472 || isOpera) {
473 pageY -= doc.body.offsetTop;
474 }
475
476 // Accumulate the scroll positions for everything but the body element
477 for (var op = el; (op = op.offsetParent) && op !== doc.body;) {
478 pageX -= op.scrollLeft;
479 // see https://bugs.opera.com/show_bug.cgi?id=249965
480 if (!isOpera || op.tagName !== 'TR') {
481 pageY -= op.scrollTop;
482 }
483 }
484 }
485
486 // Figure out the viewport container so we can subtract the window's
487 // scroll offsets.
488 var scrollEl = !isWebkit && doc.compatMode === 'CSS1Compat'
489 ? doc.documentElement
490 : doc.body;
491
492 var left = pageX - scrollEl.scrollLeft, top = pageY - scrollEl.scrollTop;
493 return ({
494 top: top,
495 left: left,
496 right: left + el.clientWidth,
497 bottom: top + el.clientHeight
498 });
499 }
500
501 /**
502 * Returns the value of the named attribute on element.
503 *
504 * @param {HTMLElement} element a DOM element.
505 * @param {string} name the name of an attribute.
506 */
507 function getAttribute(element, name) {
508 switch (name) {
509 case 'style':
510 if ((typeof element.style.cssText) === 'string') {
511 return element.style.cssText;
512 }
513 break;
514 case 'class':
515 return element.className;
516 case 'for':
517 return element.htmlFor;
518 }
519 return element.getAttribute(name);
520 }
521
522 function getAttributeNode(element, name) {
523 return element.getAttributeNode(name);
524 }
525
526 function hasAttribute(element, name) {
527 if (element.hasAttribute) { // Non IE
528 return element.hasAttribute(name);
529 } else {
530 var attr = getAttributeNode(element, name);
531 return attr !== null && attr.specified;
532 }
533 }
534
535 return {
536 addEventListener: addEventListener,
537 removeEventListener: removeEventListener,
538 initEvent: initEvent,
539 dispatchEvent: dispatchEvent,
540 cloneNode: cloneNode,
541 createStylesheet: createStylesheet,
542 setAttribute: setAttribute,
543 getAttribute: getAttribute,
544 getAttributeNode: getAttributeNode,
545 hasAttribute: hasAttribute,
546 getBoundingClientRect: getBoundingClientRect,
547 untameEventType: untameEventType,
548 extendedCreateElementFeature: features.extendedCreateElement
549 };
550 })();