// ========================================================================== // Project: SproutCore - JavaScript Application Framework // Copyright: ©2006-2009 Sprout Systems, Inc. and contributors. // Portions ©2008-2009 Apple Inc. All rights reserved. // License: Licened under MIT license (see license.js) // ========================================================================== require('system/builder') ; /** CoreQuery is a simplified DOM manipulation library used internally by SproutCore to find and edit DOM elements. Outside of SproutCore, you should generally use a more full-featured DOM library such as Prototype or jQuery. CoreQuery itself is a subset of jQuery with some additional plugins. If you have jQuery already loaded when SproutCore loads, in fact, it will replace CoreQuery with the full jQuery library and install any CoreQuery plugins, including support for the SC.Enumerable mixin. Much of this code is adapted from jQuery 1.2.6, which is available under an MIT license just like SproutCore. h1. Using CoreQuery You can work with CoreQuery much like you would work with jQuery. The core manipulation object is exposed as SC.$. To find some elements on the page you just pass in a selector like this: {{{ var cq = SC.$('p'); }}} The object returned from this call is a CoreQuery object that implements SC.Enumerable as well as a number of other useful manipulation methods. Often times we call this object the "matched set", because it usually an array of elements matching the selector key you passed. To work with the matched set, just call the various helper methods on it. Here are some of the more useful ones: {{{ // change all of the text red cq.css('color','red'); // hide/show the set cq.hide(); cq.show(); // set the text content of the set cq.text("Hello World!"); }}} Of course, you can also chain these methods, just like jQuery. Here is how you might find all of the headings in your page, change their text and color: {{{ SC.$('h1').text('Hello World!').css('color','red'); }}} h1. Using CoreQuery with Views Usually you will not want to just blindly edit the HTML content in your application. Instead, you will use CoreQuery to update the portion of the page managed by your SC.View instances. Every SC.View instance has a $() property just like SC.$(). The difference is that this function will start searching from the root of the view. For example, you could use the following code in your updateDisplay method to set your content and color: {{{ updateDisplay: function() { this.$().text(this.get('value')).css('color','red'); } }}} You could also work on content within your view, for example this will change the title on your view held in the span.title element: {{{ updateDisplay: function() { this.$('span.title').text('Hello World'); this.$().setClassName('sc-enabled', YES) ; } }}} @class @extends SC.Builder.fn */ SC.CoreQuery = (function() { // Define CoreQuery inside of its own scope to support some jQuery idioms. // A simple way to check for HTML strings or ID strings // (both of which we optimize for) var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/, // Is it a simple selector isSimple = /^.[^:#\[\.]*$/, undefined ; var styleFloat = SC.browser.msie ? "styleFloat" : "cssFloat"; // used for the find() method. var chars = (SC.browser.safari && parseInt(SC.browser.version,0) < 417) ? "(?:[\\w*_-]|\\\\.)" : "(?:[\\w\u0128-\uFFFF*_-]|\\\\.)" ; var quickID = new RegExp("^(" + chars + "+)(#)(" + chars + "+)") ; var singleClass = new RegExp("^([#.]?)(" + chars + "*)"); var quickSplit = new RegExp("([#.]?)(" + chars + "*)",'g'); // Constants used in CQ.css() var LEFT_RIGHT = ["Left", "Right"]; var TOP_BOTTOM = ["Top", "Bottom"]; var CSS_DISPLAY_PROPS = { position: "absolute", visibility: "hidden", display:"block" } ; var getWH = function getWH(elem, name, which) { var val = name == "width" ? elem.offsetWidth : elem.offsetHeight; var padding = 0, border = 0, loc=which.length, dim; while(--loc>=0) { dim = which[loc]; padding += parseFloat(CQ.curCSS( elem, "padding" + dim, true)) || 0; border += parseFloat(CQ.curCSS( elem, "border" + dim + "Width", true)) ||0; } val -= Math.round(padding + border); return val; } ; var expando = SC.guidKey, uuid = 0, windowData = {}, // exclude the following css properties to add px exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, // cache defaultView defaultView = document.defaultView || {}; // A helper method for determining if an element's values are broken var styleIsBorked = function styleIsBorked( elem ) { if ( !SC.browser.safari ) return false; // defaultView is cached var ret = defaultView.getComputedStyle( elem, null ); return !ret || ret.getPropertyValue("color") == ""; } ; // Helper function used by the dimensions and offset modules function num(elem, prop) { return elem[0] && parseInt( CQ.curCSS(elem[0], prop, true), 10 ) || 0; } ; var CoreQuery, CQ ; // implement core methods here from jQuery that we want available all the // time. Use this area to implement jQuery-compatible methods ONLY. // New methods should be added at the bottom of the file, where they will // be installed as plugins on CoreQuery or jQuery. CQ = CoreQuery = SC.Builder.create( /** @scope SC.CoreQuery.fn */ { /** Indicates that this is a jQuery-like object. */ jquery: 'SC.CoreQuery', /** Called on a new CoreQuery instance when it is first created. You can pass a variety of options to the CoreQuery constructor function including: - a simple selector: this will find the element and return it - element or array of elements - this will return a query with them - html-string: this will convert to DOM. @returns {CoreQuery} CoreQuery instance */ init: function( selector, context ) { // Make sure that a selection was provided selector = selector || document; // Handle $(DOMElement) if ( selector.nodeType ) { this[0] = selector; this.length = 1; return this ; // Handle HTML strings } else if ( typeof selector == "string" ) { // Are we dealing with HTML string or an ID? var match = quickExpr.exec( selector ); // Verify a match, and that no context was specified for #id if ( match && (match[1] || !context) ) { // HANDLE: $(html) -> $(array) if ( match[1] ) selector = CQ.clean( [ match[1] ], context ); // HANDLE: $("#id") else { var elem = document.getElementById( match[3] ); // Make sure an element was located if ( elem ){ // Handle the case where IE and Opera return items // by name instead of ID if ( elem.id != match[3] ) return CQ().find( selector ); // Otherwise, we inject the element directly into the jQuery object return CQ( elem ); } selector = []; } // HANDLE: $(expr, [context]) // (which is just equivalent to: $(content).find(expr) } else return CQ( context ).find( selector ); // HANDLE: $(function) // Shortcut for document ready } else if (SC.typeOf(selector) === SC.T_FUNCTION) { return SC.ready(selector); } return this.setArray(CQ.makeArray(selector)); }, /** Return the number of elements in the matched set. */ size: function() { return this.length; }, /** Return the nth element of the working array OR return a clean array with the result set, if no number is passed. @param {Number} num (Optional) @returns {Object|Array} */ get: function( num ) { return num == undefined ? CQ.makeArray(this) : this[num]; }, /** Find subelements matching the passed selector. Note that CoreQuery supports only a very simplified selector search method. See CoreQuery.find() for more information. @param {String} selector @returns {CoreQuery} new instance with match */ find: function( selector ) { var elems = CQ.map(this, function(elem){ return CQ.find( selector, elem ); }); return this.pushStack(elems); }, /** Filters the matching set to include only those matching the passed selector. Note that CoreQuery supports only a simplified selector search method. See CoreQuery.find() for more information. Also note that CoreQuery implements SC.Enumerable, which means you can also call this method with a callback and target and the callback will be executed on every element in the matching set to return a result. @param {String} selector @returns {CoreQuery} */ filter: function( selector ) { return this.pushStack( (SC.typeOf(selector) === SC.T_FUNCTION) && CQ.grep(this, function(elem, i){ return selector.call( elem, i ); }) || CQ.multiFilter( selector, this ) ); }, /** Returns the results not matching the passed selector. This is the opposite of filter. @param {String} selector @returns {CoreQuery} */ not: function( selector ) { if ( selector.constructor == String ) { // test special case where just one selector is passed in if ( isSimple.test( selector ) ) return this.pushStack( CQ.multiFilter( selector, this, true ) ); else selector = CQ.multiFilter( selector, this ); } var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; return this.filter(function() { return isArrayLike ? CQ.inArray( this, selector ) < 0 : this != selector; }); }, /** Force the current matched set of elements to become the specified array of elements (destroying the stack in the process) You should use pushStack() in order to do this, but maintain the stack. This method is mostly used internally. You will not need to use it yourself very often. @param {Array} elems @returns {CoreQuery} receiver */ setArray: function( elems ) { // Resetting the length to 0, then using the native Array push // is a super-fast way to populate an object with array-like properties this.length = 0; Array.prototype.push.apply( this, elems ); return this; }, /** Executes the passed function on every element in the CoreQuery object. Returns an array with the return values. Note that null values will be omitted from the resulting set. This differs from SC.Enumerable and the JavaScript standard. The callback must have the signature: {{{ function(currentElement, currentIndex) { return mappedValue; } }}} Note that "this" on the function will also be the currentElement. @param {Function} callback @returns {CoreQuery} results */ map: function( callback ) { return this.pushStack( CQ.map(this, function(elem, i){ return callback.call( elem, i, elem ); })); }, /** Execute a callback for every element in the matched set. (You can seed the arguments with an array of args, but this is only used internally.) @param {Function} callback @param {Object} args @returns {CoreQuery} receiver */ each: function( callback, args ) { return CQ.each( this, callback, args ); }, /** Determine the position of an element within a matched set of elements. jQuery-compatible name for indexOf(). @param {Element} elem @returns {Number} location */ index: function( elem ) { if (elem && elem.jquery) elem = elem[0]; return Array.prototype.indexOf.call(this, elem); }, /** Returns a new CoreQuery object that contains just the matching item. @param {Number} i @returns {CoreQuery} */ eq: function( i ) { return this.slice( i, +i + 1 ); }, /** Slice the CoreQuery result set just like you might slice and array. Returns a new CoreQuery object with the result set. @returns {CoreQuery} */ slice: function() { return this.pushStack( Array.prototype.slice.apply( this, arguments ) ); }, /** Adds the relevant elements to the existing matching set. */ add: function( selector ) { return this.pushStack( CQ.merge( this.get(), typeof selector == 'string' ? CQ( selector ) : CQ.makeArray( selector ) ).uniq()) ; }, /** Get to set the named attribute value on the element. You can either pass in the name of an attribute you would like to read from the first matched element, a single attribute/value pair to set on all elements or a hash of attribute/value pairs to set on all elements. @param {String} name attribute name @param {Object} value attribute value @param {String} type ? @returns {CoreQuery} receiver */ attr: function( name, value, type ) { var options = name; // Look for the case where we're accessing a style value if ( name.constructor == String ) if ( value === undefined ) return this[0] && CQ[ type || "attr" ]( this[0], name ); else { options = {}; options[ name ] = value; } // Check to see if we're setting style values return this.each(function(i){ // Set all the styles for ( name in options ) CQ.attr( (type)?this.style:this, name, CQ.prop( this, options[ name ], type, i, name )); }); }, html: function( value ) { return value === undefined ? (this[0] ? this[0].innerHTML.replace(/ CQ\d+="(?:\d+|null)"/g, "") : null) : this.empty().append( value ); // return value == undefined ? // (this[0] ? this[0].innerHTML : null) : // this.empty().append( value ); }, andSelf: function() { return this.add( this.prevObject ); }, /** Returns YES if every element in the matching set matches the passed selector. Remember that only simple selectors are supported in CoreQuery. @param {String} selector @return {Boolean} */ is: function( selector ) { return !!selector && CQ.multiFilter( selector, this ).length > 0; }, /** Returns YES if every element in the matching set has the named CSS class. @param {String} className @returns {Boolean} */ hasClass: function( className ) { return Array.prototype.every.call(this, function(elem) { return (elem.nodeType!=1) || CQ.className.has(elem, className) ; }); }, /** Provides a standardized, cross-browser method to get and set the value attribute of a form element. Optionally pass a value to set or no value to get. @param {Object} value @return {Object|CoreQuery} */ val: function( value ) { // get the value if ( value == undefined ) { var elem = this[0]; if (elem) { if(CQ.nodeName(elem, 'option')) return (elem.attributes.value || {}).specified ? elem.value : elem.text; // We need to handle select boxes special if ( CQ.nodeName( elem, "select" ) ) { var index = elem.selectedIndex, values = [], options = elem.options, one = elem.type == "select-one"; // Nothing was selected if ( index < 0 ) return null; // Loop through all the selected options var i, max = one ? index+1:options.length; for (i = one ? index : 0; i < max; i++ ) { var option = options[ i ]; if ( option.selected ) { value = CQ(option).val(); // get value if (one) return value; // We don't need an array for one values.push( value ); // Multi-Selects return an array } } return values; } // Everything else, we just grab the value return (elem.value || "").replace(/\r/g, ""); } return undefined; // otherwise set the value } else { if( value.constructor == Number ) value += ''; // force to string this.each(function(){ if ( this.nodeType != 1 ) return; // handle radio/checkbox. set the checked value if (SC.typeOf(value) === SC.T_ARRAY && (/radio|checkbox/).test(this.type)) { this.checked = (CQ.inArray(this.value, value) >= 0 || CQ.inArray(this.name, value) >= 0); // handle selects } else if ( CQ.nodeName( this, "select" ) ) { var values = CQ.makeArray(value); CQ( "option", this ).each(function(){ this.selected = (CQ.inArray( this.value, values ) >= 0 || CQ.inArray( this.text, values ) >= 0); }); if (!values.length) this.selectedIndex = -1; // otherwise, just set the value property } else this.value = value; }); } return this ; }, /** Returns a clone of the matching set of elements. Note that this will NOT clone event handlers like the jQuery version does becaue CoreQuery does not deal with events. */ clone: function() { // Do the clone var ret = this.map(function(){ if ( SC.browser.msie && !CQ.isXMLDoc(this) ) { // IE copies events bound via attachEvent when // using cloneNode. Calling detachEvent on the // clone will also remove the events from the orignal // In order to get around this, we use innerHTML. // Unfortunately, this means some modifications to // attributes in IE that are actually only stored // as properties will not be copied (such as the // the name attribute on an input). var clone = this.cloneNode(true), container = document.createElement("div"); container.appendChild(clone); return CQ.clean([container.innerHTML])[0]; } else return this.cloneNode(true); }); // Need to set the expando to null on the cloned set if it exists // removeData doesn't work here, IE removes it from the original as well // this is primarily for IE but the data expando shouldn't be copied // over in any browser var clone = ret.find("*").andSelf().each(function(){ if ( this[ SC.guidKey ] != undefined ) this[ SC.guidKey ] = null; }); // Return the cloned set return ret; }, /** Set or retrieve the specified CSS value. Pass only a key to get the current value, pass a key and value to change it. @param {String} key @param {Object} value @returns {Object|CoreQuery} */ css: function( key, value ) { // ignore negative width and height values if ((key == 'width' || key == 'height') && parseFloat(value,0) < 0 ) { value = undefined; } return this.attr( key, value, "curCSS" ); }, /** Set or retrieve the text content of an element. Pass a text element to update or set to end it. @param {String} text @returns {String|CoreQuery} */ text: function( text ) { if ( typeof text != "object" && text != null ) return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); var ret = ""; CQ.each( text || this, function(){ CQ.each( this.childNodes, function(){ if ( this.nodeType != 8 ) ret += this.nodeType != 1 ? this.nodeValue : CQ.fn.text( [ this ] ); }); }); return ret; }, /** Simple method to show elements without animation. */ show: function() { var isVisible = SC.$.isVisible; this.each(function() { if (!isVisible(this)) { // try to restore to natural layout as defined by CSS this.style.display = this.oldblock || ''; // handle edge case where the CSS style is none so we can't detect // the natural display state. if (CQ.css(this,'display') == 'none') { var elem = CQ('<' + this.tagName + '/>'); CQ('body').append(elem); this.style.display = elem.css('display'); // edge case where we still can't get the display if (this.style.display === 'none') this.style.display = 'block'; elem.remove(); elem = null; } } }) ; return this ; }, /** Simple method to hide elements without animation. */ hide: function() { var isVisible = SC.$.isVisible; this.each(function() { if (isVisible(this)) { this.oldblock = this.oldblock || CQ.css(this,'display'); this.style.display = 'none'; } }) ; return this ; }, /** Low-level dom manipulation method used by append(), before(), after() among others. Unlike the jQuery version, this version does not execute