diff --git a/CHANGELOG b/CHANGELOG index f5df87f..2d4e595 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +* Make `Event.stopObserving` return element in all cases. [#810 state:resolved] (Yaffle, Tobie Langel) + +* String#startsWith, String#endsWith performance optimization (Yaffle, Tobie Langel, kangax) + +* Rewrite String#camelize using String#replace with a replacement function (Phred, John-David Dalton, Samuel Lebeau, kangax) + *1.6.1* (August 24, 2009) * Avoid triggering a warning when Java is disabled in IE8. [#668 state:resolved] (orv, kangax, Andrew Dupont, Tobie Langel) diff --git a/src/dom/dom.js b/src/dom/dom.js index 3fae168..0f0e247 100644 --- a/src/dom/dom.js +++ b/src/dom/dom.js @@ -60,15 +60,55 @@ if (!Node.ELEMENT_NODE) { /** section: DOM * class Element + * + * The `Element` object provides a variety of powerful DOM methods for + * interacting with DOM elements — creating them, updating them, + * traversing them, etc. You can access these either as methods of `Element` + * itself, passing in the element to work with as the first argument, or as + * methods on extended element *instances*: + * + * // Using Element: + * Element.addClassName('target', 'highlighted'); + * + * // Using an extended element instance: + * $('target').addClassName('highlighted'); + * + * `Element` is also a constructor for building element instances from scratch, + * see [`new Element`](#new-constructor) for details. + * + * Most `Element` methods return the element instance, so that you can chain + * them easily: + * + * $('message').addClassName('read').update('I read this message!'); + * + * ##### More Information + * + * For more information about extended elements, check out ["How Prototype + * extends the DOM"](http://prototypejs.org/learn/extensions), which will walk + * you through the inner workings of Prototype's DOM extension mechanism. **/ /** * new Element(tagName[, attributes]) - * - tagName (String): The name of the HTML element to create. - * - attributes (Object): A list of attribute/value pairs to set on the - * element. + * - tagName (String): The name of the HTML element to create. + * - attributes (Object): An optional group of attribute/value pairs to set on + * the element. * - * Creates an HTML element with `tagName` as the tag name. + * Creates an HTML element with `tagName` as the tag name, optionally with the + * given attributes. This can be markedly more concise than working directly + * with the DOM methods, and takes advantage of Prototype's workarounds for + * various browser issues with certain attributes: + * + * ##### Example + * + * // The old way: + * var a = document.createElement('a'); + * a.setAttribute('class', 'foo'); + * a.setAttribute('href', '/foo.html'); + * a.appendChild(document.createTextNode("Next page")); + * + * // The new way: + * var a = new Element('a', {'class': 'foo', href: '/foo.html'}).update("Next page"); **/ (function(global) { @@ -430,8 +470,34 @@ Element.Methods = { /** * Element.ancestors(@element) -> [Element...] * - * Collects all of `element`'s ancestors and returns them as an array of - * elements. + * Collects all of `element`'s ancestor elements and returns them as an + * array of extended elements. + * + * The returned array's first element is `element`'s direct ancestor (its + * `parentNode`), the second one is its grandparent, and so on until the + * `html` element is reached. `html` will always be the last member of the + * array. Calling `ancestors` on the `html` element will return an empty + * array. + * + * ##### Example + * + * Assuming: + * + * language: html + * + * [...] + * + *
+ *
+ *
+ *
+ * + * + * + * Then: + * + * $('kid').ancestors(); + * // -> [div#father, body, html] **/ ancestors: function(element) { return Element.recursivelyCollect(element, 'parentNode'); @@ -440,8 +506,10 @@ Element.Methods = { /** * Element.descendants(@element) -> [Element...] * - * Collects all of element's descendants and returns them as an array of - * elements. + * Collects all of the element's descendants (its children, their children, + * etc.) and returns them as an array of extended elements. As with all of + * Prototype's DOM traversal methods, only Elements are returned, other + * nodes (text nodes, etc.) are skipped. **/ descendants: function(element) { return Element.select(element, "*"); @@ -461,11 +529,10 @@ Element.Methods = { return $(element); }, - /** + /** deprecated, alias of: Element.childElements * Element.immediateDescendants(@element) -> [Element...] * - * Collects all of `element`'s immediate descendants (i.e., children) and - * returns them as an array of elements. + * **This method is deprecated, please see [[Element.childElements]]**. **/ immediateDescendants: function(element) { if (!(element = $(element).firstChild)) return []; @@ -606,7 +673,30 @@ Element.Methods = { * - selector (String): A CSS selector. * * Finds all siblings of the current element that match the given - * selector(s). + * selector(s). If you provide multiple selectors, siblings matching *any* + * of the selectors are included. If a sibling matches multiple selectors, + * it is only included once. The order of the returned array is not defined. + * + * ##### Example + * + * Assuming this list: + * + * language: html + * + * + * Then: + * + * $('nyc').adjacent('li.us'); + * // -> [li#chi, li#la, li#aus] + * $('nyc').adjacent('li.uk', 'li.jp'); + * // -> [li#lon, li#tok] **/ adjacent: function(element) { var args = Array.prototype.slice.call(arguments, 1); @@ -692,11 +782,15 @@ Element.Methods = { return Element.getDimensions(element).width; }, - /** + /** deprecated * Element.classNames(@element) -> [String...] * * Returns a new instance of [[Element.ClassNames]], an [[Enumerable]] * object used to read and write CSS class names of `element`. + * + * **Deprecated**, please see [[Element.addClassName]], + * [[Element.removeClassName]], and [[Element.hasClassName]]. If you want + * an array of classnames, you can use `$w(element.className)`. **/ classNames: function(element) { return new Element.ClassNames(element); @@ -716,8 +810,24 @@ Element.Methods = { /** * Element.addClassName(@element, className) -> Element + * - className (String): The class name to add. * - * Adds a CSS class to `element`. + * Adds the given CSS class to `element`. + * + * ##### Example + * + * Assuming this HTML: + * + * language: html + *
+ * + * Then: + * + * $('mutsu').className; + * // -> 'apple fruit' + * $('mutsu').addClassName('food'); + * $('mutsu').className; + * // -> 'apple fruit food' **/ addClassName: function(element, className) { if (!(element = $(element))) return; @@ -752,7 +862,44 @@ Element.Methods = { /** * Element.cleanWhitespace(@element) -> Element * - * Removes whitespace-only text node children from `element`. + * Removes all of `element`'s child text nodes that contain *only* + * whitespace. Returns `element`. + * + * This can be very useful when using standard properties like `nextSibling`, + * `previousSibling`, `firstChild` or `lastChild` to walk the DOM. Usually + * you'd only do that if you are interested in all of the DOM nodes, not + * just Elements (since if you just need to traverse the Elements in the + * DOM tree, you can use [[Element.up]], [[Element.down]], + * [[Element.next]], and [[Element.previous]] instead). + * + * #### Example + * + * Consider the following HTML snippet: + * + * language: html + * + * + * Let's grab what we think is the first list item using the raw DOM + * method: + * + * var element = $('apples'); + * element.firstChild.innerHTML; + * // -> undefined + * + * It's undefined because the `firstChild` of the `apples` element is a + * text node containing the whitespace after the end of the `ul` and before + * the first `li`. + * + * If we remove the useless whitespace, then `firstChild` works as expected: + * + * var element = $('apples'); + * element.cleanWhitespace(); + * element.firstChild.innerHTML; + * // -> 'Mutsu' **/ cleanWhitespace: function(element) { element = $(element); @@ -777,8 +924,28 @@ Element.Methods = { /** * Element.descendantOf(@element, ancestor) -> Boolean + * - ancestor (Element | String): The element to check against (or its ID). * * Checks if `element` is a descendant of `ancestor`. + * + * ##### Example + * + * Assuming: + * + * language: html + *
+ *
+ *
+ *
+ *
+ * + * Then: + * + * $('homo-sapiens').descendantOf('australopithecus'); + * // -> true + * + * $('homo-erectus').descendantOf('homo-sapiens'); + * // -> false **/ descendantOf: function(element, ancestor) { element = $(element), ancestor = $(ancestor); @@ -979,10 +1146,24 @@ Element.Methods = { * Element.cumulativeOffset(@element) -> Array * * Returns the offsets of `element` from the top left corner of the - * document. + * document, in pixels. * * Returns an array in the form of `[leftValue, topValue]`. Also accessible * as properties: `{ left: leftValue, top: topValue }`. + * + * ##### Example + * + * Assuming the div `foo` is at (25,40), then: + * + * var offset = $('foo').cumulativeOffset(); + * offset[0]; + * // -> 25 + * offset[1]; + * // -> 40 + * offset.left; + * // -> 25 + * offset.top; + * // -> 40 **/ cumulativeOffset: function(element) { var valueT = 0, valueL = 0; @@ -1073,11 +1254,25 @@ Element.Methods = { /** * Element.cumulativeScrollOffset(@element) -> Array * - * Calculates the cumulative scroll offset of an element in nested - * scrolling containers. + * Calculates the cumulative scroll offset (in pixels) of an element in + * nested scrolling containers. * * Returns an array in the form of `[leftValue, topValue]`. Also accessible * as properties: `{ left: leftValue, top: topValue }`. + * + * ##### Example + * + * Assuming the div `foo` is at scroll offset (0,257), then: + * + * var offset = $('foo').cumulativeOffset(); + * offset[0]; + * // -> 0 + * offset[1]; + * // -> 257 + * offset.left; + * // -> 0 + * offset.top; + * // -> 257 **/ cumulativeScrollOffset: function(element) { var valueT = 0, valueL = 0; @@ -1141,15 +1336,60 @@ Element.Methods = { /** * Element.clonePosition(@element, source[, options]) -> Element + * - source (Element | String): The source element (or its ID). + * - options (Object): The position fields to clone. * - * Clones the position and/or dimensions of `source` onto `element` as - * defined by `options`. + * Clones the position and/or dimensions of `source` onto the element as + * defined by `options`, with an optional offset for the `left` and `top` + * properties. * - * Valid keys for `options` are: `setLeft`, `setTop`, `setWidth`, and - * `setHeight` (all booleans which default to `true`); and `offsetTop` - * and `offsetLeft` (numbers which default to `0`). Use these to control - * which aspects of `source`'s layout are cloned and how much to offset - * the resulting position of `element`. + * Note that the element will be positioned exactly like `source` whether or + * not it is part of the same [CSS containing + * block](http://www.w3.org/TR/CSS21/visudet.html#containing-block-details). + * + * ##### Options + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
NameDefaultDescription
setLefttrueClones source's left CSS property onto element.
setToptrueClones source's top CSS property onto element.
setWidthtrueClones source's width onto element.
setHeighttrueClones source's width onto element.
offsetLeft0Number by which to offset element's left CSS property.
offsetTop0Number by which to offset element's top CSS property.
**/ clonePosition: function(element, source) { var options = Object.extend({ @@ -1197,8 +1437,40 @@ Object.extend(Element.Methods, { **/ getElementsBySelector: Element.Methods.select, - /** alias of: Element.immediateDescendants + /** * Element.childElements(@element) -> [Element...] + * + * Collects all of the element's children and returns them as an array of + * [extended](http://prototypejs.org/api/element/extend) elements, in + * document order. The first entry in the array is the topmost child of + * `element`, the next is the child after that, etc. + * + * Like all of Prototype's DOM traversal methods, `childElements` ignores + * text nodes and returns element nodes only. + * + * ##### Example + * + * Assuming: + * + * language: html + *
+ * Some text in a text node + *
+ *
+ *
+ *
+ *
+ * + * Then: + * + * $('australopithecus').childElements(); + * // -> [div#homo-erectus] + * + * $('homo-erectus').childElements(); + * // -> [div#homo-neanderthalensis, div#homo-sapiens] + * + * $('homo-sapiens').childElements(); + * // -> [] **/ childElements: Element.Methods.immediateDescendants }); @@ -1658,16 +1930,31 @@ Object.extend(Element, Element.Methods); div = null; -})(document.createElement('div')) +})(document.createElement('div')); /** * Element.extend(element) -> Element * - * Extends `element` with all of the methods contained in `Element.Methods` - * and `Element.Methods.Simulated`. - * If `element` is an `input`, `textarea`, or `select` tag, it will also be - * extended with the methods from `Form.Element.Methods`. If it is a `form` - * tag, it will also be extended with the methods from `Form.Methods`. + * Extends the given element instance with all of the Prototype goodness and + * syntactic sugar, as well as any extensions added via [[Element.addMethods]]. + * (If the element instance was already extended, this is a no-op.) + * + * You only need to use `Element.extend` on element instances you've acquired + * directly from the DOM; **all** Prototype methods that return element + * instances (such as [[$]], [[Element.down]], etc.) will pre-extend the + * element before returning it. + * + * Check out ["How Prototype extends the + * DOM"](http://prototypejs.org/learn/extensions) for more about element + * extensions. + * + * ##### Details + * + * Specifically, `Element.extend` extends the given instance with the methods + * contained in `Element.Methods` and `Element.Methods.Simulated`. If `element` + * is an `input`, `textarea`, or `select` element, it will also be extended + * with the methods from `Form.Element.Methods`. If it is a `form` element, it + * will also be extended with the methods from `Form.Methods`. **/ Element.extend = (function() { @@ -1758,12 +2045,126 @@ Element.hasAttribute = function(element, attribute) { /** * Element.addMethods(methods) -> undefined * Element.addMethods(tagName, methods) -> undefined + * - tagName (String): (Optional) The name of the HTML tag for which the + * methods should be available; if not given, all HTML elements will have + * the new methods. + * - methods (Object): A hash of methods to add. * - * Takes a hash of methods and makes them available as methods of extended - * elements and of the `Element` object. + * `Element.addMethods` makes it possible to mix your *own* methods into the + * `Element` object and extended element instances (all of them, or only ones + * with the given HTML tag if you specify `tagName`). * - * The second usage form is for adding methods only to specific tag names. + * You define the methods in a hash that you provide to `Element.addMethods`. + * Here's an example adding two methods: * + * Element.addMethods({ + * + * // myOwnMethod: Do something cool with the element + * myOwnMethod: function(element) { + * if (!(element = $(element))) return; + * // ...do smething with 'element'... + * return element; + * }, + * + * // wrap: Wrap the element in a new element using the given tag + * wrap: function(element, tagName) { + * var wrapper; + * if (!(element = $(element))) return; + * wrapper = new Element(tagName); + * element.parentNode.replaceChild(wrapper, element); + * wrapper.appendChild(element); + * return wrapper; + * } + * + * }); + * + * Once added, those can be used either via `Element`: + * + * // Wrap the element with the ID 'foo' in a div + * Element.wrap('foo', 'div'); + * + * ...or as instance methods of extended elements: + * + * // Wrap the element with the ID 'foo' in a div + * $('foo').wrap('div'); + * + * Note the following requirements and conventions for methods added to + * `Element`: + * + * - The first argument is *always* an element or ID, by convention this + * argument is called `element`. + * - The method passes the `element` argument through [[$]] and typically + * returns if the result is undefined. + * - Barring a good reason to return something else, the method returns the + * extended element to enable chaining. + * + * Our `myOwnMethod` method above returns the element because it doesn't have + * a good reason to return anything else. Our `wrap` method returns the + * wrapper, because that makes more sense for that method. + * + * ##### Extending only specific elements + * + * If you call `Element.addMethods` with *two* arguments, it will apply the + * methods only to elements with the given HTML tag: + * + * Element.addMethods('DIV', my_div_methods); + * // the given methods are now available on DIV elements, but not others + * + * You can also pass an *array* of tag names as the first argument: + * + * Element.addMethods(['DIV', 'SPAN'], my_additional_methods); + * // DIV and SPAN now both have the given methods + * + * (Tag names in the first argument are not case sensitive.) + * + * Note: `Element.addMethods` has built-in security which prevents you from + * overriding native element methods or properties (like `getAttribute` or + * `innerHTML`), but nothing prevents you from overriding one of Prototype's + * methods. Prototype uses a lot of its methods internally; overriding its + * methods is best avoided or at least done only with great care. + * + * ##### Example 1 + * + * Our `wrap` method earlier was a complete example. For instance, given this + * paragraph: + * + * language: html + *

Some content...

+ * + * ...we might wrap it in a `div`: + * + * $('first').wrap('div'); + * + * ...or perhaps wrap it and apply some style to the `div` as well: + * + * $('first').wrap('div').setStyle({ + * backgroundImage: 'url(images/rounded-corner-top-left.png) top left' + * }); + * + * ##### Example 2 + * + * We can add a method to elements that makes it a bit easier to update them + * via [[Ajax.Updater]]: + * + * Element.addMethods({ + * ajaxUpdate: function(element, url, options) { + * if (!(element = $(element))) return; + * element.update('Loading...'); + * options = options || {}; + * options.onFailure = options.onFailure || defaultFailureHandler.curry(element); + * new Ajax.Updater(element, url, options); + * return element; + * } + * }); + * + * Now we can update an element via an Ajax call much more concisely than + * before: + * + * $('foo').ajaxUpdate('/new/content'); + * + * That will use Ajax.Updater to load new content into the 'foo' element, + * showing a spinner while the call is in progress. It even applies a default + * failure handler (since we didn't supply one). **/ Element.addMethods = function(methods) { var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; diff --git a/src/dom/event.js b/src/dom/event.js index 7980c52..0d86e11 100644 --- a/src/dom/event.js +++ b/src/dom/event.js @@ -618,43 +618,33 @@ element = $(element); var registry = Element.retrieve(element, 'prototype_event_registry'); + if (!registry) return element; - if (Object.isUndefined(registry)) return element; - - if (eventName && !handler) { - // If an event name is passed without a handler, we stop observing all - // handlers of that type. - var responders = registry.get(eventName); - - if (Object.isUndefined(responders)) return element; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); - return element; - } else if (!eventName) { - // If both the event name and the handler are omitted, we stop observing - // _all_ handlers on the element. + if (!eventName) { + // We stop observing all events. + // e.g.: $(element).stopObserving(); registry.each( function(pair) { - var eventName = pair.key, responders = pair.value; - - responders.each( function(r) { - Element.stopObserving(element, eventName, r.handler); - }); + var eventName = pair.key; + stopObserving(element, eventName); }); return element; } var responders = registry.get(eventName); + if (!responders) return element; - // Fail gracefully if there are no responders assigned. - if (!responders) return; + if (!handler) { + // We stop observing all handlers for the given eventName. + // e.g.: $(element).stopObserving('click'); + responders.each(function(r) { + stopObserving(element, eventName, r.handler); + }); + return element; + } var responder = responders.find( function(r) { return r.handler === handler; }); if (!responder) return element; - var actualEventName = _getDOMEventName(eventName); - if (eventName.include(':')) { // Custom event. if (element.removeEventListener) @@ -665,6 +655,7 @@ } } else { // Ordinary event. + var actualEventName = _getDOMEventName(eventName); if (element.removeEventListener) element.removeEventListener(actualEventName, responder, false); else diff --git a/src/dom/selector.js b/src/dom/selector.js index 947fd89..7692a62 100644 --- a/src/dom/selector.js +++ b/src/dom/selector.js @@ -309,7 +309,7 @@ Object.extend(Selector, { while (e && le != e && (/\S/).test(e)) { le = e; for (var i = 0; i 'MozBinding' **/ function camelize() { - var parts = this.split('-'), len = parts.length; - if (len == 1) return parts[0]; - - var camelized = this.charAt(0) == '-' - ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) - : parts[0]; - - for (var i = 1; i < len; i++) - camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); - - return camelized; + return this.replace(/-+(.)?/g, function(match, chr) { + return chr ? chr.toUpperCase() : ''; + }); } /** @@ -437,7 +429,9 @@ Object.extend(String.prototype, (function() { * Checks if the string starts with `substring`. **/ function startsWith(pattern) { - return this.indexOf(pattern) === 0; + // We use `lastIndexOf` instead of `indexOf` to avoid tying execution + // time to string length when string doesn't start with pattern. + return this.lastIndexOf(pattern, 0) === 0; } /** @@ -447,7 +441,9 @@ Object.extend(String.prototype, (function() { **/ function endsWith(pattern) { var d = this.length - pattern.length; - return d >= 0 && this.lastIndexOf(pattern) === d; + // We use `indexOf` instead of `lastIndexOf` to avoid tying execution + // time to string length when string doesn't end with pattern. + return d >= 0 && this.indexOf(pattern, d) === d; } /** diff --git a/test/unit/event_test.js b/test/unit/event_test.js index fc6df25..3e3416c 100644 --- a/test/unit/event_test.js +++ b/test/unit/event_test.js @@ -184,6 +184,8 @@ new Test.Unit.Runner({ span.observe("test:somethingHappened", observer); this.assertEqual(span, span.stopObserving("test:somethingHappened")); + this.assertEqual(span, span.stopObserving("test:somethingOtherHappened", observer)); + span.observe("test:somethingHappened", observer); this.assertEqual(span, span.stopObserving()); this.assertEqual(span, span.stopObserving()); // assert it again, after there are no observers