diff --git a/CHANGELOG b/CHANGELOG index 7be5966..12607d7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Fix $(form).serialize() in Safari and add support for extending specific tags to Element.addMethods. Closes #7358. [Andrew Dupont] + * Add String.prototype.startsWith, String.prototype.endsWith, and String.prototype.include. Closes #7075. [Tobie Langel] * Improve performance of String.prototype.escapeHTML by using a cached div and text node. Closes #6937. [altblue] diff --git a/src/dom.js b/src/dom.js index c8332bb..c02cddd 100644 --- a/src/dom.js +++ b/src/dom.js @@ -41,26 +41,31 @@ document.getElementsByClassName = function(className, parentElement) { if (!window.Element) var Element = new Object(); + Element.extend = function(element) { - if (!element || _nativeExtensions || element.nodeType == 3) return element; - - if (!element._extended && element.tagName && element != window) { - var methods = Object.clone(Element.Methods), cache = Element.extend.cache; - - if (element.tagName == 'FORM') - Object.extend(methods, Form.Methods); - if (['INPUT', 'TEXTAREA', 'SELECT'].include(element.tagName)) - Object.extend(methods, Form.Element.Methods); - - Object.extend(methods, Element.Methods.Simulated); - - for (var property in methods) { - var value = methods[property]; - if (typeof value == 'function' && !(property in element)) - element[property] = cache.findOrStore(value); - } + var F = Prototype.BrowserFeatures; + if (!element || !element.tagName || element.nodeType == 3 || + element._extended || F.SpecificElementExtensions || element == window) + return element; + + var methods = {}, tagName = element.tagName, cache = Element.extend.cache, + T = Element.Methods.ByTag; + + // extend methods for all tags (Safari doesn't need this) + if (!F.ElementExtensions) { + Object.extend(methods, Element.Methods), + Object.extend(methods, Element.Methods.Simulated); } - + + // extend methods for specific tags + if (T[tagName]) Object.extend(methods, T[tagName]); + + for (var property in methods) { + var value = methods[property]; + if (typeof value == 'function' && !(property in element)) + element[property] = cache.findOrStore(value); + } + element._extended = true; return element; }; @@ -471,6 +476,8 @@ Element.Methods.Simulated = { } }; +Element.Methods.ByTag = {}; + // IE is missing .innerHTML support for TABLE-related elements if (document.all && !window.opera){ Element.Methods.update = function(element, html) { @@ -510,19 +517,36 @@ if (document.all && !window.opera){ Object.extend(Element, Element.Methods); -var _nativeExtensions = false; - -if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) - ['', 'Form', 'Input', 'TextArea', 'Select'].each(function(tag) { - var className = 'HTML' + tag + 'Element'; - if(window[className]) return; - var klass = window[className] = {}; - klass.prototype = document.createElement(tag ? tag.toLowerCase() : 'div').__proto__; - }); +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div').__proto__) { + window.HTMLElement = {}; + window.HTMLElement.prototype = document.createElement('div').__proto__; + Prototype.BrowserFeatures.ElementExtensions = true; +} Element.addMethods = function(methods) { - Object.extend(Element.Methods, methods || {}); + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + if (!tagName) + Object.extend(Element.Methods, methods || {}); + else { + if (tagName.constructor == Array) { + tagName.each(extend); + } + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = {}; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + function copy(methods, destination, onlyIfAbsent) { onlyIfAbsent = onlyIfAbsent || false; var cache = Element.extend.cache; @@ -533,16 +557,44 @@ Element.addMethods = function(methods) { } } - if (typeof HTMLElement != 'undefined') { + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = {}; + window[klass].prototype = document.createElement(tagName).__proto__; + return window[klass]; + } + + if (F.ElementExtensions) { copy(Element.Methods, HTMLElement.prototype); copy(Element.Methods.Simulated, HTMLElement.prototype, true); - copy(Form.Methods, HTMLFormElement.prototype); - [HTMLInputElement, HTMLTextAreaElement, HTMLSelectElement].each(function(klass) { - copy(Form.Element.Methods, klass.prototype); - }); - _nativeExtensions = true; } -} + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (typeof klass == "undefined") continue; + copy(T[tag], klass.prototype); + } + } +}; var Toggle = new Object(); Toggle.display = Element.toggle; diff --git a/src/form.js b/src/form.js index fc815fb..233d160 100644 --- a/src/form.js +++ b/src/form.js @@ -156,6 +156,15 @@ Form.Element.Methods = { } Object.extend(Form.Element, Form.Element.Methods); +Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) +}); + +/*--------------------------------------------------------------------------*/ + var Field = Form.Element; var $F = Form.Element.getValue; @@ -304,4 +313,3 @@ Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { return Form.serialize(this.element); } }); - diff --git a/src/prototype.js b/src/prototype.js index 3cab972..37c8fee 100644 --- a/src/prototype.js +++ b/src/prototype.js @@ -3,7 +3,11 @@ var Prototype = { Version: '<%= PROTOTYPE_VERSION %>', BrowserFeatures: { - XPath: !!document.evaluate + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + (document.createElement('div').__proto__ !== + document.createElement('form').__proto__) }, ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', diff --git a/test/unit/dom.html b/test/unit/dom.html index 4850e09..abe603e 100644 --- a/test/unit/dom.html +++ b/test/unit/dom.html @@ -256,6 +256,18 @@ var testVar = 'to be updated'; + Element.addMethods("LI", { + pancakes: function(element) { return "pancakes"; } + }); + + Element.addMethods("DIV", { + waffles: function(element) { return "waffles"; } + }); + + Element.addMethods($w("li div"), { + orangeJuice: function(element) { return "orange juice"; } + }); + new Test.Unit.Runner({ testDollarFunction: function() { with(this) { @@ -271,6 +283,7 @@ var elt = $('testdiv'); assertIdentical(elt, $(elt)); assertRespondsTo('hide', elt); + assertRespondsTo('childOf', elt); }}, testGetElementsByClassName: function() {with(this) { @@ -282,7 +295,7 @@ testInsertWithTR: function() {with(this) { new Insertion.After('second_row', 'Third Row'); - assert($('second_row').childOf('table')); + assert($('second_row').descendantOf('table')); }}, testElementVisible: function(){with(this) { @@ -801,8 +814,27 @@ elem.scrollTo(); assertEqual(Position.page(elem)[1], 0); window.scrollTo(0, 0); - }} + }}, + testSpecificElementMethods: function() {with(this) { + var elem = $('navigation_test_f'); + + assert(Element.Methods.ByTag[elem.tagName]); + assertRespondsTo('pancakes', elem); + assertEqual("pancakes", elem.pancakes()); + + var elem2 = $('test-visible'); + + assert(Element.Methods.ByTag[elem2.tagName]); + assertUndefined(elem2.pancakes); + assertRespondsTo('waffles', elem2); + assertEqual("waffles", elem2.waffles()); + + assertRespondsTo('orangeJuice', elem); + assertRespondsTo('orangeJuice', elem2); + assertEqual("orange juice", elem.orangeJuice()); + assertEqual("orange juice", elem2.orangeJuice()); + }} }, 'testlog'); // ]]> diff --git a/test/unit/form.html b/test/unit/form.html index 96a79b4..363135e 100644 --- a/test/unit/form.html +++ b/test/unit/form.html @@ -280,7 +280,27 @@ testFormSerializeWorksWithNonFormElements: function() {with(this) { assertEqual('nvm%5B%5D=One&nvm%5B%5D=Three&evu=&evm%5B%5D=&evm%5B%5D=Three', Form.serialize('form_fieldset')); assertEqual('vu=1&vm%5B%5D=1&vm%5B%5D=3&nvu=One&nvm%5B%5D=One&nvm%5B%5D=Three&evu=&evm%5B%5D=&evm%5B%5D=Three', Form.serialize('form_wrapper')); - }} + }}, + + testFormMethodsOnExtendedElements: function() {with(this) { + assertEqual(Form.serialize('form'), $('form').serialize()); + assertEqual(Form.Element.serialize('input_enabled'), $('input_enabled').serialize()); + assertNotEqual($('form').serialize, $('input_enabled').serialize); + + Element.addMethods('INPUT', { anInputMethod: function(input) { return 'input' } }); + Element.addMethods('SELECT', { aSelectMethod: function(select) { return 'select' } }); + + document.getElementById('tf_text')._extended = false; + document.getElementById('tf_selectOne')._extended = false; + + assert($('tf_text').anInputMethod); + assert(!$('tf_text').aSelectMethod); + assertEqual('input', $('tf_text').anInputMethod()); + + assert($('tf_selectOne').aSelectMethod); + assert(!$('tf_selectOne').anInputMethod); + assertEqual('select', $('tf_selectOne').aSelectMethod()); + }} }, 'testlog'); // ]]>