diff options
author | Thomas Fuchs <thomas@fesch.at> | 2005-09-28 08:20:47 +0000 |
---|---|---|
committer | Thomas Fuchs <thomas@fesch.at> | 2005-09-28 08:20:47 +0000 |
commit | 516dc2c0f16cf187f981b5e8648a7f7f1b31d190 (patch) | |
tree | d645e19a02ab5eb371302fe4742b7fd7e0a1a1e2 /actionpack/lib | |
parent | dd21e9ae39a2dc4b7eb607ff2c200c864fa19b28 (diff) | |
download | rails-516dc2c0f16cf187f981b5e8648a7f7f1b31d190.tar.gz rails-516dc2c0f16cf187f981b5e8648a7f7f1b31d190.tar.bz2 rails-516dc2c0f16cf187f981b5e8648a7f7f1b31d190.zip |
Update script.aculo.us to 1.5_rc2, and Prototype to 1.4.0_pre7
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@2386 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionpack/lib')
9 files changed, 2283 insertions, 768 deletions
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index b5f630aae0..61a0f0b45a 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -51,7 +51,12 @@ module ActionView # <script type="text/javascript" src="/javascripts/dragdrop.js"></script> def javascript_include_tag(*sources) options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { } - sources = ['prototype', 'effects', 'controls', 'dragdrop'] if sources.first == :defaults + if sources.first == :defaults + sources = ['prototype', 'scriptaculous'] + if defined?(RAILS_ROOT) and File.exists?("#{RAILS_ROOT}/public/javascripts/application.js") + sources << 'application' + end + end sources.collect { |source| source = javascript_path(source) content_tag("script", "", { "type" => "text/javascript", "src" => source }.merge(options)) diff --git a/actionpack/lib/action_view/helpers/java_script_macros_helper.rb b/actionpack/lib/action_view/helpers/java_script_macros_helper.rb index 8c67edd645..1cc814feaf 100644 --- a/actionpack/lib/action_view/helpers/java_script_macros_helper.rb +++ b/actionpack/lib/action_view/helpers/java_script_macros_helper.rb @@ -74,9 +74,8 @@ module ActionView # input field. # # Required +options+ are: - # <tt>:url</tt>:: Specifies the DOM ID of the element whose - # innerHTML should be updated with the autocomplete - # entries returned by XMLHttpRequest. + # <tt>:url</tt>:: URL to call for autocompletion results + # in url_for format. # # Addtional +options+ are: # <tt>:update</tt>:: Specifies the DOM ID of the element whose @@ -130,10 +129,41 @@ module ActionView # See the RDoc on ActionController::AutoComplete to learn more about this. def text_field_with_auto_complete(object, method, tag_options = {}, completion_options = {}) (completion_options[:skip_style] ? "" : auto_complete_stylesheet) + - text_field(object, method, { :autocomplete => "off" }.merge!(tag_options)) + + text_field(object, method, tag_options) + content_tag("div", "", :id => "#{object}_#{method}_auto_complete", :class => "auto_complete") + auto_complete_field("#{object}_#{method}", { :url => { :action => "auto_complete_for_#{object}_#{method}" } }.update(completion_options)) end + + private + def auto_complete_stylesheet + content_tag("style", <<-EOT + div.auto_complete { + width: 350px; + background: #fff; + } + div.auto_complete ul { + border:1px solid #888; + margin:0; + padding:0; + width:100%; + list-style-type:none; + } + div.auto_complete ul li { + margin:0; + padding:3px; + } + div.auto_complete ul li.selected { + background-color: #ffb; + } + div.auto_complete ul strong.highlight { + color: #800; + margin:0; + padding:0; + } + EOT + ) + end + end end end diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index acc83144d2..72d9f6dcec 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -498,34 +498,6 @@ module ActionView callbacks end - def auto_complete_stylesheet - content_tag("style", <<-EOT - div.auto_complete { - width: 350px; - background: #fff; - } - div.auto_complete ul { - border:1px solid #888; - margin:0; - padding:0; - width:100%; - list-style-type:none; - } - div.auto_complete ul li { - margin:0; - padding:3px; - } - div.auto_complete ul li.selected { - background-color: #ffb; - } - div.auto_complete ul strong.highlight { - color: #800; - margin:0; - padding:0; - } - EOT - ) - end end JavascriptHelper = JavaScriptHelper unless const_defined? :JavascriptHelper diff --git a/actionpack/lib/action_view/helpers/javascripts/controls.js b/actionpack/lib/action_view/helpers/javascripts/controls.js index cece0a914b..77046d9c35 100644 --- a/actionpack/lib/action_view/helpers/javascripts/controls.js +++ b/actionpack/lib/action_view/helpers/javascripts/controls.js @@ -1,41 +1,12 @@ // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) // (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) +// (c) 2005 Jon Tirsen (http://www.tirsen.com) +// Contributors: +// Richard Livsey +// Rahul Bhargava +// Rob Wills // -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { - var children = $(element).childNodes; - var text = ""; - var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); - - for (var i = 0; i < children.length; i++) { - if(children[i].nodeType==3) { - text+=children[i].nodeValue; - } else { - if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) - text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); - } - } - - return text; -} +// See scriptaculous.js for full license. // Autocompleter.Base handles all the autocompletion functionality // that's independent of the data source for autocompletion. This @@ -46,7 +17,7 @@ Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { // a getUpdatedChoices function that will be invoked every time // the text inside the monitored textbox changes. This method // should get the text for which to provide autocompletion by -// invoking this.getEntry(), NOT by directly accessing +// invoking this.getToken(), NOT by directly accessing // this.element.value. This is to allow incremental tokenized // autocompletion. Specific auto-completion logic (AJAX, etc) // belongs in getUpdatedChoices. @@ -57,7 +28,7 @@ Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); // will incrementally autocomplete with a comma as the token. // Additionally, ',' in the above example can be replaced with -// a token array, e.g. { tokens: new Array (',', '\n') } which +// a token array, e.g. { tokens: [',', '\n'] } which // enables autocompletion on multiple tokens. This is most // useful when one of the tokens is \n (a newline), as it // allows smart autocompletion after linebreaks. @@ -65,57 +36,54 @@ Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { var Autocompleter = {} Autocompleter.Base = function() {}; Autocompleter.Base.prototype = { - base_initialize: function(element, update, options) { + baseInitialize: function(element, update, options) { this.element = $(element); this.update = $(update); - this.has_focus = false; + this.hasFocus = false; this.changed = false; this.active = false; this.index = 0; - this.entry_count = 0; + this.entryCount = 0; if (this.setOptions) this.setOptions(options); else - this.options = {} - - this.options.tokens = this.options.tokens || new Array(); + this.options = options || {}; + + this.options.paramName = this.options.paramName || this.element.name; + this.options.tokens = this.options.tokens || []; this.options.frequency = this.options.frequency || 0.4; - this.options.min_chars = this.options.min_chars || 1; + this.options.minChars = this.options.minChars || 1; this.options.onShow = this.options.onShow || function(element, update){ if(!update.style.position || update.style.position=='absolute') { update.style.position = 'absolute'; - var offsets = Position.cumulativeOffset(element); - update.style.left = offsets[0] + 'px'; - update.style.top = (offsets[1] + element.offsetHeight) + 'px'; - update.style.width = element.offsetWidth + 'px'; + Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight}); } new Effect.Appear(update,{duration:0.15}); }; this.options.onHide = this.options.onHide || function(element, update){ new Effect.Fade(update,{duration:0.15}) }; - - if(this.options.indicator) - this.indicator = $(this.options.indicator); if (typeof(this.options.tokens) == 'string') this.options.tokens = new Array(this.options.tokens); - + this.observer = null; + this.element.setAttribute('autocomplete','off'); + Element.hide(this.update); - + Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); }, show: function() { - if(this.update.style.display=='none') this.options.onShow(this.element, this.update); - if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') { + if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update); + if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && (Element.getStyle(this.update, 'position')=='absolute')) { new Insertion.After(this.update, '<iframe id="' + this.update.id + '_iefix" '+ - 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + + 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); this.iefix = $(this.update.id+'_iefix'); } @@ -126,18 +94,18 @@ Autocompleter.Base.prototype = { Element.show(this.iefix); } }, - + hide: function() { - if(this.update.style.display=='') this.options.onHide(this.element, this.update); + if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update); if(this.iefix) Element.hide(this.iefix); }, - + startIndicator: function() { - if(this.indicator) Element.show(this.indicator); + if(this.options.indicator) Element.show(this.options.indicator); }, - + stopIndicator: function() { - if(this.indicator) Element.hide(this.indicator); + if(this.options.indicator) Element.hide(this.options.indicator); }, onKeyPress: function(event) { @@ -145,22 +113,23 @@ Autocompleter.Base.prototype = { switch(event.keyCode) { case Event.KEY_TAB: case Event.KEY_RETURN: - this.select_entry(); + this.selectEntry(); Event.stop(event); case Event.KEY_ESC: this.hide(); this.active = false; + Event.stop(event); return; case Event.KEY_LEFT: case Event.KEY_RIGHT: return; case Event.KEY_UP: - this.mark_previous(); + this.markPrevious(); this.render(); if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); return; case Event.KEY_DOWN: - this.mark_next(); + this.markNext(); this.render(); if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); return; @@ -168,15 +137,15 @@ Autocompleter.Base.prototype = { else if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) return; - + this.changed = true; - this.has_focus = true; - + this.hasFocus = true; + if(this.observer) clearTimeout(this.observer); this.observer = setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); }, - + onHover: function(event) { var element = Event.findElement(event, 'LI'); if(this.index != element.autocompleteIndex) @@ -190,92 +159,97 @@ Autocompleter.Base.prototype = { onClick: function(event) { var element = Event.findElement(event, 'LI'); this.index = element.autocompleteIndex; - this.select_entry(); - Event.stop(event); + this.selectEntry(); + this.hide(); }, onBlur: function(event) { // needed to make click events working setTimeout(this.hide.bind(this), 250); - this.has_focus = false; + this.hasFocus = false; this.active = false; }, render: function() { - if(this.entry_count > 0) { - for (var i = 0; i < this.entry_count; i++) + if(this.entryCount > 0) { + for (var i = 0; i < this.entryCount; i++) this.index==i ? - Element.addClassName(this.get_entry(i),"selected") : - Element.removeClassName(this.get_entry(i),"selected"); - - if(this.has_focus) { - if(this.get_current_entry().scrollIntoView) - this.get_current_entry().scrollIntoView(false); + Element.addClassName(this.getEntry(i),"selected") : + Element.removeClassName(this.getEntry(i),"selected"); + if(this.hasFocus) { this.show(); this.active = true; } } else this.hide(); }, - mark_previous: function() { + markPrevious: function() { if(this.index > 0) this.index-- - else this.index = this.entry_count-1; + else this.index = this.entryCcount-1; }, - mark_next: function() { - if(this.index < this.entry_count-1) this.index++ + markNext: function() { + if(this.index < this.entryCount-1) this.index++ else this.index = 0; }, - get_entry: function(index) { + getEntry: function(index) { return this.update.firstChild.childNodes[index]; }, - get_current_entry: function() { - return this.get_entry(this.index); + getCurrentEntry: function() { + return this.getEntry(this.index); }, - select_entry: function() { + selectEntry: function() { this.active = false; - value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML(); - this.updateElement(value); - this.element.focus(); + this.updateElement(this.getCurrentEntry()); }, - updateElement: function(value) { - var last_token_pos = this.findLastToken(); - if (last_token_pos != -1) { - var new_value = this.element.value.substr(0, last_token_pos + 1); - var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/); + updateElement: function(selectedElement) { + if (this.options.updateElement) { + this.options.updateElement(selectedElement); + return; + } + + var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal'); + var lastTokenPos = this.findLastToken(); + if (lastTokenPos != -1) { + var newValue = this.element.value.substr(0, lastTokenPos + 1); + var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/); if (whitespace) - new_value += whitespace[0]; - this.element.value = new_value + value; + newValue += whitespace[0]; + this.element.value = newValue + value; } else { this.element.value = value; - } + } + this.element.focus(); + + if (this.options.afterUpdateElement) + this.options.afterUpdateElement(this.element, selectedElement); }, - + updateChoices: function(choices) { - if(!this.changed && this.has_focus) { + if(!this.changed && this.hasFocus) { this.update.innerHTML = choices; Element.cleanWhitespace(this.update); Element.cleanWhitespace(this.update.firstChild); if(this.update.firstChild && this.update.firstChild.childNodes) { - this.entry_count = + this.entryCount = this.update.firstChild.childNodes.length; - for (var i = 0; i < this.entry_count; i++) { - entry = this.get_entry(i); + for (var i = 0; i < this.entryCount; i++) { + var entry = this.getEntry(i); entry.autocompleteIndex = i; this.addObservers(entry); } } else { - this.entry_count = 0; + this.entryCount = 0; } - + this.stopIndicator(); - + this.index = 0; this.render(); } @@ -288,7 +262,7 @@ Autocompleter.Base.prototype = { onObserverEvent: function() { this.changed = false; - if(this.getEntry().length>=this.options.min_chars) { + if(this.getToken().length>=this.options.minChars) { this.startIndicator(); this.getUpdatedChoices(); } else { @@ -297,58 +271,56 @@ Autocompleter.Base.prototype = { } }, - getEntry: function() { - var token_pos = this.findLastToken(); - if (token_pos != -1) - var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,''); + getToken: function() { + var tokenPos = this.findLastToken(); + if (tokenPos != -1) + var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,''); else var ret = this.element.value; - + return /\n/.test(ret) ? '' : ret; }, findLastToken: function() { - var last_token_pos = -1; + var lastTokenPos = -1; for (var i=0; i<this.options.tokens.length; i++) { - var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]); - if (this_token_pos > last_token_pos) - last_token_pos = this_token_pos; + var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]); + if (thisTokenPos > lastTokenPos) + lastTokenPos = thisTokenPos; } - return last_token_pos; + return lastTokenPos; } } Ajax.Autocompleter = Class.create(); -Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(), -Object.extend(new Ajax.Base(), { +Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), { initialize: function(element, update, url, options) { - this.base_initialize(element, update, options); + this.baseInitialize(element, update, options); this.options.asynchronous = true; - this.options.onComplete = this.onComplete.bind(this) - this.options.method = 'post'; + this.options.onComplete = this.onComplete.bind(this); this.options.defaultParams = this.options.parameters || null; this.url = url; }, - + getUpdatedChoices: function() { - entry = encodeURIComponent(this.element.name) + '=' + - encodeURIComponent(this.getEntry()); - + entry = encodeURIComponent(this.options.paramName) + '=' + + encodeURIComponent(this.getToken()); + this.options.parameters = this.options.callback ? this.options.callback(this.element, entry) : entry; - + if(this.options.defaultParams) this.options.parameters += '&' + this.options.defaultParams; - + new Ajax.Request(this.url, this.options); }, - + onComplete: function(request) { this.updateChoices(request.responseText); } -})); +}); // The local array autocompleter. Used when you'd prefer to // inject an array of autocompletion options into the page, rather @@ -362,22 +334,22 @@ Object.extend(new Ajax.Base(), { // Extra local autocompletion options: // - choices - How many autocompletion choices to offer // -// - partial_search - If false, the autocompleter will match entered +// - partialSearch - If false, the autocompleter will match entered // text only at the beginning of strings in the // autocomplete array. Defaults to true, which will // match text at the beginning of any *word* in the // strings in the autocomplete array. If you want to // search anywhere in the string, additionally set -// the option full_search to true (default: off). +// the option fullSearch to true (default: off). // -// - full_search - Search anywhere in autocomplete array strings. +// - fullSsearch - Search anywhere in autocomplete array strings. // -// - partial_chars - How many characters to enter before triggering -// a partial match (unlike min_chars, which defines +// - partialChars - How many characters to enter before triggering +// a partial match (unlike minChars, which defines // how many characters are required to do any match // at all). Defaults to 2. // -// - ignore_case - Whether to ignore case when autocompleting. +// - ignoreCase - Whether to ignore case when autocompleting. // Defaults to true. // // It's possible to pass in a custom function as the 'selector' @@ -388,7 +360,7 @@ Object.extend(new Ajax.Base(), { Autocompleter.Local = Class.create(); Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { initialize: function(element, update, array, options) { - this.base_initialize(element, update, options); + this.baseInitialize(element, update, options); this.options.array = array; }, @@ -399,41 +371,42 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { setOptions: function(options) { this.options = Object.extend({ choices: 10, - partial_search: true, - partial_chars: 2, - ignore_case: true, - full_search: false, + partialSearch: true, + partialChars: 2, + ignoreCase: true, + fullSearch: false, selector: function(instance) { - var ret = new Array(); // Beginning matches - var partial = new Array(); // Inside matches - var entry = instance.getEntry(); + var ret = []; // Beginning matches + var partial = []; // Inside matches + var entry = instance.getToken(); var count = 0; - + for (var i = 0; i < instance.options.array.length && - ret.length < instance.options.choices ; i++) { + ret.length < instance.options.choices ; i++) { + var elem = instance.options.array[i]; - var found_pos = instance.options.ignore_case ? + var foundPos = instance.options.ignoreCase ? elem.toLowerCase().indexOf(entry.toLowerCase()) : elem.indexOf(entry); - while (found_pos != -1) { - if (found_pos == 0 && elem.length != entry.length) { + while (foundPos != -1) { + if (foundPos == 0 && elem.length != entry.length) { ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + elem.substr(entry.length) + "</li>"); break; - } else if (entry.length >= instance.options.partial_chars && - instance.options.partial_search && found_pos != -1) { - if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) { - partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" + - elem.substr(found_pos, entry.length) + "</strong>" + elem.substr( - found_pos + entry.length) + "</li>"); + } else if (entry.length >= instance.options.partialChars && + instance.options.partialSearch && foundPos != -1) { + if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) { + partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" + + elem.substr(foundPos, entry.length) + "</strong>" + elem.substr( + foundPos + entry.length) + "</li>"); break; } } - found_pos = instance.options.ignore_case ? - elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) : - elem.indexOf(entry, found_pos + 1); + foundPos = instance.options.ignoreCase ? + elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) : + elem.indexOf(entry, foundPos + 1); } } @@ -444,3 +417,287 @@ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { }, options || {}); } }); + +// AJAX in-place editor +// +// The constructor takes three parameters. The first is the element +// that should support in-place editing. The second is the url to submit +// the changed value to. The server should respond with the updated +// value (the server might have post-processed it or validation might +// have prevented it from changing). The third is a hash of options. +// +// Supported options are (all are optional and have sensible defaults): +// - okText - The text of the submit button that submits the changed value +// to the server (default: "ok") +// - cancelText - The text of the link that cancels editing (default: "cancel") +// - savingText - The text being displayed as the AJAX engine communicates +// with the server (default: "Saving...") +// - formId - The id given to the <form> element +// (default: the id of the element to edit plus '-inplaceeditor') + +Ajax.InPlaceEditor = Class.create(); +Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99"; +Ajax.InPlaceEditor.prototype = { + initialize: function(element, url, options) { + this.url = url; + this.element = $(element); + + this.options = Object.extend({ + okText: "ok", + cancelText: "cancel", + savingText: "Saving...", + clickToEditText: "Click to edit", + okText: "ok", + rows: 1, + onComplete: function(transport, element) { + new Effect.Highlight(element, {startcolor: this.options.highlightcolor}); + }, + onFailure: function(transport) { + alert("Error communicating with the server: " + transport.responseText.stripTags()); + }, + callback: function(form) { + return Form.serialize(form); + }, + handleLineBreaks: true, + loadingText: 'Loading...', + savingClassName: 'inplaceeditor-saving', + formClassName: 'inplaceeditor-form', + highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor, + highlightendcolor: "#FFFFFF", + externalControl: null, + ajaxOptions: {} + }, options || {}); + + if(!this.options.formId && this.element.id) { + this.options.formId = this.element.id + "-inplaceeditor"; + if ($(this.options.formId)) { + // there's already a form with that name, don't specify an id + this.options.formId = null; + } + } + + if (this.options.externalControl) { + this.options.externalControl = $(this.options.externalControl); + } + + this.originalBackground = Element.getStyle(this.element, 'background-color'); + if (!this.originalBackground) { + this.originalBackground = "transparent"; + } + + this.element.title = this.options.clickToEditText; + + this.onclickListener = this.enterEditMode.bindAsEventListener(this); + this.mouseoverListener = this.enterHover.bindAsEventListener(this); + this.mouseoutListener = this.leaveHover.bindAsEventListener(this); + Event.observe(this.element, 'click', this.onclickListener); + Event.observe(this.element, 'mouseover', this.mouseoverListener); + Event.observe(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.observe(this.options.externalControl, 'click', this.onclickListener); + Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + }, + enterEditMode: function() { + if (this.saving) return; + if (this.editing) return; + this.editing = true; + this.onEnterEditMode(); + if (this.options.externalControl) { + Element.hide(this.options.externalControl); + } + Element.hide(this.element); + this.form = this.getForm(); + this.element.parentNode.insertBefore(this.form, this.element); + Field.focus(this.editField); + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + }, + getForm: function() { + form = document.createElement("form"); + form.id = this.options.formId; + Element.addClassName(form, this.options.formClassName) + form.onsubmit = this.onSubmit.bind(this); + + this.createEditField(form); + + if (this.options.textarea) { + var br = document.createElement("br"); + form.appendChild(br); + } + + okButton = document.createElement("input"); + okButton.type = "submit"; + okButton.value = this.options.okText; + form.appendChild(okButton); + + cancelLink = document.createElement("a"); + cancelLink.href = "#"; + cancelLink.appendChild(document.createTextNode(this.options.cancelText)); + cancelLink.onclick = this.onclickCancel.bind(this); + form.appendChild(cancelLink); + return form; + }, + hasHTMLLineBreaks: function(string) { + if (!this.options.handleLineBreaks) return false; + return string.match(/<br/i) || string.match(/<p>/i); + }, + convertHTMLLineBreaks: function(string) { + return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, ""); + }, + createEditField: function(form) { + if (this.options.rows == 1 && !this.hasHTMLLineBreaks(this.getText())) { + this.options.textarea = false; + var textField = document.createElement("input"); + textField.type = "text"; + textField.name = "value"; + textField.value = this.getText(); + textField.style.backgroundColor = this.options.highlightcolor; + var size = this.options.size || this.options.cols || 0; + if (size != 0) + textField.size = size; + form.appendChild(textField); + this.editField = textField; + } else { + this.options.textarea = true; + var textArea = document.createElement("textarea"); + textArea.name = "value"; + textArea.value = this.convertHTMLLineBreaks(this.getText()); + textArea.rows = this.options.rows; + textArea.cols = this.options.cols || 40; + form.appendChild(textArea); + this.editField = textArea; + } + }, + getText: function() { + if (this.options.loadTextURL) { + this.loadExternalText(); + return this.options.loadingText; + } else { + return this.element.innerHTML; + } + }, + loadExternalText: function() { + new Ajax.Request( + this.options.loadTextURL, + { + asynchronous: true, + onComplete: this.onLoadedExternalText.bind(this) + } + ); + }, + onLoadedExternalText: function(transport) { + this.form.value.value = transport.responseText.stripTags(); + }, + onclickCancel: function() { + this.onComplete(); + this.leaveEditMode(); + return false; + }, + onFailure: function(transport) { + this.options.onFailure(transport); + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + this.oldInnerHTML = null; + } + return false; + }, + onSubmit: function() { + this.saving = true; + new Ajax.Updater( + { + success: this.element, + // don't update on failure (this could be an option) + failure: null + }, + this.url, + Object.extend({ + parameters: this.options.callback(this.form, this.editField.value), + onComplete: this.onComplete.bind(this), + onFailure: this.onFailure.bind(this) + }, this.options.ajaxOptions) + ); + this.onLoading(); + // stop the event to avoid a page refresh in Safari + if (arguments.length > 1) { + Event.stop(arguments[0]); + } + return false; + }, + onLoading: function() { + this.saving = true; + this.removeForm(); + this.leaveHover(); + this.showSaving(); + }, + showSaving: function() { + this.oldInnerHTML = this.element.innerHTML; + this.element.innerHTML = this.options.savingText; + Element.addClassName(this.element, this.options.savingClassName); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + }, + removeForm: function() { + if(this.form) { + Element.remove(this.form); + this.form = null; + } + }, + enterHover: function() { + if (this.saving) return; + this.element.style.backgroundColor = this.options.highlightcolor; + if (this.effect) { + this.effect.cancel(); + } + Element.addClassName(this.element, this.options.hoverClassName) + }, + leaveHover: function() { + if (this.options.backgroundColor) { + this.element.style.backgroundColor = this.oldBackground; + } + Element.removeClassName(this.element, this.options.hoverClassName) + if (this.saving) return; + this.effect = new Effect.Highlight(this.element, { + startcolor: this.options.highlightcolor, + endcolor: this.options.highlightendcolor, + restorecolor: this.originalBackground + }); + }, + leaveEditMode: function() { + Element.removeClassName(this.element, this.options.savingClassName); + this.removeForm(); + this.leaveHover(); + this.element.style.backgroundColor = this.originalBackground; + Element.show(this.element); + if (this.options.externalControl) { + Element.show(this.options.externalControl); + } + this.editing = false; + this.saving = false; + this.oldInnerHTML = null; + this.onLeaveEditMode(); + }, + onComplete: function(transport) { + this.leaveEditMode(); + this.options.onComplete.bind(this)(transport, this.element); + }, + onEnterEditMode: function() {}, + onLeaveEditMode: function() {}, + dispose: function() { + if (this.oldInnerHTML) { + this.element.innerHTML = this.oldInnerHTML; + } + this.leaveEditMode(); + Event.stopObserving(this.element, 'click', this.onclickListener); + Event.stopObserving(this.element, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.element, 'mouseout', this.mouseoutListener); + if (this.options.externalControl) { + Event.stopObserving(this.options.externalControl, 'click', this.onclickListener); + Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener); + Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener); + } + } +};
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/javascripts/dragdrop.js b/actionpack/lib/action_view/helpers/javascripts/dragdrop.js index c0fd1d1e53..a8ed953a7f 100644 --- a/actionpack/lib/action_view/helpers/javascripts/dragdrop.js +++ b/actionpack/lib/action_view/helpers/javascripts/dragdrop.js @@ -2,193 +2,79 @@ // // Element.Class part Copyright (c) 2005 by Rick Olson // -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Element.Class = { - // Element.toggleClass(element, className) toggles the class being on/off - // Element.toggleClass(element, className1, className2) toggles between both classes, - // defaulting to className1 if neither exist - toggle: function(element, className) { - if(Element.Class.has(element, className)) { - Element.Class.remove(element, className); - if(arguments.length == 3) Element.Class.add(element, arguments[2]); - } else { - Element.Class.add(element, className); - if(arguments.length == 3) Element.Class.remove(element, arguments[2]); - } - }, - - // gets space-delimited classnames of an element as an array - get: function(element) { - element = $(element); - return element.className.split(' '); - }, - - // functions adapted from original functions by Gavin Kistner - remove: function(element) { - element = $(element); - var regEx; - for(var i = 1; i < arguments.length; i++) { - regEx = new RegExp("^" + arguments[i] + "\\b\\s*|\\s*\\b" + arguments[i] + "\\b", 'g'); - element.className = element.className.replace(regEx, '') - } - }, - - add: function(element) { - element = $(element); - for(var i = 1; i < arguments.length; i++) { - Element.Class.remove(element, arguments[i]); - element.className += (element.className.length > 0 ? ' ' : '') + arguments[i]; - } - }, - - // returns true if all given classes exist in said element - has: function(element) { - element = $(element); - if(!element || !element.className) return false; - var regEx; - for(var i = 1; i < arguments.length; i++) { - regEx = new RegExp("\\b" + arguments[i] + "\\b"); - if(!regEx.test(element.className)) return false; - } - return true; - }, - - // expects arrays of strings and/or strings as optional paramters - // Element.Class.has_any(element, ['classA','classB','classC'], 'classD') - has_any: function(element) { - element = $(element); - if(!element || !element.className) return false; - var regEx; - for(var i = 1; i < arguments.length; i++) { - if((typeof arguments[i] == 'object') && - (arguments[i].constructor == Array)) { - for(var j = 0; j < arguments[i].length; j++) { - regEx = new RegExp("\\b" + arguments[i][j] + "\\b"); - if(regEx.test(element.className)) return true; - } - } else { - regEx = new RegExp("\\b" + arguments[i] + "\\b"); - if(regEx.test(element.className)) return true; - } - } - return false; - }, - - childrenWith: function(element, className) { - var children = $(element).getElementsByTagName('*'); - var elements = new Array(); - - for (var i = 0; i < children.length; i++) { - if (Element.Class.has(children[i], className)) { - elements.push(children[i]); - break; - } - } - - return elements; - } -} +// See scriptaculous.js for full license. /*--------------------------------------------------------------------------*/ var Droppables = { - drops: false, - + drops: [], + remove: function(element) { - for(var i = 0; i < this.drops.length; i++) - if(this.drops[i].element == element) - this.drops.splice(i,1); + this.drops = this.drops.reject(function(e) { return e==element }); }, - + add: function(element) { - var element = $(element); + element = $(element); var options = Object.extend({ greedy: true, hoverclass: null }, arguments[1] || {}); - + // cache containers if(options.containment) { - options._containers = new Array(); + options._containers = []; var containment = options.containment; if((typeof containment == 'object') && (containment.constructor == Array)) { - for(var i=0; i<containment.length; i++) - options._containers.push($(containment[i])); + containment.each( function(c) { options._containers.push($(c)) }); } else { options._containers.push($(containment)); } - options._containers_length = - options._containers.length-1; } - + Element.makePositioned(element); // fix IE - options.element = element; - - // activate the droppable - if(!this.drops) this.drops = []; + this.drops.push(options); }, - - is_contained: function(element, drop) { - var containers = drop._containers; + + isContained: function(element, drop) { var parentNode = element.parentNode; - var i = drop._containers_length; - do { if(parentNode==containers[i]) return true; } while (i--); - return false; + return drop._containers.detect(function(c) { return parentNode == c }); }, - - is_affected: function(pX, pY, element, drop) { + + isAffected: function(pX, pY, element, drop) { return ( (drop.element!=element) && ((!drop._containers) || - this.is_contained(element, drop)) && + this.isContained(element, drop)) && ((!drop.accept) || (Element.Class.has_any(element, drop.accept))) && Position.within(drop.element, pX, pY) ); }, - + deactivate: function(drop) { - Element.Class.remove(drop.element, drop.hoverclass); + if(drop.hoverclass) + Element.Class.remove(drop.element, drop.hoverclass); this.last_active = null; }, - + activate: function(drop) { if(this.last_active) this.deactivate(this.last_active); - if(drop.hoverclass) { + if(drop.hoverclass) Element.Class.add(drop.element, drop.hoverclass); - this.last_active = drop; - } + this.last_active = drop; }, - + show: function(event, element) { - if(!this.drops) return; + if(!this.drops.length) return; var pX = Event.pointerX(event); var pY = Event.pointerY(event); Position.prepare(); - + var i = this.drops.length-1; do { var drop = this.drops[i]; - if(this.is_affected(pX, pY, element, drop)) { + if(this.isAffected(pX, pY, element, drop)) { if(drop.onHover) drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element)); if(drop.greedy) { @@ -197,43 +83,41 @@ var Droppables = { } } } while (i--); + + if(this.last_active) this.deactivate(this.last_active); }, - + fire: function(event, element) { if(!this.last_active) return; Position.prepare(); - - if (this.is_affected(Event.pointerX(event), Event.pointerY(event), element, this.last_active)) + + if (this.isAffected(Event.pointerX(event), Event.pointerY(event), element, this.last_active)) if (this.last_active.onDrop) - this.last_active.onDrop(element, this.last_active); - + this.last_active.onDrop(element, this.last_active.element); }, - + reset: function() { if(this.last_active) this.deactivate(this.last_active); } } -Draggables = { - observers: new Array(), +var Draggables = { + observers: [], addObserver: function(observer) { this.observers.push(observer); }, removeObserver: function(element) { // element instead of obsever fixes mem leaks - for(var i = 0; i < this.observers.length; i++) - if(this.observers[i].element && (this.observers[i].element == element)) - this.observers.splice(i,1); + this.observers = this.observers.reject( function(o) { return o.element==element }); }, notify: function(eventName, draggable) { // 'onStart', 'onEnd' - for(var i = 0; i < this.observers.length; i++) - this.observers[i][eventName](draggable); + this.observers.invoke(eventName, draggable); } } /*--------------------------------------------------------------------------*/ -Draggable = Class.create(); +var Draggable = Class.create(); Draggable.prototype = { initialize: function(element) { var options = Object.extend({ @@ -242,7 +126,8 @@ Draggable.prototype = { new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7}); }, reverteffect: function(element, top_offset, left_offset) { - new Effect.MoveBy(element, -top_offset, -left_offset, {duration:0.4}); + var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02; + new Effect.MoveBy(element, -top_offset, -left_offset, {duration:dur}); }, endeffect: function(element) { new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0}); @@ -250,12 +135,12 @@ Draggable.prototype = { zindex: 1000, revert: false }, arguments[1] || {}); - + this.element = $(element); this.handle = options.handle ? $(options.handle) : this.element; - - Element.makePositioned(this.element); // fix IE - + + Element.makePositioned(this.element); // fix IE + this.offsetX = 0; this.offsetY = 0; this.originalLeft = this.currentLeft(); @@ -263,27 +148,34 @@ Draggable.prototype = { this.originalX = this.element.offsetLeft; this.originalY = this.element.offsetTop; this.originalZ = parseInt(this.element.style.zIndex || "0"); - + this.options = options; - + this.active = false; this.dragging = false; - + this.eventMouseDown = this.startDrag.bindAsEventListener(this); this.eventMouseUp = this.endDrag.bindAsEventListener(this); this.eventMouseMove = this.update.bindAsEventListener(this); this.eventKeypress = this.keyPress.bindAsEventListener(this); - Event.observe(this.handle, "mousedown", this.eventMouseDown); + this.registerEvents(); + }, + destroy: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + this.unregisterEvents(); + }, + registerEvents: function() { Event.observe(document, "mouseup", this.eventMouseUp); Event.observe(document, "mousemove", this.eventMouseMove); Event.observe(document, "keypress", this.eventKeypress); + Event.observe(this.handle, "mousedown", this.eventMouseDown); }, - destroy: function() { - Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); - Event.stopObserving(document, "mouseup", this.eventMouseUp); - Event.stopObserving(document, "mousemove", this.eventMouseMove); - Event.stopObserving(document, "keypress", this.eventKeypress); + unregisterEvents: function() { + //if(!this.active) return; + //Event.stopObserving(document, "mouseup", this.eventMouseUp); + //Event.stopObserving(document, "mousemove", this.eventMouseMove); + //Event.stopObserving(document, "keypress", this.eventKeypress); }, currentLeft: function() { return parseInt(this.element.style.left || '0'); @@ -293,27 +185,42 @@ Draggable.prototype = { }, startDrag: function(event) { if(Event.isLeftClick(event)) { - this.active = true; - var style = this.element.style; - this.originalY = this.element.offsetTop - this.currentTop() - this.originalTop; - this.originalX = this.element.offsetLeft - this.currentLeft() - this.originalLeft; - this.offsetY = event.clientY - this.originalY - this.originalTop; - this.offsetX = event.clientX - this.originalX - this.originalLeft; + // abort on form elements, fixes a Firefox issue + var src = Event.element(event); + if(src.tagName && ( + src.tagName=='INPUT' || + src.tagName=='SELECT' || + src.tagName=='BUTTON' || + src.tagName=='TEXTAREA')) return; + // this.registerEvents(); + this.active = true; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.element); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); Event.stop(event); } }, finishDrag: function(event, success) { + // this.unregisterEvents(); + this.active = false; this.dragging = false; - + + if(this.options.ghosting) { + Position.relativize(this.element); + Element.remove(this._clone); + this._clone = null; + } + if(success) Droppables.fire(event, this.element); Draggables.notify('onEnd', this); - + var revert = this.options.revert; if(revert && typeof revert == 'function') revert = revert(this.element); - + if(revert && this.options.reverteffect) { this.options.reverteffect(this.element, this.currentTop()-this.originalTop, @@ -322,12 +229,13 @@ Draggable.prototype = { this.originalLeft = this.currentLeft(); this.originalTop = this.currentTop(); } - + this.element.style.zIndex = this.originalZ; - + if(this.options.endeffect) this.options.endeffect(this.element); - + + Droppables.reset(); }, keyPress: function(event) { @@ -347,13 +255,15 @@ Draggable.prototype = { this.dragging = false; }, draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.element); + offsets[0] -= this.currentLeft(); + offsets[1] -= this.currentTop(); var style = this.element.style; - this.originalX = this.element.offsetLeft - this.currentLeft() - this.originalLeft; - this.originalY = this.element.offsetTop - this.currentTop() - this.originalTop; if((!this.options.constraint) || (this.options.constraint=='horizontal')) - style.left = ((event.clientX - this.originalX) - this.offsetX) + "px"; + style.left = (pointer[0] - offsets[0] - this.offsetX) + "px"; if((!this.options.constraint) || (this.options.constraint=='vertical')) - style.top = ((event.clientY - this.originalY) - this.offsetY) + "px"; + style.top = (pointer[1] - offsets[1] - this.offsetY) + "px"; if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering }, update: function(event) { @@ -363,17 +273,24 @@ Draggable.prototype = { this.dragging = true; if(style.position=="") style.position = "relative"; style.zIndex = this.options.zindex; + + if(this.options.ghosting) { + this._clone = this.element.cloneNode(true); + Position.absolutize(this.element); + this.element.parentNode.insertBefore(this._clone, this.element); + } + Draggables.notify('onStart', this); if(this.options.starteffect) this.options.starteffect(this.element); } - + Droppables.show(event, this.element); this.draw(event); if(this.options.change) this.options.change(this); - + // fix AppleWebKit rendering if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); - + Event.stop(event); } } @@ -381,7 +298,7 @@ Draggable.prototype = { /*--------------------------------------------------------------------------*/ -SortableObserver = Class.create(); +var SortableObserver = Class.create(); SortableObserver.prototype = { initialize: function(element, observer) { this.element = $(element); @@ -391,147 +308,197 @@ SortableObserver.prototype = { onStart: function() { this.lastValue = Sortable.serialize(this.element); }, - onEnd: function() { + onEnd: function() { + Sortable.unmark(); if(this.lastValue != Sortable.serialize(this.element)) this.observer(this.element) } } -Sortable = { +var Sortable = { sortables: new Array(), options: function(element){ - var element = $(element); - for(var i=0;i<this.sortables.length;i++) - if(this.sortables[i].element == element) - return this.sortables[i]; - return null; + element = $(element); + return this.sortables.detect(function(s) { return s.element == element }); }, destroy: function(element){ - var element = $(element); - for(var i=0;i<this.sortables.length;i++) { - if(this.sortables[i].element == element) { - var s = this.sortables[i]; - Draggables.removeObserver(s.element); - for(var j=0;j<s.droppables.length;j++) - Droppables.remove(s.droppables[j]); - for(var j=0;j<s.draggables.length;j++) - s.draggables[j].destroy(); - this.sortables.splice(i,1); - } - } + element = $(element); + this.sortables.findAll(function(s) { return s.element == element }).each(function(s){ + Draggables.removeObserver(s.element); + s.droppables.each(function(d){ Droppables.remove(d) }); + s.draggables.invoke('destroy'); + }); + this.sortables = this.sortables.reject(function(s) { return s.element == element }); }, create: function(element) { - var element = $(element); + element = $(element); var options = Object.extend({ element: element, tag: 'li', // assumes li children, override with tag: 'tagname' + dropOnEmpty: false, + tree: false, // fixme: unimplemented overlap: 'vertical', // one of 'vertical', 'horizontal' constraint: 'vertical', // one of 'vertical', 'horizontal', false containment: element, // also takes array of elements (or id's); or false handle: false, // or a CSS class only: false, hoverclass: null, + ghosting: false, onChange: function() {}, onUpdate: function() {} }, arguments[1] || {}); - + // clear any old sortable with same element this.destroy(element); - + // build options for the draggables var options_for_draggable = { revert: true, + ghosting: options.ghosting, constraint: options.constraint, - handle: handle }; + handle: options.handle }; + if(options.starteffect) options_for_draggable.starteffect = options.starteffect; + if(options.reverteffect) options_for_draggable.reverteffect = options.reverteffect; + else + if(options.ghosting) options_for_draggable.reverteffect = function(element) { + element.style.top = 0; + element.style.left = 0; + }; + if(options.endeffect) options_for_draggable.endeffect = options.endeffect; + if(options.zindex) options_for_draggable.zindex = options.zindex; - + // build options for the droppables var options_for_droppable = { overlap: options.overlap, containment: options.containment, hoverclass: options.hoverclass, - onHover: function(element, dropon, overlap) { - if(overlap>0.5) { - if(dropon.previousSibling != element) { - var oldParentNode = element.parentNode; - element.style.visibility = "hidden"; // fix gecko rendering - dropon.parentNode.insertBefore(element, dropon); - if(dropon.parentNode!=oldParentNode && oldParentNode.sortable) - oldParentNode.sortable.onChange(element); - if(dropon.parentNode.sortable) - dropon.parentNode.sortable.onChange(element); - } - } else { - var nextElement = dropon.nextSibling || null; - if(nextElement != element) { - var oldParentNode = element.parentNode; - element.style.visibility = "hidden"; // fix gecko rendering - dropon.parentNode.insertBefore(element, nextElement); - if(dropon.parentNode!=oldParentNode && oldParentNode.sortable) - oldParentNode.sortable.onChange(element); - if(dropon.parentNode.sortable) - dropon.parentNode.sortable.onChange(element); - } - } - } + onHover: Sortable.onHover, + greedy: !options.dropOnEmpty } // fix for gecko engine Element.cleanWhitespace(element); - + options.draggables = []; options.droppables = []; - - // make it so - var elements = element.childNodes; - for (var i = 0; i < elements.length; i++) - if(elements[i].tagName && elements[i].tagName==options.tag.toUpperCase() && - (!options.only || (Element.Class.has(elements[i], options.only)))) { - - // handles are per-draggable - var handle = options.handle ? - Element.Class.childrenWith(elements[i], options.handle)[0] : elements[i]; - - options.draggables.push(new Draggable(elements[i], Object.extend(options_for_draggable, { handle: handle }))); - - Droppables.add(elements[i], options_for_droppable); - options.droppables.push(elements[i]); - - } - + + // make it so + + // drop on empty handling + if(options.dropOnEmpty) { + Droppables.add(element, + {containment: options.containment, onHover: Sortable.onEmptyHover, greedy: false}); + options.droppables.push(element); + } + + (this.findElements(element, options) || []).each( function(e) { + // handles are per-draggable + var handle = options.handle ? + Element.Class.childrenWith(e, options.handle)[0] : e; + options.draggables.push( + new Draggable(e, Object.extend(options_for_draggable, { handle: handle }))); + Droppables.add(e, options_for_droppable); + options.droppables.push(e); + }); + // keep reference this.sortables.push(options); - + // for onupdate Draggables.addObserver(new SortableObserver(element, options.onUpdate)); }, + + // return all suitable-for-sortable elements in a guaranteed order + findElements: function(element, options) { + if(!element.hasChildNodes()) return null; + var elements = []; + $A(element.childNodes).each( function(e) { + if(e.tagName && e.tagName==options.tag.toUpperCase() && + (!options.only || (Element.Class.has(e, options.only)))) + elements.push(e); + if(options.tree) { + var grandchildren = this.findElements(e, options); + if(grandchildren) elements.push(grandchildren); + } + }); + + return (elements.length>0 ? elements.flatten() : null); + }, + + onHover: function(element, dropon, overlap) { + if(overlap>0.5) { + Sortable.mark(dropon, 'before'); + if(dropon.previousSibling != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, dropon); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } else { + Sortable.mark(dropon, 'after'); + var nextElement = dropon.nextSibling || null; + if(nextElement != element) { + var oldParentNode = element.parentNode; + element.style.visibility = "hidden"; // fix gecko rendering + dropon.parentNode.insertBefore(element, nextElement); + if(dropon.parentNode!=oldParentNode) + Sortable.options(oldParentNode).onChange(element); + Sortable.options(dropon.parentNode).onChange(element); + } + } + }, + + onEmptyHover: function(element, dropon) { + if(element.parentNode!=dropon) { + dropon.appendChild(element); + } + }, + + unmark: function() { + if(Sortable._marker) Element.hide(Sortable._marker); + }, + + mark: function(dropon, position) { + // mark on ghosting only + var sortable = Sortable.options(dropon.parentNode); + if(sortable && !sortable.ghosting) return; + + if(!Sortable._marker) { + Sortable._marker = $('dropmarker') || document.createElement('DIV'); + Element.hide(Sortable._marker); + Element.Class.add(Sortable._marker, 'dropmarker'); + Sortable._marker.style.position = 'absolute'; + document.getElementsByTagName("body").item(0).appendChild(Sortable._marker); + } + var offsets = Position.cumulativeOffset(dropon); + Sortable._marker.style.top = offsets[1] + 'px'; + if(position=='after') Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px'; + Sortable._marker.style.left = offsets[0] + 'px'; + Element.show(Sortable._marker); + }, + serialize: function(element) { - var element = $(element); + element = $(element); var sortableOptions = this.options(element); var options = Object.extend({ tag: sortableOptions.tag, only: sortableOptions.only, name: element.id }, arguments[1] || {}); - - var items = $(element).childNodes; - var queryComponents = new Array(); - - for(var i=0; i<items.length; i++) - if(items[i].tagName && items[i].tagName==options.tag.toUpperCase() && - (!options.only || (Element.Class.has(items[i], options.only)))) - queryComponents.push( - encodeURIComponent(options.name) + "[]=" + - encodeURIComponent(items[i].id.split("_")[1])); - - return queryComponents.join("&"); + return $A(element.childNodes).collect( function(item) { + return (encodeURIComponent(options.name) + "[]=" + + encodeURIComponent(item.id.split("_")[1])); + }).join("&"); } -}
\ No newline at end of file +}
\ No newline at end of file diff --git a/actionpack/lib/action_view/helpers/javascripts/effects.js b/actionpack/lib/action_view/helpers/javascripts/effects.js index a8735f507c..d6fbed805e 100644 --- a/actionpack/lib/action_view/helpers/javascripts/effects.js +++ b/actionpack/lib/action_view/helpers/javascripts/effects.js @@ -1,30 +1,51 @@ // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) -// -// Parts (c) 2005 Justin Palmer (http://encytemedia.com/) -// Parts (c) 2005 Mark Pilgrim (http://diveintomark.org/) +// Contributors: +// Justin Palmer (http://encytemedia.com/) +// Mark Pilgrim (http://diveintomark.org/) +// Martin Bialasinki // -// Permission is hereby granted, free of charge, to any person obtaining -// a copy of this software and associated documentation files (the -// "Software"), to deal in the Software without restriction, including -// without limitation the rights to use, copy, modify, merge, publish, -// distribute, sublicense, and/or sell copies of the Software, and to -// permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// See scriptaculous.js for full license. + +var Effect = { + tagifyText: function(element) { + var tagifyStyle = "position:relative"; + if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ";zoom:1"; + element = $(element); + $A(element.childNodes).each( function(child) { + if(child.nodeType==3) { + child.nodeValue.toArray().each( function(character) { + element.insertBefore( + Builder.node('span',{style: tagifyStyle}, + character == " " ? String.fromCharCode(160) : character), + child); + }); + Element.remove(child); + } + }); + }, + multiple: function(element, effect) { + var elements; + if(((typeof element == 'object') || + (typeof element == 'function')) && + (element.length)) + elements = element; + else + elements = $(element).childNodes; + + var options = Object.extend({ + speed: 0.1, + delay: 0.0 + }, arguments[2] || {}); + var speed = options.speed; + var delay = options.delay; + $A(elements).each( function(element, index) { + new effect(element, Object.extend(options, { delay: delay + index * speed })); + }); + } +}; -Effect = {} -Effect2 = Effect; // deprecated +var Effect2 = Effect; // deprecated /* ------------- transitions ------------- */ @@ -40,7 +61,7 @@ Effect.Transitions.reverse = function(pos) { return 1-pos; } Effect.Transitions.flicker = function(pos) { - return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random(0.25); + return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4; } Effect.Transitions.wobble = function(pos) { return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5; @@ -56,73 +77,109 @@ Effect.Transitions.full = function(pos) { return 1; } -/* ------------- element ext -------------- */ - -Element.makePositioned = function(element) { - element = $(element); - if(element.style.position == "") - element.style.position = "relative"; -} - -Element.makeClipping = function(element) { - element = $(element); - element._overflow = element.style.overflow || 'visible'; - if(element._overflow!='hidden') element.style.overflow = 'hidden'; -} +/* ------------- core effects ------------- */ -Element.undoClipping = function(element) { - element = $(element); - if(element._overflow!='hidden') element.style.overflow = element._overflow; +Effect.Queue = { + effects: [], + interval: null, + add: function(effect) { + var timestamp = new Date().getTime(); + + switch(effect.options.queue) { + case 'front': + // move unstarted effects after this effect + this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { + e.startOn += effect.finishOn; + e.finishOn += effect.finishOn; + }); + break; + case 'end': + // start effect after last queued effect has finished + timestamp = this.effects.pluck('finishOn').max() || timestamp; + break; + } + + effect.startOn += timestamp; + effect.finishOn += timestamp; + this.effects.push(effect); + if(!this.interval) + this.interval = setInterval(this.loop.bind(this), 40); + }, + remove: function(effect) { + this.effects = this.effects.reject(function(e) { return e==effect }); + if(this.effects.length == 0) { + clearInterval(this.interval); + this.interval = null; + } + }, + loop: function() { + var timePos = new Date().getTime(); + this.effects.invoke('loop', timePos); + } } -/* ------------- core effects ------------- */ - Effect.Base = function() {}; Effect.Base.prototype = { setOptions: function(options) { this.options = Object.extend({ transition: Effect.Transitions.sinoidal, duration: 1.0, // seconds - fps: 25.0, // max. 100fps + fps: 25.0, // max. 25fps due to Effect.Queue implementation sync: false, // true for combining from: 0.0, - to: 1.0 + to: 1.0, + delay: 0.0, + queue: 'parallel' }, options || {}); }, start: function(options) { this.setOptions(options || {}); this.currentFrame = 0; - this.startOn = new Date().getTime(); + this.state = 'idle'; + this.startOn = this.options.delay*1000; this.finishOn = this.startOn + (this.options.duration*1000); - if(this.options.beforeStart) this.options.beforeStart(this); - if(!this.options.sync) this.loop(); + this.event('beforeStart'); + if(!this.options.sync) Effect.Queue.add(this); }, - loop: function() { - var timePos = new Date().getTime(); - if(timePos >= this.finishOn) { - this.render(this.options.to); - if(this.finish) this.finish(); - if(this.options.afterFinish) this.options.afterFinish(this); - return; - } - var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); - var frame = Math.round(pos * this.options.fps * this.options.duration); - if(frame > this.currentFrame) { - this.render(pos); - this.currentFrame = frame; + loop: function(timePos) { + if(timePos >= this.startOn) { + if(timePos >= this.finishOn) { + this.render(1.0); + this.cancel(); + this.event('beforeFinish'); + if(this.finish) this.finish(); + this.event('afterFinish'); + return; + } + var pos = (timePos - this.startOn) / (this.finishOn - this.startOn); + var frame = Math.round(pos * this.options.fps * this.options.duration); + if(frame > this.currentFrame) { + this.render(pos); + this.currentFrame = frame; + } } - this.timeout = setTimeout(this.loop.bind(this), 10); }, render: function(pos) { + if(this.state == 'idle') { + this.state = 'running'; + this.event('beforeSetup'); + if(this.setup) this.setup(); + this.event('afterSetup'); + } if(this.options.transition) pos = this.options.transition(pos); pos *= (this.options.to-this.options.from); - pos += this.options.from; - if(this.options.beforeUpdate) this.options.beforeUpdate(this); + pos += this.options.from; + this.event('beforeUpdate'); if(this.update) this.update(pos); - if(this.options.afterUpdate) this.options.afterUpdate(this); + this.event('afterUpdate'); }, cancel: function() { - if(this.timeout) clearTimeout(this.timeout); + if(!this.options.sync) Effect.Queue.remove(this); + this.state = 'finished'; + }, + event: function(eventName) { + if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); + if(this.options[eventName]) this.options[eventName](this); } } @@ -133,35 +190,36 @@ Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), { this.start(arguments[1]); }, update: function(position) { - for (var i = 0; i < this.effects.length; i++) - this.effects[i].render(position); + this.effects.invoke('render', position); }, finish: function(position) { - for (var i = 0; i < this.effects.length; i++) - if(this.effects[i].finish) this.effects[i].finish(position); + this.effects.each( function(effect) { + effect.render(1.0); + effect.cancel(); + effect.event('beforeFinish'); + if(effect.finish) effect.finish(position); + effect.event('afterFinish'); + }); } }); -// Internet Explorer caveat: works only on elements the have -// a 'layout', meaning having a given width or height. -// There is no way to safely set this automatically. Effect.Opacity = Class.create(); Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), { initialize: function(element) { this.element = $(element); - options = Object.extend({ - from: 0.0, + + // make this work on IE on elements without 'layout' + if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout)) + this.element.style.zoom = 1; + + var options = Object.extend({ + from: Element.getOpacity(this.element) || 0.0, to: 1.0 }, arguments[1] || {}); this.start(options); }, update: function(position) { - this.setOpacity(position); - }, - setOpacity: function(opacity) { - opacity = (opacity == 1) ? 0.99999 : opacity; - this.element.style.opacity = opacity; - this.element.style.filter = "alpha(opacity:"+opacity*100+")"; + Element.setOpacity(this.element, position); } }); @@ -169,16 +227,23 @@ Effect.MoveBy = Class.create(); Object.extend(Object.extend(Effect.MoveBy.prototype, Effect.Base.prototype), { initialize: function(element, toTop, toLeft) { this.element = $(element); - this.originalTop = parseFloat(this.element.style.top || '0'); - this.originalLeft = parseFloat(this.element.style.left || '0'); this.toTop = toTop; this.toLeft = toLeft; - Element.makePositioned(this.element); this.start(arguments[3]); }, + setup: function() { + // Bug in Opera: Opera returns the "real" position of a static element or + // relative element that does not have top/left explicitly set. + // ==> Always set top and left for position relative elements in your stylesheets + // (to 0 if you do not need them) + + Element.makePositioned(this.element); + this.originalTop = parseFloat(Element.getStyle(this.element,'top') || '0'); + this.originalLeft = parseFloat(Element.getStyle(this.element,'left') || '0'); + }, update: function(position) { - topd = this.toTop * position + this.originalTop; - leftd = this.toLeft * position + this.originalLeft; + var topd = this.toTop * position + this.originalTop; + var leftd = this.toLeft * position + this.originalLeft; this.setPosition(topd, leftd); }, setPosition: function(topd, leftd) { @@ -191,50 +256,76 @@ Effect.Scale = Class.create(); Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), { initialize: function(element, percent) { this.element = $(element) - options = Object.extend({ + var options = Object.extend({ scaleX: true, scaleY: true, scaleContent: true, scaleFromCenter: false, scaleMode: 'box', // 'box' or 'contents' or {} with provided values - scaleFrom: 100.0 + scaleFrom: 100.0, + scaleTo: percent }, arguments[2] || {}); + this.start(options); + }, + setup: function() { + this.restoreAfterFinish = this.options.restoreAfterFinish || false; + this.elementPositioning = Element.getStyle(this.element,'position'); + this.elementStyleTop = this.element.style.top; + this.elementStyleLeft = this.element.style.left; + this.elementStyleWidth = this.element.style.width; + this.elementStyleHeight = this.element.style.height; + this.elementStyleFontSize = this.element.style.fontSize; this.originalTop = this.element.offsetTop; this.originalLeft = this.element.offsetLeft; - if(this.element.style.fontSize=="") this.sizeEm = 1.0; - if(this.element.style.fontSize && this.element.style.fontSize.indexOf("em")>0) - this.sizeEm = parseFloat(this.element.style.fontSize); - this.factor = (percent/100.0) - (options.scaleFrom/100.0); - if(options.scaleMode=='box') { + var fontSize = Element.getStyle(this.element,'font-size') || "100%"; + if(fontSize.indexOf("em")>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = "em"; + } else if(fontSize.indexOf("px")>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = "px"; + } else if(fontSize.indexOf("%")>0) { + this.fontSize = parseFloat(fontSize); + this.fontSizeType = "%"; + } + this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; + if(this.options.scaleMode=='box') { this.originalHeight = this.element.clientHeight; this.originalWidth = this.element.clientWidth; - } else - if(options.scaleMode=='contents') { + } else if(this.options.scaleMode=='contents') { this.originalHeight = this.element.scrollHeight; this.originalWidth = this.element.scrollWidth; } else { - this.originalHeight = options.scaleMode.originalHeight; - this.originalWidth = options.scaleMode.originalWidth; + this.originalHeight = this.options.scaleMode.originalHeight; + this.originalWidth = this.options.scaleMode.originalWidth; } - this.start(options); }, - update: function(position) { - currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); - if(this.options.scaleContent && this.sizeEm) - this.element.style.fontSize = this.sizeEm*currentScale + "em"; + var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); + if(this.options.scaleContent && this.fontSize) + this.element.style.fontSize = this.fontSize*currentScale + this.fontSizeType; this.setDimensions( this.originalWidth * currentScale, this.originalHeight * currentScale); }, - + finish: function(position) { + if (this.restoreAfterFinish) { + var els = this.element.style; + els.top = this.elementStyleTop; + els.left = this.elementStyleLeft; + els.width = this.elementStyleWidth; + els.height = this.elementStyleHeight; + els.height = this.elementStyleHeight; + els.fontSize = this.elementStyleFontSize; + } + }, setDimensions: function(width, height) { if(this.options.scaleX) this.element.style.width = width + 'px'; if(this.options.scaleY) this.element.style.height = height + 'px'; if(this.options.scaleFromCenter) { - topd = (height - this.originalHeight)/2; - leftd = (width - this.originalWidth)/2; - if(this.element.style.position=='absolute') { + var topd = (height - this.originalHeight)/2; + var leftd = (width - this.originalWidth)/2; + if(this.elementPositioning == 'absolute') { if(this.options.scaleY) this.element.style.top = this.originalTop-topd + "px"; if(this.options.scaleX) this.element.style.left = this.originalLeft-leftd + "px"; } else { @@ -249,33 +340,28 @@ Effect.Highlight = Class.create(); Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), { initialize: function(element) { this.element = $(element); - - // try to parse current background color as default for endcolor - // browser stores this as: "rgb(255, 255, 255)", convert to "#ffffff" format - var endcolor = "#ffffff"; - var current = this.element.style.backgroundColor; - if(current && current.slice(0,4) == "rgb(") { - endcolor = "#"; - var cols = current.slice(4,current.length-1).split(','); - var i=0; do { endcolor += parseInt(cols[i]).toColorPart() } while (++i<3); } - var options = Object.extend({ - startcolor: "#ffff99", - endcolor: endcolor, - restorecolor: current + startcolor: "#ffff99" }, arguments[1] || {}); - + this.start(options); + }, + setup: function() { + // Disable background image during the effect + this.oldBgImage = this.element.style.backgroundImage; + this.element.style.backgroundImage = "none"; + if(!this.options.endcolor) + this.options.endcolor = Element.getStyle(this.element, 'background-color').parseColor('#ffffff'); + if (typeof this.options.restorecolor == "undefined") + this.options.restorecolor = this.element.style.backgroundColor; // init color calculations this.colors_base = [ - parseInt(options.startcolor.slice(1,3),16), - parseInt(options.startcolor.slice(3,5),16), - parseInt(options.startcolor.slice(5),16) ]; + parseInt(this.options.startcolor.slice(1,3),16), + parseInt(this.options.startcolor.slice(3,5),16), + parseInt(this.options.startcolor.slice(5),16) ]; this.colors_delta = [ - parseInt(options.endcolor.slice(1,3),16)-this.colors_base[0], - parseInt(options.endcolor.slice(3,5),16)-this.colors_base[1], - parseInt(options.endcolor.slice(5),16)-this.colors_base[2] ]; - - this.start(options); + parseInt(this.options.endcolor.slice(1,3),16)-this.colors_base[0], + parseInt(this.options.endcolor.slice(3,5),16)-this.colors_base[1], + parseInt(this.options.endcolor.slice(5),16)-this.colors_base[2]]; }, update: function(position) { var colors = [ @@ -287,6 +373,7 @@ Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), }, finish: function() { this.element.style.backgroundColor = this.options.restorecolor; + this.element.style.backgroundImage = this.oldBgImage; } }); @@ -294,6 +381,9 @@ Effect.ScrollTo = Class.create(); Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { initialize: function(element) { this.element = $(element); + this.start(arguments[1] || {}); + }, + setup: function() { Position.prepare(); var offsets = Position.cumulativeOffset(this.element); var max = window.innerHeight ? @@ -302,8 +392,7 @@ Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { (document.documentElement.clientHeight ? document.documentElement.clientHeight : document.body.clientHeight); this.scrollStart = Position.deltaY; - this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; - this.start(arguments[1] || {}); + this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart; }, update: function(position) { Position.prepare(); @@ -312,51 +401,61 @@ Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), { } }); -/* ------------- prepackaged effects ------------- */ +/* ------------- combination effects ------------- */ Effect.Fade = function(element) { - options = Object.extend({ - from: 1.0, + var oldOpacity = Element.getInlineOpacity(element); + var options = Object.extend({ + from: Element.getOpacity(element) || 1.0, to: 0.0, - afterFinish: function(effect) - { Element.hide(effect.element); - effect.setOpacity(1); } + afterFinishInternal: function(effect) + { if (effect.options.to == 0) { + Element.hide(effect.element); + Element.setInlineOpacity(effect.element, oldOpacity); + } + } }, arguments[1] || {}); - new Effect.Opacity(element,options); + return new Effect.Opacity(element,options); } Effect.Appear = function(element) { - options = Object.extend({ - from: 0.0, + var options = Object.extend({ + from: (Element.getStyle(element, "display") == "none" ? 0.0 : Element.getOpacity(element) || 0.0), to: 1.0, - beforeStart: function(effect) - { effect.setOpacity(0); - Element.show(effect.element); }, - afterUpdate: function(effect) - { Element.show(effect.element); } + beforeSetup: function(effect) + { Element.setOpacity(effect.element, effect.options.from); + Element.show(effect.element); } }, arguments[1] || {}); - new Effect.Opacity(element,options); + return new Effect.Opacity(element,options); } Effect.Puff = function(element) { - new Effect.Parallel( - [ new Effect.Scale(element, 200, { sync: true, scaleFromCenter: true }), - new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ], - { duration: 1.0, - afterUpdate: function(effect) + element = $(element); + var oldOpacity = Element.getInlineOpacity(element); + var oldPosition = element.style.position; + return new Effect.Parallel( + [ new Effect.Scale(element, 200, + { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), + new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], + Object.extend({ duration: 1.0, + beforeSetupInternal: function(effect) { effect.effects[0].element.style.position = 'absolute'; }, - afterFinish: function(effect) - { Element.hide(effect.effects[0].element); } - } + afterFinishInternal: function(effect) + { Element.hide(effect.effects[0].element); + effect.effects[0].element.style.position = oldPosition; + Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + }, arguments[1] || {}) ); } Effect.BlindUp = function(element) { + element = $(element); Element.makeClipping(element); - new Effect.Scale(element, 0, + return new Effect.Scale(element, 0, Object.extend({ scaleContent: false, scaleX: false, - afterFinish: function(effect) + restoreAfterFinish: true, + afterFinishInternal: function(effect) { Element.hide(effect.element); Element.undoClipping(effect.element); @@ -366,120 +465,179 @@ Effect.BlindUp = function(element) { } Effect.BlindDown = function(element) { - $(element).style.height = '0px'; - Element.makeClipping(element); - Element.show(element); - new Effect.Scale(element, 100, + element = $(element); + var oldHeight = element.style.height; + var elementDimensions = Element.getDimensions(element); + return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, - scaleX: false, - scaleMode: 'contents', + scaleX: false, scaleFrom: 0, - afterFinish: function(effect) { + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + Element.makeClipping(effect.element); + effect.element.style.height = "0px"; + Element.show(effect.element); + }, + afterFinishInternal: function(effect) { Element.undoClipping(effect.element); + effect.element.style.height = oldHeight; } }, arguments[1] || {}) ); } Effect.SwitchOff = function(element) { - new Effect.Appear(element, - { duration: 0.4, - transition: Effect.Transitions.flicker, - afterFinish: function(effect) - { effect.element.style.overflow = 'hidden'; - new Effect.Scale(effect.element, 1, - { duration: 0.3, scaleFromCenter: true, - scaleX: false, scaleContent: false, - afterUpdate: function(effect) { - if(effect.element.style.position=="") - effect.element.style.position = 'relative'; }, - afterFinish: function(effect) { Element.hide(effect.element); } - } ) - } - } ); + element = $(element); + var oldOpacity = Element.getInlineOpacity(element); + return new Effect.Appear(element, { + duration: 0.4, + from: 0, + transition: Effect.Transitions.flicker, + afterFinishInternal: function(effect) { + new Effect.Scale(effect.element, 1, { + duration: 0.3, scaleFromCenter: true, + scaleX: false, scaleContent: false, restoreAfterFinish: true, + beforeSetup: function(effect) { + Element.makePositioned(effect.element); + Element.makeClipping(effect.element); + }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element); + Element.setInlineOpacity(effect.element, oldOpacity); + } + }) + } + }); } Effect.DropOut = function(element) { - new Effect.Parallel( + element = $(element); + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldOpacity = Element.getInlineOpacity(element); + return new Effect.Parallel( [ new Effect.MoveBy(element, 100, 0, { sync: true }), - new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0 } ) ], - { duration: 0.5, - afterFinish: function(effect) - { Element.hide(effect.effects[0].element); } - }); + new Effect.Opacity(element, { sync: true, to: 0.0 }) ], + Object.extend( + { duration: 0.5, + beforeSetup: function(effect) { + Element.makePositioned(effect.effects[0].element); }, + afterFinishInternal: function(effect) { + Element.hide(effect.effects[0].element); + Element.undoPositioned(effect.effects[0].element); + effect.effects[0].element.style.left = oldLeft; + effect.effects[0].element.style.top = oldTop; + Element.setInlineOpacity(effect.effects[0].element, oldOpacity); } + }, arguments[1] || {})); } Effect.Shake = function(element) { - new Effect.MoveBy(element, 0, 20, - { duration: 0.05, afterFinish: function(effect) { + element = $(element); + var oldTop = element.style.top; + var oldLeft = element.style.left; + return new Effect.MoveBy(element, 0, 20, + { duration: 0.05, afterFinishInternal: function(effect) { new Effect.MoveBy(effect.element, 0, -40, - { duration: 0.1, afterFinish: function(effect) { + { duration: 0.1, afterFinishInternal: function(effect) { new Effect.MoveBy(effect.element, 0, 40, - { duration: 0.1, afterFinish: function(effect) { + { duration: 0.1, afterFinishInternal: function(effect) { new Effect.MoveBy(effect.element, 0, -40, - { duration: 0.1, afterFinish: function(effect) { + { duration: 0.1, afterFinishInternal: function(effect) { new Effect.MoveBy(effect.element, 0, 40, - { duration: 0.1, afterFinish: function(effect) { + { duration: 0.1, afterFinishInternal: function(effect) { new Effect.MoveBy(effect.element, 0, -20, - { duration: 0.05, afterFinish: function(effect) { + { duration: 0.05, afterFinishInternal: function(effect) { + Element.undoPositioned(effect.element); + effect.element.style.left = oldLeft; + effect.element.style.top = oldTop; }}) }}) }}) }}) }}) }}); } Effect.SlideDown = function(element) { element = $(element); - element.style.height = '0px'; - Element.makeClipping(element); Element.cleanWhitespace(element); - Element.makePositioned(element.firstChild); - Element.show(element); - new Effect.Scale(element, 100, + // SlideDown need to have the content of the element wrapped in a container element with fixed height! + var oldInnerBottom = element.firstChild.style.bottom; + var elementDimensions = Element.getDimensions(element); + return new Effect.Scale(element, 100, Object.extend({ scaleContent: false, scaleX: false, - scaleMode: 'contents', scaleFrom: 0, - afterUpdate: function(effect) - { effect.element.firstChild.style.bottom = - (effect.originalHeight - effect.element.clientHeight) + 'px'; }, - afterFinish: function(effect) - { Element.undoClipping(effect.element); } + scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, + restoreAfterFinish: true, + afterSetup: function(effect) { + Element.makePositioned(effect.element.firstChild); + if (window.opera) effect.element.firstChild.style.top = ""; + Element.makeClipping(effect.element); + element.style.height = '0'; + Element.show(element); + }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinishInternal: function(effect) { + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element.firstChild); + effect.element.firstChild.style.bottom = oldInnerBottom; } }, arguments[1] || {}) ); } Effect.SlideUp = function(element) { element = $(element); - Element.makeClipping(element); Element.cleanWhitespace(element); - Element.makePositioned(element.firstChild); - Element.show(element); - new Effect.Scale(element, 0, + var oldInnerBottom = element.firstChild.style.bottom; + return new Effect.Scale(element, 0, Object.extend({ scaleContent: false, scaleX: false, - afterUpdate: function(effect) - { effect.element.firstChild.style.bottom = - (effect.originalHeight - effect.element.clientHeight) + 'px'; }, - afterFinish: function(effect) - { + scaleMode: 'box', + scaleFrom: 100, + restoreAfterFinish: true, + beforeStartInternal: function(effect) { + Element.makePositioned(effect.element.firstChild); + if (window.opera) effect.element.firstChild.style.top = ""; + Element.makeClipping(effect.element); + Element.show(element); + }, + afterUpdateInternal: function(effect) { + effect.element.firstChild.style.bottom = + (effect.originalHeight - effect.element.clientHeight) + 'px'; }, + afterFinishInternal: function(effect) { Element.hide(effect.element); - Element.undoClipping(effect.element); - } + Element.undoClipping(effect.element); + Element.undoPositioned(effect.element.firstChild); + effect.element.firstChild.style.bottom = oldInnerBottom; } }, arguments[1] || {}) ); } Effect.Squish = function(element) { - new Effect.Scale(element, 0, - { afterFinish: function(effect) { Element.hide(effect.element); } }); + // Bug in opera makes the TD containing this element expand for a instance after finish + return new Effect.Scale(element, window.opera ? 1 : 0, + { restoreAfterFinish: true, + beforeSetup: function(effect) { + Element.makeClipping(effect.element); }, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); } + }); } Effect.Grow = function(element) { element = $(element); var options = arguments[1] || {}; - var originalWidth = element.clientWidth; - var originalHeight = element.clientHeight; - element.style.overflow = 'hidden'; - Element.show(element); + var elementDimensions = Element.getDimensions(element); + var originalWidth = elementDimensions.width; + var originalHeight = elementDimensions.height; + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldHeight = element.style.height; + var oldWidth = element.style.width; + var oldOpacity = Element.getInlineOpacity(element); var direction = options.direction || 'center'; var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; @@ -517,18 +675,40 @@ Effect.Grow = function(element) { break; } - new Effect.MoveBy(element, initialMoveY, initialMoveX, { + return new Effect.MoveBy(element, initialMoveY, initialMoveX, { duration: 0.01, - beforeUpdate: function(effect) { $(element).style.height = '0px'; }, - afterFinish: function(effect) { + beforeSetup: function(effect) { + Element.hide(effect.element); + Element.makeClipping(effect.element); + Element.makePositioned(effect.element); + }, + afterFinishInternal: function(effect) { new Effect.Parallel( - [ new Effect.Opacity(element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }), - new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }), - new Effect.Scale(element, 100, { + [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: opacityTransition }), + new Effect.MoveBy(effect.element, moveY, moveX, { sync: true, transition: moveTransition }), + new Effect.Scale(effect.element, 100, { scaleMode: { originalHeight: originalHeight, originalWidth: originalWidth }, - sync: true, scaleFrom: 0, scaleTo: 100, transition: scaleTransition })], - options); } - }); + sync: true, scaleFrom: window.opera ? 1 : 0, transition: scaleTransition, restoreAfterFinish: true}) + ], Object.extend({ + beforeSetup: function(effect) { + effect.effects[0].element.style.height = 0; + Element.show(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + var el = effect.effects[0].element; + var els = el.style; + Element.undoClipping(el); + Element.undoPositioned(el); + els.top = oldTop; + els.left = oldLeft; + els.height = oldHeight; + els.width = originalWidth; + Element.setInlineOpacity(el, oldOpacity); + } + }, options) + ) + } + }); } Effect.Shrink = function(element) { @@ -537,8 +717,11 @@ Effect.Shrink = function(element) { var originalWidth = element.clientWidth; var originalHeight = element.clientHeight; - element.style.overflow = 'hidden'; - Element.show(element); + var oldTop = element.style.top; + var oldLeft = element.style.left; + var oldHeight = element.style.height; + var oldWidth = element.style.width; + var oldOpacity = Element.getInlineOpacity(element); var direction = options.direction || 'center'; var moveTransition = options.moveTransition || Effect.Transitions.sinoidal; @@ -569,44 +752,65 @@ Effect.Shrink = function(element) { break; } - new Effect.Parallel( + return new Effect.Parallel( [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: opacityTransition }), - new Effect.Scale(element, 0, { sync: true, transition: moveTransition }), - new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: scaleTransition }) ], - options); + new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: scaleTransition, restoreAfterFinish: true}), + new Effect.MoveBy(element, moveY, moveX, { sync: true, transition: moveTransition }) + ], Object.extend({ + beforeStartInternal: function(effect) { + Element.makePositioned(effect.effects[0].element); + Element.makeClipping(effect.effects[0].element); + }, + afterFinishInternal: function(effect) { + var el = effect.effects[0].element; + var els = el.style; + Element.hide(el); + Element.undoClipping(el); + Element.undoPositioned(el); + els.top = oldTop; + els.left = oldLeft; + els.height = oldHeight; + els.width = oldWidth; + Element.setInlineOpacity(el, oldOpacity); + } + }, options) + ); } Effect.Pulsate = function(element) { + element = $(element); var options = arguments[1] || {}; + var oldOpacity = Element.getInlineOpacity(element); var transition = options.transition || Effect.Transitions.sinoidal; var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) }; reverser.bind(transition); - new Effect.Opacity(element, - Object.extend(Object.extend({ duration: 3.0, - afterFinish: function(effect) { Element.show(effect.element); } + return new Effect.Opacity(element, + Object.extend(Object.extend({ duration: 3.0, from: 0, + afterFinishInternal: function(effect) { Element.setInlineOpacity(effect.element, oldOpacity); } }, options), {transition: reverser})); } Effect.Fold = function(element) { - $(element).style.overflow = 'hidden'; - new Effect.Scale(element, 5, Object.extend({ - scaleContent: false, - scaleTo: 100, - scaleX: false, - afterFinish: function(effect) { - new Effect.Scale(element, 1, { - scaleContent: false, - scaleTo: 0, - scaleY: false, - afterFinish: function(effect) { Element.hide(effect.element) } }); - }}, arguments[1] || {})); -} - -// old: new Effect.ContentZoom(element, percent) -// new: Element.setContentZoom(element, percent) - -Element.setContentZoom = function(element, percent) { - var element = $(element); - element.style.fontSize = (percent/100) + "em"; - if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + element = $(element); + var originalTop = element.style.top; + var originalLeft = element.style.left; + var originalWidth = element.style.width; + var originalHeight = element.style.height; + Element.makeClipping(element); + return new Effect.Scale(element, 5, Object.extend({ + scaleContent: false, + scaleX: false, + afterFinishInternal: function(effect) { + new Effect.Scale(element, 1, { + scaleContent: false, + scaleY: false, + afterFinishInternal: function(effect) { + Element.hide(effect.element); + Element.undoClipping(effect.element); + effect.element.style.top = originalTop; + effect.element.style.left = originalLeft; + effect.element.style.width = originalWidth; + effect.element.style.height = originalHeight; + } }); + }}, arguments[1] || {})); } diff --git a/actionpack/lib/action_view/helpers/javascripts/prototype.js b/actionpack/lib/action_view/helpers/javascripts/prototype.js index 5feddb6dda..c0ec65450d 100644 --- a/actionpack/lib/action_view/helpers/javascripts/prototype.js +++ b/actionpack/lib/action_view/helpers/javascripts/prototype.js @@ -1,4 +1,4 @@ -/* Prototype JavaScript framework, version 1.3.1 +/* Prototype JavaScript framework, version 1.4.0_pre7 * (c) 2005 Sam Stephenson <sam@conio.net> * * THIS FILE IS AUTOMATICALLY GENERATED. When sending patches, please diff @@ -11,8 +11,10 @@ /*--------------------------------------------------------------------------*/ var Prototype = { - Version: '1.3.1', - emptyFunction: function() {} + Version: '1.4.0_pre7', + + emptyFunction: function() {}, + K: function(x) {return x} } var Class = { @@ -32,29 +34,36 @@ Object.extend = function(destination, source) { return destination; } -Object.prototype.extend = function(object) { - return Object.extend.apply(this, [this, object]); -} - Function.prototype.bind = function(object) { var __method = this; return function() { - __method.apply(object, arguments); + return __method.apply(object, arguments); } } Function.prototype.bindAsEventListener = function(object) { var __method = this; return function(event) { - __method.call(object, event || window.event); + return __method.call(object, event || window.event); } } -Number.prototype.toColorPart = function() { - var digits = this.toString(16); - if (this < 16) return '0' + digits; - return digits; -} +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); var Try = { these: function() { @@ -140,14 +149,14 @@ if (!Function.prototype.apply) { object.__apply__ = this; var result = eval('object.__apply__(' + - parameterStrings[i].join(', ') + ')'); + parameterStrings.join(', ') + ')'); object.__apply__ = null; return result; } } -String.prototype.extend({ +Object.extend(String.prototype, { stripTags: function() { return this.replace(/<\/?[^>]+>/gi, ''); }, @@ -163,9 +172,262 @@ String.prototype.extend({ var div = document.createElement('div'); div.innerHTML = this.stripTags(); return div.childNodes[0].nodeValue; + }, + + parseQuery: function() { + var str = this; + if (str.substring(0,1) == '?') { + str = this.substring(1); + } + var result = {}; + var pairs = str.split('&'); + for (var i = 0; i < pairs.length; i++) { + var pair = pairs[i].split('='); + result[pair[0]] = pair[1]; + } + return result; } }); + +var _break = new Object(); +var _continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != _continue) throw e; + } + }); + } catch (e) { + if (e != _break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + if (!(result &= (iterator || Prototype.K)(value, index))) + throw _break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result &= (iterator || Prototype.K)(value, index)) + throw _break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw _break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw _break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value >= (result || value)) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value <= (result || value)) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + iterator(value = collections.pluck(index)); + return value; + }); + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); + +var $A = Array.from = function(iterable) { + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + } +}); + +Object.extend(Array.prototype, Enumerable); + +var Range = Class.create(); +Object.extend(Range.prototype, Enumerable); +Object.extend(Range.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new Range(start, end, exclusive); +} + var Ajax = { getTransport: function() { return Try.these( @@ -183,7 +445,8 @@ Ajax.Base.prototype = { method: 'post', asynchronous: true, parameters: '' - }.extend(options || {}); + } + Object.extend(this.options, options || {}); }, responseIsSuccess: function() { @@ -201,7 +464,7 @@ Ajax.Request = Class.create(); Ajax.Request.Events = ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; -Ajax.Request.prototype = (new Ajax.Base()).extend({ +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { initialize: function(url, options) { this.transport = Ajax.getTransport(); this.setOptions(options); @@ -262,16 +525,26 @@ Ajax.Request.prototype = (new Ajax.Base()).extend({ if (readyState != 1) this.respondToReadyState(this.transport.readyState); }, + + evalJSON: function() { + try { + var json = this.transport.getResponseHeader('X-JSON'), object; + object = eval(json); + return object; + } catch (e) { + } + }, respondToReadyState: function(readyState) { var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); if (event == 'Complete') (this.options['on' + this.transport.status] || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] - || Prototype.emptyFunction)(this.transport); + || Prototype.emptyFunction)(transport, json); - (this.options['on' + event] || Prototype.emptyFunction)(this.transport); + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ if (event == 'Complete') @@ -282,7 +555,7 @@ Ajax.Request.prototype = (new Ajax.Base()).extend({ Ajax.Updater = Class.create(); Ajax.Updater.ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:<\/script>)'; -Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({ +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { initialize: function(container, url, options) { this.containers = { success: container.success ? $(container.success) : $(container), @@ -294,9 +567,9 @@ Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({ this.setOptions(options); var onComplete = this.options.onComplete || Prototype.emptyFunction; - this.options.onComplete = (function() { + this.options.onComplete = (function(transport, object) { this.updateContent(); - onComplete(this.transport); + onComplete(transport, object); }).bind(this); this.request(url); @@ -320,8 +593,7 @@ Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({ if (this.responseIsSuccess()) { if (this.onComplete) - setTimeout((function() {this.onComplete( - this.transport)}).bind(this), 10); + setTimeout(this.onComplete.bind(this), 10); } if (this.options.evalScripts && scripts) { @@ -335,7 +607,7 @@ Ajax.Updater.prototype.extend(Ajax.Request.prototype).extend({ }); Ajax.PeriodicalUpdater = Class.create(); -Ajax.PeriodicalUpdater.prototype = (new Ajax.Base()).extend({ +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { initialize: function(container, url, options) { this.setOptions(options); this.onComplete = this.options.onComplete; @@ -494,20 +766,35 @@ Abstract.Insertion.prototype = { this.content = content; if (this.adjacency && this.element.insertAdjacentHTML) { - this.element.insertAdjacentHTML(this.adjacency, this.content); + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + if (this.element.tagName.toLowerCase() == 'tbody') { + this.fragment = this.contentFromAnonymousTable(); + this.insertContent(); + } else { + throw e; + } + } } else { this.range = this.element.ownerDocument.createRange(); if (this.initializeRange) this.initializeRange(); this.fragment = this.range.createContextualFragment(this.content); this.insertContent(); } + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '<table><tbody>' + this.content + '</tbody></table>'; + return div.childNodes[0].childNodes[0].childNodes[0]; } } var Insertion = new Object(); Insertion.Before = Class.create(); -Insertion.Before.prototype = (new Abstract.Insertion('beforeBegin')).extend({ +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { initializeRange: function() { this.range.setStartBefore(this.element); }, @@ -518,7 +805,7 @@ Insertion.Before.prototype = (new Abstract.Insertion('beforeBegin')).extend({ }); Insertion.Top = Class.create(); -Insertion.Top.prototype = (new Abstract.Insertion('afterBegin')).extend({ +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { initializeRange: function() { this.range.selectNodeContents(this.element); this.range.collapse(true); @@ -530,7 +817,7 @@ Insertion.Top.prototype = (new Abstract.Insertion('afterBegin')).extend({ }); Insertion.Bottom = Class.create(); -Insertion.Bottom.prototype = (new Abstract.Insertion('beforeEnd')).extend({ +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { initializeRange: function() { this.range.selectNodeContents(this.element); this.range.collapse(this.element); @@ -542,7 +829,7 @@ Insertion.Bottom.prototype = (new Abstract.Insertion('beforeEnd')).extend({ }); Insertion.After = Class.create(); -Insertion.After.prototype = (new Abstract.Insertion('afterEnd')).extend({ +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { initializeRange: function() { this.range.setStartAfter(this.element); }, @@ -704,19 +991,32 @@ Form.Element.Serializers = { textarea: function(element) { return [element.name, element.value]; }, - + select: function(element) { - var value = ''; - if (element.type == 'select-one') { - var index = element.selectedIndex; - if (index >= 0) - value = element.options[index].value || element.options[index].text; - } else { - value = new Array(); - for (var i = 0; i < element.length; i++) { - var opt = element.options[i]; - if (opt.selected) - value.push(opt.value || opt.text); + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) + value = opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) + optValue = opt.text; + value.push(optValue); } } return [element.name, value]; @@ -754,14 +1054,14 @@ Abstract.TimedObserver.prototype = { } Form.Element.Observer = Class.create(); -Form.Element.Observer.prototype = (new Abstract.TimedObserver()).extend({ +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { getValue: function() { return Form.Element.getValue(this.element); } }); Form.Observer = Class.create(); -Form.Observer.prototype = (new Abstract.TimedObserver()).extend({ +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { getValue: function() { return Form.serialize(this.element); } @@ -826,14 +1126,14 @@ Abstract.EventObserver.prototype = { } Form.Element.EventObserver = Class.create(); -Form.Element.EventObserver.prototype = (new Abstract.EventObserver()).extend({ +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { getValue: function() { return Form.Element.getValue(this.element); } }); Form.EventObserver = Class.create(); -Form.EventObserver.prototype = (new Abstract.EventObserver()).extend({ +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { getValue: function() { return Form.serialize(this.element); } @@ -880,6 +1180,7 @@ Object.extend(Event, { event.stopPropagation(); } else { event.returnValue = false; + event.cancelBubble = true; } }, @@ -920,7 +1221,7 @@ Object.extend(Event, { useCapture = useCapture || false; if (name == 'keypress' && - ((navigator.appVersion.indexOf('AppleWebKit') > 0) + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) || element.attachEvent)) name = 'keydown'; @@ -932,7 +1233,7 @@ Object.extend(Event, { useCapture = useCapture || false; if (name == 'keypress' && - ((navigator.appVersion.indexOf('AppleWebKit') > 0) + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) || element.detachEvent)) name = 'keydown'; diff --git a/actionpack/lib/action_view/helpers/javascripts/slider.js b/actionpack/lib/action_view/helpers/javascripts/slider.js new file mode 100644 index 0000000000..1712b98943 --- /dev/null +++ b/actionpack/lib/action_view/helpers/javascripts/slider.js @@ -0,0 +1,258 @@ +// Copyright (c) 2005 Marty Haught +// +// See scriptaculous.js for full license. + +if(!Control) var Control = {}; +Control.Slider = Class.create(); + +// options: +// axis: 'vertical', or 'horizontal' (default) +// increment: (default: 1) +// step: (default: 1) +// +// callbacks: +// onChange(value) +// onSlide(value) +Control.Slider.prototype = { + initialize: function(handle, track, options) { + this.handle = $(handle); + this.track = $(track); + + this.options = options || {}; + + this.axis = this.options.axis || 'horizontal'; + this.increment = this.options.increment || 1; + this.step = parseInt(this.options.step) || 1; + this.value = 0; + + var defaultMaximum = Math.round(this.track.offsetWidth / this.increment); + if(this.isVertical()) defaultMaximum = Math.round(this.track.offsetHeight / this.increment); + + this.maximum = this.options.maximum || defaultMaximum; + this.minimum = this.options.minimum || 0; + + // Will be used to align the handle onto the track, if necessary + this.alignX = parseInt (this.options.alignX) || 0; + this.alignY = parseInt (this.options.alignY) || 0; + + // Zero out the slider position + this.setCurrentLeft(Position.cumulativeOffset(this.track)[0] - Position.cumulativeOffset(this.handle)[0] + this.alignX); + this.setCurrentTop(this.trackTop() - Position.cumulativeOffset(this.handle)[1] + this.alignY); + + this.offsetX = 0; + this.offsetY = 0; + + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + this.originalZ = parseInt(this.handle.style.zIndex || "0"); + + // Prepopulate Slider value + this.setSliderValue(parseInt(this.options.sliderValue) || 0); + + this.active = false; + this.dragging = false; + this.disabled = false; + + // FIXME: use css + this.handleImage = $(this.options.handleImage) || false; + this.handleDisabled = this.options.handleDisabled || false; + this.handleEnabled = false; + if(this.handleImage) + this.handleEnabled = this.handleImage.src || false; + + if(this.options.disabled) + this.setDisabled(); + + // Value Array + this.values = this.options.values || false; // Add method to validate and sort?? + + Element.makePositioned(this.handle); // fix IE + + this.eventMouseDown = this.startDrag.bindAsEventListener(this); + this.eventMouseUp = this.endDrag.bindAsEventListener(this); + this.eventMouseMove = this.update.bindAsEventListener(this); + this.eventKeypress = this.keyPress.bindAsEventListener(this); + + Event.observe(this.handle, "mousedown", this.eventMouseDown); + Event.observe(document, "mouseup", this.eventMouseUp); + Event.observe(document, "mousemove", this.eventMouseMove); + Event.observe(document, "keypress", this.eventKeypress); + }, + dispose: function() { + Event.stopObserving(this.handle, "mousedown", this.eventMouseDown); + Event.stopObserving(document, "mouseup", this.eventMouseUp); + Event.stopObserving(document, "mousemove", this.eventMouseMove); + Event.stopObserving(document, "keypress", this.eventKeypress); + }, + setDisabled: function(){ + this.disabled = true; + if(this.handleDisabled) + this.handleImage.src = this.handleDisabled; + }, + setEnabled: function(){ + this.disabled = false; + if(this.handleEnabled) + this.handleImage.src = this.handleEnabled; + }, + currentLeft: function() { + return parseInt(this.handle.style.left || '0'); + }, + currentTop: function() { + return parseInt(this.handle.style.top || '0'); + }, + setCurrentLeft: function(left) { + this.handle.style.left = left +"px"; + }, + setCurrentTop: function(top) { + this.handle.style.top = top +"px"; + }, + trackLeft: function(){ + return Position.cumulativeOffset(this.track)[0]; + }, + trackTop: function(){ + return Position.cumulativeOffset(this.track)[1]; + }, + getNearestValue: function(value){ + if(this.values){ + var i = 0; + var offset = Math.abs(this.values[0] - value); + var newValue = this.values[0]; + + for(i=0; i < this.values.length; i++){ + var currentOffset = Math.abs(this.values[i] - value); + if(currentOffset < offset){ + newValue = this.values[i]; + offset = currentOffset; + } + } + return newValue; + } + return value; + }, + setSliderValue: function(sliderValue){ + // First check our max and minimum and nearest values + sliderValue = this.getNearestValue(sliderValue); + if(sliderValue > this.maximum) sliderValue = this.maximum; + if(sliderValue < this.minimum) sliderValue = this.minimum; + var offsetDiff = (sliderValue - (this.value||this.minimum)) * this.increment; + + if(this.isVertical()){ + this.setCurrentTop(offsetDiff + this.currentTop()); + } else { + this.setCurrentLeft(offsetDiff + this.currentLeft()); + } + this.value = sliderValue; + this.updateFinished(); + }, + minimumOffset: function(){ + return(this.isVertical() ? + this.trackTop() + this.alignY : + this.trackLeft() + this.alignX); + }, + maximumOffset: function(){ + return(this.isVertical() ? + this.trackTop() + this.alignY + (this.maximum - this.minimum) * this.increment : + this.trackLeft() + this.alignX + (this.maximum - this.minimum) * this.increment); + }, + isVertical: function(){ + return (this.axis == 'vertical'); + }, + startDrag: function(event) { + if(Event.isLeftClick(event)) { + if(!this.disabled){ + this.active = true; + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.handle); + this.offsetX = (pointer[0] - offsets[0]); + this.offsetY = (pointer[1] - offsets[1]); + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + } + Event.stop(event); + } + }, + update: function(event) { + if(this.active) { + if(!this.dragging) { + var style = this.handle.style; + this.dragging = true; + if(style.position=="") style.position = "relative"; + style.zIndex = this.options.zindex; + } + this.draw(event); + // fix AppleWebKit rendering + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); + Event.stop(event); + } + }, + draw: function(event) { + var pointer = [Event.pointerX(event), Event.pointerY(event)]; + var offsets = Position.cumulativeOffset(this.handle); + + offsets[0] -= this.currentLeft(); + offsets[1] -= this.currentTop(); + + // Adjust for the pointer's position on the handle + pointer[0] -= this.offsetX; + pointer[1] -= this.offsetY; + var style = this.handle.style; + + if(this.isVertical()){ + if(pointer[1] > this.maximumOffset()) + pointer[1] = this.maximumOffset(); + if(pointer[1] < this.minimumOffset()) + pointer[1] = this.minimumOffset(); + + // Increment by values + if(this.values){ + this.value = this.getNearestValue(Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum); + pointer[1] = this.trackTop() + this.alignY + (this.value - this.minimum) * this.increment; + } else { + this.value = Math.round((pointer[1] - this.minimumOffset()) / this.increment) + this.minimum; + } + style.top = pointer[1] - offsets[1] + "px"; + } else { + if(pointer[0] > this.maximumOffset()) pointer[0] = this.maximumOffset(); + if(pointer[0] < this.minimumOffset()) pointer[0] = this.minimumOffset(); + // Increment by values + if(this.values){ + this.value = this.getNearestValue(Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum); + pointer[0] = this.trackLeft() + this.alignX + (this.value - this.minimum) * this.increment; + } else { + this.value = Math.round((pointer[0] - this.minimumOffset()) / this.increment) + this.minimum; + } + style.left = (pointer[0] - offsets[0]) + "px"; + } + if(this.options.onSlide) this.options.onSlide(this.value); + }, + endDrag: function(event) { + if(this.active && this.dragging) { + this.finishDrag(event, true); + Event.stop(event); + } + this.active = false; + this.dragging = false; + }, + finishDrag: function(event, success) { + this.active = false; + this.dragging = false; + this.handle.style.zIndex = this.originalZ; + this.originalLeft = this.currentLeft(); + this.originalTop = this.currentTop(); + this.updateFinished(); + }, + updateFinished: function() { + if(this.options.onChange) this.options.onChange(this.value); + }, + keyPress: function(event) { + if(this.active && !this.disabled) { + switch(event.keyCode) { + case Event.KEY_ESC: + this.finishDrag(event, false); + Event.stop(event); + break; + } + if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); + } + } +} diff --git a/actionpack/lib/action_view/helpers/javascripts/util.js b/actionpack/lib/action_view/helpers/javascripts/util.js new file mode 100644 index 0000000000..c83101b7d2 --- /dev/null +++ b/actionpack/lib/action_view/helpers/javascripts/util.js @@ -0,0 +1,521 @@ +// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) +// +// See scriptaculous.js for full license. + +Object.inspect = function(obj) { + var info = []; + + if(typeof obj in ["string","number"]) { + return obj; + } else { + for(property in obj) + if(typeof obj[property]!="function") + info.push(property + ' => ' + + (typeof obj[property] == "string" ? + '"' + obj[property] + '"' : + obj[property])); + } + + return ("'" + obj + "' #" + typeof obj + + ": {" + info.join(", ") + "}"); +} + +// borrowed from http://www.schuerig.de/michael/javascript/stdext.js +// Copyright (c) 2005, Michael Schuerig, michael@schuerig.de + +Array.flatten = function(array, excludeUndefined) { + if (excludeUndefined === undefined) { + excludeUndefined = false; + } + var result = []; + var len = array.length; + for (var i = 0; i < len; i++) { + var el = array[i]; + if (el instanceof Array) { + var flat = el.flatten(excludeUndefined); + result = result.concat(flat); + } else if (!excludeUndefined || el != undefined) { + result.push(el); + } + } + return result; +}; + +if (!Array.prototype.flatten) { + Array.prototype.flatten = function(excludeUndefined) { + return Array.flatten(this, excludeUndefined); + } +} + +String.prototype.toArray = function() { + var results = []; + for (var i = 0; i < this.length; i++) + results.push(this.charAt(i)); + return results; +} + +/*--------------------------------------------------------------------------*/ + +var Builder = { + node: function(elementName) { + var element = document.createElement('div'); + element.innerHTML = + "<" + elementName + "></" + elementName + ">"; + + // attributes (or text) + if(arguments[1]) + if(this._isStringOrNumber(arguments[1]) || + (arguments[1] instanceof Array)) { + this._children(element.firstChild, arguments[1]); + } else { + var attrs = this._attributes(arguments[1]); + if(attrs.length) + element.innerHTML = "<" +elementName + " " + + attrs + "></" + elementName + ">"; + } + + // text, or array of children + if(arguments[2]) + this._children(element.firstChild, arguments[2]); + + return element.firstChild; + }, + _text: function(text) { + return document.createTextNode(text); + }, + _attributes: function(attributes) { + var attrs = []; + for(attribute in attributes) + attrs.push((attribute=='className' ? 'class' : attribute) + + '="' + attributes[attribute].toString().escapeHTML() + '"'); + return attrs.join(" "); + }, + _children: function(element, children) { + if(typeof children=='object') { // array can hold nodes and text + children.flatten().each( function(e) { + if(typeof e=='object') + element.appendChild(e) + else + if(Builder._isStringOrNumber(e)) + element.appendChild(Builder._text(e)); + }); + } else + if(Builder._isStringOrNumber(children)) + element.appendChild(Builder._text(children)); + }, + _isStringOrNumber: function(param) { + return(typeof param=='string' || typeof param=='number'); + } +} + +/* ------------- element ext -------------- */ + +// adapted from http://dhtmlkitchen.com/learn/js/setstyle/index4.jsp +// note: Safari return null on elements with display:none; see http://bugzilla.opendarwin.org/show_bug.cgi?id=4125 +// instead of "auto" values returns null so it's easier to use with || constructs + +String.prototype.camelize = function() { + var oStringList = this.split('-'); + if(oStringList.length == 1) + return oStringList[0]; + var ret = this.indexOf("-") == 0 ? + oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) : oStringList[0]; + for(var i = 1, len = oStringList.length; i < len; i++){ + var s = oStringList[i]; + ret += s.charAt(0).toUpperCase() + s.substring(1) + } + return ret; +} + +Element.getStyle = function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if(!value) + if(document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = (css!=null) ? css.getPropertyValue(style) : null; + } else if(element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + + // If top, left, bottom, or right values have been queried, return "auto" for consistency resaons + // if position is "static", as Opera (and others?) returns the pixel values relative to root element + // (or positioning context?) + if (window.opera && (style == "left" || style == "top" || style == "right" || style == "bottom")) + if (Element.getStyle(element, "position") == "static") value = "auto"; + + if(value=='auto') value = null; + return value; +} + +// converts rgb() and #xxx to #xxxxxx format, +// returns self (or first argument) if not convertable +String.prototype.parseColor = function() { + color = "#"; + if(this.slice(0,4) == "rgb(") { + var cols = this.slice(4,this.length-1).split(','); + var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3); + } else { + if(this.slice(0,1) == '#') { + if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase(); + if(this.length==7) color = this.toLowerCase(); + } + } + return(color.length==7 ? color : (arguments[0] || this)); +} + +Element.makePositioned = function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if(pos =='static' || !pos) { + element._madePositioned = true; + element.style.position = "relative"; + // Opera returns the offset relative to the positioning context, when an element is position relative + // but top and left have not been defined + if (window.opera){ + element.style.top = 0; + element.style.left = 0; + } + } +} + +Element.undoPositioned = function(element) { + element = $(element); + if(typeof element._madePositioned != "undefined"){ + element._madePositioned = undefined; + element.style.position = ""; + element.style.top = ""; + element.style.left = ""; + element.style.bottom = ""; + element.style.right = ""; + } +} + +Element.makeClipping = function(element) { + element = $(element); + if (typeof element._overflow != 'undefined') return; + element._overflow = element.style.overflow; + if((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') element.style.overflow = 'hidden'; +} + +Element.undoClipping = function(element) { + element = $(element); + if (typeof element._overflow == 'undefined') return; + element.style.overflow = element._overflow; + element._overflow = undefined; +} + +Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { + var children = $(element).childNodes; + var text = ""; + var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); + + for (var i = 0; i < children.length; i++) { + if(children[i].nodeType==3) { + text+=children[i].nodeValue; + } else { + if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) + text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); + } + } + + return text; +} + +Element.setContentZoom = function(element, percent) { + element = $(element); + element.style.fontSize = (percent/100) + "em"; + if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0); +} + +Element.getOpacity = function(element){ + return parseFloat(Element.getStyle(element, "opacity") || '1'); +} + +Element.setOpacity = function(element, value){ + element= $(element); + var els = element.style; + if (value == 1){ + els.opacity = '0.999999'; + els.filter = null; + } else { + if(value < 0.00001) value = 0; + els.opacity = value; + els.filter = "alpha(opacity:"+value*100+")"; + } +} + +Element.getInlineOpacity = function(element){ + element= $(element); + var op; + op = element.style.opacity; + if (typeof op != "undefined" && op != "") return op; + return ""; +} + +Element.setInlineOpacity = function(element, value){ + element= $(element); + var els = element.style; + els.opacity = value; +} + +Element.getDimensions = function(element){ + element = $(element); + // All *Width and *Height properties give 0 on elements with display "none", so enable the element temporarily + if (element.style.display == "none"){ + var originalVisibility = element.style.visibility; + var originalPosition = element.style.position; + element.style.visibility = "hidden"; + element.style.position = "absolute"; + element.style.display = ""; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + element.style.display = "none"; + element.style.position = originalPosition; + element.style.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + } else { + return {width: element.offsetWidth, height: element.offsetHeight}; + } +} + +/*--------------------------------------------------------------------------*/ + +Position.positionedOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element,'position'); + if(p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; +} + +// Safari returns margins on body which is incorrect if the child is absolutely positioned. +// for performance reasons, we create a specialized version of Position.cumulativeOffset for +// KHTML/WebKit only + +if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + } +} + +Position.page = function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; +} + +// elements with display:none don't return an offsetParent, +// fall back to manual calculation +Position.offsetParent = function(element) { + if(element.offsetParent) return element.offsetParent; + if(element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element,'position')!='static') + return element; + + return document.body; +} + +Position.clone = function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent==document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + "px"; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + "px"; + if(options.setWidth) target.style.width = source.offsetWidth + "px"; + if(options.setHeight) target.style.height = source.offsetHeight + "px"; +} + +Position.absolutize = function(element) { + element = $(element); + if(element.style.position=='absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; +} + +Position.relativize = function(element) { + element = $(element); + if(element.style.position=='relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; +} + +/*--------------------------------------------------------------------------*/ + +Element.Class = { + // Element.toggleClass(element, className) toggles the class being on/off + // Element.toggleClass(element, className1, className2) toggles between both classes, + // defaulting to className1 if neither exist + toggle: function(element, className) { + if(Element.Class.has(element, className)) { + Element.Class.remove(element, className); + if(arguments.length == 3) Element.Class.add(element, arguments[2]); + } else { + Element.Class.add(element, className); + if(arguments.length == 3) Element.Class.remove(element, arguments[2]); + } + }, + + // gets space-delimited classnames of an element as an array + get: function(element) { + element = $(element); + return element.className.split(' '); + }, + + // functions adapted from original functions by Gavin Kistner + remove: function(element) { + element = $(element); + var regEx; + for(var i = 1; i < arguments.length; i++) { + regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)", 'g'); + element.className = element.className.replace(regEx, '') + } + }, + + add: function(element) { + element = $(element); + for(var i = 1; i < arguments.length; i++) { + Element.Class.remove(element, arguments[i]); + element.className += (element.className.length > 0 ? ' ' : '') + arguments[i]; + } + }, + + // returns true if all given classes exist in said element + has: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + if((typeof arguments[i] == 'object') && + (arguments[i].constructor == Array)) { + for(var j = 0; j < arguments[i].length; j++) { + regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)"); + if(!regEx.test(element.className)) return false; + } + } else { + regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)"); + if(!regEx.test(element.className)) return false; + } + } + return true; + }, + + // expects arrays of strings and/or strings as optional paramters + // Element.Class.has_any(element, ['classA','classB','classC'], 'classD') + has_any: function(element) { + element = $(element); + if(!element || !element.className) return false; + var regEx; + for(var i = 1; i < arguments.length; i++) { + if((typeof arguments[i] == 'object') && + (arguments[i].constructor == Array)) { + for(var j = 0; j < arguments[i].length; j++) { + regEx = new RegExp("(^|\\s)" + arguments[i][j] + "(\\s|$)"); + if(regEx.test(element.className)) return true; + } + } else { + regEx = new RegExp("(^|\\s)" + arguments[i] + "(\\s|$)"); + if(regEx.test(element.className)) return true; + } + } + return false; + }, + + childrenWith: function(element, className) { + var children = $(element).getElementsByTagName('*'); + var elements = new Array(); + + for (var i = 0; i < children.length; i++) + if (Element.Class.has(children[i], className)) + elements.push(children[i]); + + return elements; + } +}
\ No newline at end of file |