Mercurial > caja-test
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 * "<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 })(); |