##// END OF EJS Templates
Upgraded to Prototype 1.6.0.1....
Jean-Philippe Lang -
r1580:75a5dbd01dd4
parent child
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -1,218 +1,218
1 1 /* redMine - project management software
2 2 Copyright (C) 2006-2008 Jean-Philippe Lang */
3 3
4 4 var observingContextMenuClick;
5 5
6 6 ContextMenu = Class.create();
7 7 ContextMenu.prototype = {
8 8 initialize: function (url) {
9 9 this.url = url;
10 10
11 11 // prevent selection when using Ctrl/Shit key
12 12 var tables = $$('table.issues');
13 13 for (i=0; i<tables.length; i++) {
14 14 tables[i].onselectstart = function () { return false; } // ie
15 15 tables[i].onmousedown = function () { return false; } // mozilla
16 16 }
17 17
18 18 if (!observingContextMenuClick) {
19 19 Event.observe(document, 'click', this.Click.bindAsEventListener(this));
20 20 Event.observe(document, (window.opera ? 'click' : 'contextmenu'), this.RightClick.bindAsEventListener(this));
21 21 observingContextMenuClick = true;
22 22 }
23 23
24 24 this.unselectAll();
25 25 this.lastSelected = null;
26 26 },
27 27
28 28 RightClick: function(e) {
29 29 this.hideMenu();
30 30 // do not show the context menu on links
31 if (Event.findElement(e, 'a') != document) { return; }
31 if (Event.findElement(e, 'a') != document && Event.findElement(e, 'a') != undefined) { return; }
32 32 // right-click simulated by Alt+Click with Opera
33 33 if (window.opera && !e.altKey) { return; }
34 34 var tr = Event.findElement(e, 'tr');
35 if ((tr == document) || !tr.hasClassName('hascontextmenu')) { return; }
35 if (tr == document || tr == undefined || !tr.hasClassName('hascontextmenu')) { return; }
36 36 Event.stop(e);
37 37 if (!this.isSelected(tr)) {
38 38 this.unselectAll();
39 39 this.addSelection(tr);
40 40 this.lastSelected = tr;
41 41 }
42 42 this.showMenu(e);
43 43 },
44 44
45 45 Click: function(e) {
46 46 this.hideMenu();
47 if (Event.findElement(e, 'a') != document) { return; }
47 if (Event.findElement(e, 'a') != document && Event.findElement(e, 'a') != undefined ) { return; }
48 48 if (window.opera && e.altKey) { return; }
49 49 if (Event.isLeftClick(e) || (navigator.appVersion.match(/\bMSIE\b/))) {
50 50 var tr = Event.findElement(e, 'tr');
51 51 if (tr!=document && tr.hasClassName('hascontextmenu')) {
52 52 // a row was clicked, check if the click was on checkbox
53 53 var box = Event.findElement(e, 'input');
54 if (box!=document) {
54 if (box!=document && box!=undefined) {
55 55 // a checkbox may be clicked
56 56 if (box.checked) {
57 57 tr.addClassName('context-menu-selection');
58 58 } else {
59 59 tr.removeClassName('context-menu-selection');
60 60 }
61 61 } else {
62 62 if (e.ctrlKey) {
63 63 this.toggleSelection(tr);
64 64 } else if (e.shiftKey) {
65 65 if (this.lastSelected != null) {
66 66 var toggling = false;
67 67 var rows = $$('.hascontextmenu');
68 68 for (i=0; i<rows.length; i++) {
69 69 if (toggling || rows[i]==tr) {
70 70 this.addSelection(rows[i]);
71 71 }
72 72 if (rows[i]==tr || rows[i]==this.lastSelected) {
73 73 toggling = !toggling;
74 74 }
75 75 }
76 76 } else {
77 77 this.addSelection(tr);
78 78 }
79 79 } else {
80 80 this.unselectAll();
81 81 this.addSelection(tr);
82 82 }
83 83 this.lastSelected = tr;
84 84 }
85 85 } else {
86 86 // click is outside the rows
87 87 var t = Event.findElement(e, 'a');
88 88 if ((t != document) && (Element.hasClassName(t, 'disabled') || Element.hasClassName(t, 'submenu'))) {
89 89 Event.stop(e);
90 90 }
91 91 }
92 92 }
93 93 },
94 94
95 95 showMenu: function(e) {
96 96 var mouse_x = Event.pointerX(e);
97 97 var mouse_y = Event.pointerY(e);
98 98 var render_x = mouse_x;
99 99 var render_y = mouse_y;
100 100 var dims;
101 101 var menu_width;
102 102 var menu_height;
103 103 var window_width;
104 104 var window_height;
105 105 var max_width;
106 106 var max_height;
107 107
108 108 $('context-menu').style['left'] = (render_x + 'px');
109 109 $('context-menu').style['top'] = (render_y + 'px');
110 110 Element.update('context-menu', '');
111 111
112 112 new Ajax.Updater({success:'context-menu'}, this.url,
113 113 {asynchronous:true,
114 114 evalScripts:true,
115 115 parameters:Form.serialize(Event.findElement(e, 'form')),
116 116 onComplete:function(request){
117 117 dims = $('context-menu').getDimensions();
118 118 menu_width = dims.width;
119 119 menu_height = dims.height;
120 120 max_width = mouse_x + 2*menu_width;
121 121 max_height = mouse_y + menu_height;
122 122
123 123 var ws = window_size();
124 124 window_width = ws.width;
125 125 window_height = ws.height;
126 126
127 127 /* display the menu above and/or to the left of the click if needed */
128 128 if (max_width > window_width) {
129 129 render_x -= menu_width;
130 130 $('context-menu').addClassName('reverse-x');
131 131 } else {
132 132 $('context-menu').removeClassName('reverse-x');
133 133 }
134 134 if (max_height > window_height) {
135 135 render_y -= menu_height;
136 136 $('context-menu').addClassName('reverse-y');
137 137 } else {
138 138 $('context-menu').removeClassName('reverse-y');
139 139 }
140 140 if (render_x <= 0) render_x = 1;
141 141 if (render_y <= 0) render_y = 1;
142 142 $('context-menu').style['left'] = (render_x + 'px');
143 143 $('context-menu').style['top'] = (render_y + 'px');
144 144
145 145 Effect.Appear('context-menu', {duration: 0.20});
146 146 if (window.parseStylesheets) { window.parseStylesheets(); } // IE
147 147 }})
148 148 },
149 149
150 150 hideMenu: function() {
151 151 Element.hide('context-menu');
152 152 },
153 153
154 154 addSelection: function(tr) {
155 155 tr.addClassName('context-menu-selection');
156 156 this.checkSelectionBox(tr, true);
157 157 },
158 158
159 159 toggleSelection: function(tr) {
160 160 if (this.isSelected(tr)) {
161 161 this.removeSelection(tr);
162 162 } else {
163 163 this.addSelection(tr);
164 164 }
165 165 },
166 166
167 167 removeSelection: function(tr) {
168 168 tr.removeClassName('context-menu-selection');
169 169 this.checkSelectionBox(tr, false);
170 170 },
171 171
172 172 unselectAll: function() {
173 173 var rows = $$('.hascontextmenu');
174 174 for (i=0; i<rows.length; i++) {
175 175 this.removeSelection(rows[i]);
176 176 }
177 177 },
178 178
179 179 checkSelectionBox: function(tr, checked) {
180 180 var inputs = Element.getElementsBySelector(tr, 'input');
181 181 if (inputs.length > 0) { inputs[0].checked = checked; }
182 182 },
183 183
184 184 isSelected: function(tr) {
185 185 return Element.hasClassName(tr, 'context-menu-selection');
186 186 }
187 187 }
188 188
189 189 function toggleIssuesSelection(el) {
190 190 var boxes = el.getElementsBySelector('input[type=checkbox]');
191 191 var all_checked = true;
192 192 for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } }
193 193 for (i = 0; i < boxes.length; i++) {
194 194 if (all_checked) {
195 195 boxes[i].checked = false;
196 196 boxes[i].up('tr').removeClassName('context-menu-selection');
197 197 } else if (boxes[i].checked == false) {
198 198 boxes[i].checked = true;
199 199 boxes[i].up('tr').addClassName('context-menu-selection');
200 200 }
201 201 }
202 202 }
203 203
204 204 function window_size() {
205 205 var w;
206 206 var h;
207 207 if (window.innerWidth) {
208 208 w = window.innerWidth;
209 209 h = window.innerHeight;
210 210 } else if (document.documentElement) {
211 211 w = document.documentElement.clientWidth;
212 212 h = document.documentElement.clientHeight;
213 213 } else {
214 214 w = document.body.clientWidth;
215 215 h = document.body.clientHeight;
216 216 }
217 217 return {width: w, height: h};
218 218 }
This diff has been collapsed as it changes many lines, (838 lines changed) Show them Hide them
@@ -1,833 +1,963
1 // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 // (c) 2005, 2006 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3 // (c) 2005, 2006 Jon Tirsen (http://www.tirsen.com)
1 // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 // (c) 2005-2007 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3 // (c) 2005-2007 Jon Tirsen (http://www.tirsen.com)
4 4 // Contributors:
5 5 // Richard Livsey
6 6 // Rahul Bhargava
7 7 // Rob Wills
8 8 //
9 9 // script.aculo.us is freely distributable under the terms of an MIT-style license.
10 10 // For details, see the script.aculo.us web site: http://script.aculo.us/
11 11
12 12 // Autocompleter.Base handles all the autocompletion functionality
13 13 // that's independent of the data source for autocompletion. This
14 14 // includes drawing the autocompletion menu, observing keyboard
15 15 // and mouse events, and similar.
16 16 //
17 17 // Specific autocompleters need to provide, at the very least,
18 18 // a getUpdatedChoices function that will be invoked every time
19 19 // the text inside the monitored textbox changes. This method
20 20 // should get the text for which to provide autocompletion by
21 21 // invoking this.getToken(), NOT by directly accessing
22 22 // this.element.value. This is to allow incremental tokenized
23 23 // autocompletion. Specific auto-completion logic (AJAX, etc)
24 24 // belongs in getUpdatedChoices.
25 25 //
26 26 // Tokenized incremental autocompletion is enabled automatically
27 27 // when an autocompleter is instantiated with the 'tokens' option
28 28 // in the options parameter, e.g.:
29 29 // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
30 30 // will incrementally autocomplete with a comma as the token.
31 31 // Additionally, ',' in the above example can be replaced with
32 32 // a token array, e.g. { tokens: [',', '\n'] } which
33 33 // enables autocompletion on multiple tokens. This is most
34 34 // useful when one of the tokens is \n (a newline), as it
35 35 // allows smart autocompletion after linebreaks.
36 36
37 37 if(typeof Effect == 'undefined')
38 38 throw("controls.js requires including script.aculo.us' effects.js library");
39 39
40 var Autocompleter = {}
41 Autocompleter.Base = function() {};
42 Autocompleter.Base.prototype = {
40 var Autocompleter = { }
41 Autocompleter.Base = Class.create({
43 42 baseInitialize: function(element, update, options) {
44 this.element = $(element);
43 element = $(element)
44 this.element = element;
45 45 this.update = $(update);
46 46 this.hasFocus = false;
47 47 this.changed = false;
48 48 this.active = false;
49 49 this.index = 0;
50 50 this.entryCount = 0;
51 this.oldElementValue = this.element.value;
51 52
52 53 if(this.setOptions)
53 54 this.setOptions(options);
54 55 else
55 this.options = options || {};
56 this.options = options || { };
56 57
57 58 this.options.paramName = this.options.paramName || this.element.name;
58 59 this.options.tokens = this.options.tokens || [];
59 60 this.options.frequency = this.options.frequency || 0.4;
60 61 this.options.minChars = this.options.minChars || 1;
61 62 this.options.onShow = this.options.onShow ||
62 63 function(element, update){
63 64 if(!update.style.position || update.style.position=='absolute') {
64 65 update.style.position = 'absolute';
65 66 Position.clone(element, update, {
66 67 setHeight: false,
67 68 offsetTop: element.offsetHeight
68 69 });
69 70 }
70 71 Effect.Appear(update,{duration:0.15});
71 72 };
72 73 this.options.onHide = this.options.onHide ||
73 74 function(element, update){ new Effect.Fade(update,{duration:0.15}) };
74 75
75 76 if(typeof(this.options.tokens) == 'string')
76 77 this.options.tokens = new Array(this.options.tokens);
78 // Force carriage returns as token delimiters anyway
79 if (!this.options.tokens.include('\n'))
80 this.options.tokens.push('\n');
77 81
78 82 this.observer = null;
79 83
80 84 this.element.setAttribute('autocomplete','off');
81 85
82 86 Element.hide(this.update);
83 87
84 Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
85 Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
88 Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
89 Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
86 90 },
87 91
88 92 show: function() {
89 93 if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
90 94 if(!this.iefix &&
91 (navigator.appVersion.indexOf('MSIE')>0) &&
92 (navigator.userAgent.indexOf('Opera')<0) &&
95 (Prototype.Browser.IE) &&
93 96 (Element.getStyle(this.update, 'position')=='absolute')) {
94 97 new Insertion.After(this.update,
95 98 '<iframe id="' + this.update.id + '_iefix" '+
96 99 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
97 100 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
98 101 this.iefix = $(this.update.id+'_iefix');
99 102 }
100 103 if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
101 104 },
102 105
103 106 fixIEOverlapping: function() {
104 107 Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
105 108 this.iefix.style.zIndex = 1;
106 109 this.update.style.zIndex = 2;
107 110 Element.show(this.iefix);
108 111 },
109 112
110 113 hide: function() {
111 114 this.stopIndicator();
112 115 if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
113 116 if(this.iefix) Element.hide(this.iefix);
114 117 },
115 118
116 119 startIndicator: function() {
117 120 if(this.options.indicator) Element.show(this.options.indicator);
118 121 },
119 122
120 123 stopIndicator: function() {
121 124 if(this.options.indicator) Element.hide(this.options.indicator);
122 125 },
123 126
124 127 onKeyPress: function(event) {
125 128 if(this.active)
126 129 switch(event.keyCode) {
127 130 case Event.KEY_TAB:
128 131 case Event.KEY_RETURN:
129 132 this.selectEntry();
130 133 Event.stop(event);
131 134 case Event.KEY_ESC:
132 135 this.hide();
133 136 this.active = false;
134 137 Event.stop(event);
135 138 return;
136 139 case Event.KEY_LEFT:
137 140 case Event.KEY_RIGHT:
138 141 return;
139 142 case Event.KEY_UP:
140 143 this.markPrevious();
141 144 this.render();
142 if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
145 Event.stop(event);
143 146 return;
144 147 case Event.KEY_DOWN:
145 148 this.markNext();
146 149 this.render();
147 if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
150 Event.stop(event);
148 151 return;
149 152 }
150 153 else
151 154 if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
152 (navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
155 (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;
153 156
154 157 this.changed = true;
155 158 this.hasFocus = true;
156 159
157 160 if(this.observer) clearTimeout(this.observer);
158 161 this.observer =
159 162 setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
160 163 },
161 164
162 165 activate: function() {
163 166 this.changed = false;
164 167 this.hasFocus = true;
165 168 this.getUpdatedChoices();
166 169 },
167 170
168 171 onHover: function(event) {
169 172 var element = Event.findElement(event, 'LI');
170 173 if(this.index != element.autocompleteIndex)
171 174 {
172 175 this.index = element.autocompleteIndex;
173 176 this.render();
174 177 }
175 178 Event.stop(event);
176 179 },
177 180
178 181 onClick: function(event) {
179 182 var element = Event.findElement(event, 'LI');
180 183 this.index = element.autocompleteIndex;
181 184 this.selectEntry();
182 185 this.hide();
183 186 },
184 187
185 188 onBlur: function(event) {
186 189 // needed to make click events working
187 190 setTimeout(this.hide.bind(this), 250);
188 191 this.hasFocus = false;
189 192 this.active = false;
190 193 },
191 194
192 195 render: function() {
193 196 if(this.entryCount > 0) {
194 197 for (var i = 0; i < this.entryCount; i++)
195 198 this.index==i ?
196 199 Element.addClassName(this.getEntry(i),"selected") :
197 200 Element.removeClassName(this.getEntry(i),"selected");
198
199 201 if(this.hasFocus) {
200 202 this.show();
201 203 this.active = true;
202 204 }
203 205 } else {
204 206 this.active = false;
205 207 this.hide();
206 208 }
207 209 },
208 210
209 211 markPrevious: function() {
210 212 if(this.index > 0) this.index--
211 213 else this.index = this.entryCount-1;
212 214 this.getEntry(this.index).scrollIntoView(true);
213 215 },
214 216
215 217 markNext: function() {
216 218 if(this.index < this.entryCount-1) this.index++
217 219 else this.index = 0;
218 220 this.getEntry(this.index).scrollIntoView(false);
219 221 },
220 222
221 223 getEntry: function(index) {
222 224 return this.update.firstChild.childNodes[index];
223 225 },
224 226
225 227 getCurrentEntry: function() {
226 228 return this.getEntry(this.index);
227 229 },
228 230
229 231 selectEntry: function() {
230 232 this.active = false;
231 233 this.updateElement(this.getCurrentEntry());
232 234 },
233 235
234 236 updateElement: function(selectedElement) {
235 237 if (this.options.updateElement) {
236 238 this.options.updateElement(selectedElement);
237 239 return;
238 240 }
239 241 var value = '';
240 242 if (this.options.select) {
241 var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
243 var nodes = $(selectedElement).select('.' + this.options.select) || [];
242 244 if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
243 245 } else
244 246 value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
245 247
246 var lastTokenPos = this.findLastToken();
247 if (lastTokenPos != -1) {
248 var newValue = this.element.value.substr(0, lastTokenPos + 1);
249 var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
248 var bounds = this.getTokenBounds();
249 if (bounds[0] != -1) {
250 var newValue = this.element.value.substr(0, bounds[0]);
251 var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
250 252 if (whitespace)
251 253 newValue += whitespace[0];
252 this.element.value = newValue + value;
254 this.element.value = newValue + value + this.element.value.substr(bounds[1]);
253 255 } else {
254 256 this.element.value = value;
255 257 }
258 this.oldElementValue = this.element.value;
256 259 this.element.focus();
257 260
258 261 if (this.options.afterUpdateElement)
259 262 this.options.afterUpdateElement(this.element, selectedElement);
260 263 },
261 264
262 265 updateChoices: function(choices) {
263 266 if(!this.changed && this.hasFocus) {
264 267 this.update.innerHTML = choices;
265 268 Element.cleanWhitespace(this.update);
266 269 Element.cleanWhitespace(this.update.down());
267 270
268 271 if(this.update.firstChild && this.update.down().childNodes) {
269 272 this.entryCount =
270 273 this.update.down().childNodes.length;
271 274 for (var i = 0; i < this.entryCount; i++) {
272 275 var entry = this.getEntry(i);
273 276 entry.autocompleteIndex = i;
274 277 this.addObservers(entry);
275 278 }
276 279 } else {
277 280 this.entryCount = 0;
278 281 }
279 282
280 283 this.stopIndicator();
281 284 this.index = 0;
282 285
283 286 if(this.entryCount==1 && this.options.autoSelect) {
284 287 this.selectEntry();
285 288 this.hide();
286 289 } else {
287 290 this.render();
288 291 }
289 292 }
290 293 },
291 294
292 295 addObservers: function(element) {
293 296 Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
294 297 Event.observe(element, "click", this.onClick.bindAsEventListener(this));
295 298 },
296 299
297 300 onObserverEvent: function() {
298 301 this.changed = false;
302 this.tokenBounds = null;
299 303 if(this.getToken().length>=this.options.minChars) {
300 this.startIndicator();
301 304 this.getUpdatedChoices();
302 305 } else {
303 306 this.active = false;
304 307 this.hide();
305 308 }
309 this.oldElementValue = this.element.value;
306 310 },
307 311
308 312 getToken: function() {
309 var tokenPos = this.findLastToken();
310 if (tokenPos != -1)
311 var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
312 else
313 var ret = this.element.value;
314
315 return /\n/.test(ret) ? '' : ret;
316 },
317
318 findLastToken: function() {
319 var lastTokenPos = -1;
320
321 for (var i=0; i<this.options.tokens.length; i++) {
322 var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
323 if (thisTokenPos > lastTokenPos)
324 lastTokenPos = thisTokenPos;
313 var bounds = this.getTokenBounds();
314 return this.element.value.substring(bounds[0], bounds[1]).strip();
315 },
316
317 getTokenBounds: function() {
318 if (null != this.tokenBounds) return this.tokenBounds;
319 var value = this.element.value;
320 if (value.strip().empty()) return [-1, 0];
321 var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
322 var offset = (diff == this.oldElementValue.length ? 1 : 0);
323 var prevTokenPos = -1, nextTokenPos = value.length;
324 var tp;
325 for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
326 tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
327 if (tp > prevTokenPos) prevTokenPos = tp;
328 tp = value.indexOf(this.options.tokens[index], diff + offset);
329 if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
325 330 }
326 return lastTokenPos;
331 return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
327 332 }
328 }
333 });
334
335 Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
336 var boundary = Math.min(newS.length, oldS.length);
337 for (var index = 0; index < boundary; ++index)
338 if (newS[index] != oldS[index])
339 return index;
340 return boundary;
341 };
329 342
330 Ajax.Autocompleter = Class.create();
331 Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
343 Ajax.Autocompleter = Class.create(Autocompleter.Base, {
332 344 initialize: function(element, update, url, options) {
333 345 this.baseInitialize(element, update, options);
334 346 this.options.asynchronous = true;
335 347 this.options.onComplete = this.onComplete.bind(this);
336 348 this.options.defaultParams = this.options.parameters || null;
337 349 this.url = url;
338 350 },
339 351
340 352 getUpdatedChoices: function() {
341 entry = encodeURIComponent(this.options.paramName) + '=' +
353 this.startIndicator();
354
355 var entry = encodeURIComponent(this.options.paramName) + '=' +
342 356 encodeURIComponent(this.getToken());
343 357
344 358 this.options.parameters = this.options.callback ?
345 359 this.options.callback(this.element, entry) : entry;
346 360
347 361 if(this.options.defaultParams)
348 362 this.options.parameters += '&' + this.options.defaultParams;
349
363
350 364 new Ajax.Request(this.url, this.options);
351 365 },
352 366
353 367 onComplete: function(request) {
354 368 this.updateChoices(request.responseText);
355 369 }
356
357 370 });
358 371
359 372 // The local array autocompleter. Used when you'd prefer to
360 373 // inject an array of autocompletion options into the page, rather
361 374 // than sending out Ajax queries, which can be quite slow sometimes.
362 375 //
363 376 // The constructor takes four parameters. The first two are, as usual,
364 377 // the id of the monitored textbox, and id of the autocompletion menu.
365 378 // The third is the array you want to autocomplete from, and the fourth
366 379 // is the options block.
367 380 //
368 381 // Extra local autocompletion options:
369 382 // - choices - How many autocompletion choices to offer
370 383 //
371 384 // - partialSearch - If false, the autocompleter will match entered
372 385 // text only at the beginning of strings in the
373 386 // autocomplete array. Defaults to true, which will
374 387 // match text at the beginning of any *word* in the
375 388 // strings in the autocomplete array. If you want to
376 389 // search anywhere in the string, additionally set
377 390 // the option fullSearch to true (default: off).
378 391 //
379 392 // - fullSsearch - Search anywhere in autocomplete array strings.
380 393 //
381 394 // - partialChars - How many characters to enter before triggering
382 395 // a partial match (unlike minChars, which defines
383 396 // how many characters are required to do any match
384 397 // at all). Defaults to 2.
385 398 //
386 399 // - ignoreCase - Whether to ignore case when autocompleting.
387 400 // Defaults to true.
388 401 //
389 402 // It's possible to pass in a custom function as the 'selector'
390 403 // option, if you prefer to write your own autocompletion logic.
391 404 // In that case, the other options above will not apply unless
392 405 // you support them.
393 406
394 Autocompleter.Local = Class.create();
395 Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
407 Autocompleter.Local = Class.create(Autocompleter.Base, {
396 408 initialize: function(element, update, array, options) {
397 409 this.baseInitialize(element, update, options);
398 410 this.options.array = array;
399 411 },
400 412
401 413 getUpdatedChoices: function() {
402 414 this.updateChoices(this.options.selector(this));
403 415 },
404 416
405 417 setOptions: function(options) {
406 418 this.options = Object.extend({
407 419 choices: 10,
408 420 partialSearch: true,
409 421 partialChars: 2,
410 422 ignoreCase: true,
411 423 fullSearch: false,
412 424 selector: function(instance) {
413 425 var ret = []; // Beginning matches
414 426 var partial = []; // Inside matches
415 427 var entry = instance.getToken();
416 428 var count = 0;
417 429
418 430 for (var i = 0; i < instance.options.array.length &&
419 431 ret.length < instance.options.choices ; i++) {
420 432
421 433 var elem = instance.options.array[i];
422 434 var foundPos = instance.options.ignoreCase ?
423 435 elem.toLowerCase().indexOf(entry.toLowerCase()) :
424 436 elem.indexOf(entry);
425 437
426 438 while (foundPos != -1) {
427 439 if (foundPos == 0 && elem.length != entry.length) {
428 440 ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
429 441 elem.substr(entry.length) + "</li>");
430 442 break;
431 443 } else if (entry.length >= instance.options.partialChars &&
432 444 instance.options.partialSearch && foundPos != -1) {
433 445 if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
434 446 partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
435 447 elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
436 448 foundPos + entry.length) + "</li>");
437 449 break;
438 450 }
439 451 }
440 452
441 453 foundPos = instance.options.ignoreCase ?
442 454 elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
443 455 elem.indexOf(entry, foundPos + 1);
444 456
445 457 }
446 458 }
447 459 if (partial.length)
448 460 ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
449 461 return "<ul>" + ret.join('') + "</ul>";
450 462 }
451 }, options || {});
463 }, options || { });
452 464 }
453 465 });
454 466
455 // AJAX in-place editor
456 //
457 // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
467 // AJAX in-place editor and collection editor
468 // Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).
458 469
459 470 // Use this if you notice weird scrolling problems on some browsers,
460 471 // the DOM might be a bit confused when this gets called so do this
461 472 // waits 1 ms (with setTimeout) until it does the activation
462 473 Field.scrollFreeActivate = function(field) {
463 474 setTimeout(function() {
464 475 Field.activate(field);
465 476 }, 1);
466 477 }
467 478
468 Ajax.InPlaceEditor = Class.create();
469 Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
470 Ajax.InPlaceEditor.prototype = {
479 Ajax.InPlaceEditor = Class.create({
471 480 initialize: function(element, url, options) {
472 481 this.url = url;
473 this.element = $(element);
474
475 this.options = Object.extend({
476 paramName: "value",
477 okButton: true,
478 okText: "ok",
479 cancelLink: true,
480 cancelText: "cancel",
481 savingText: "Saving...",
482 clickToEditText: "Click to edit",
483 okText: "ok",
484 rows: 1,
485 onComplete: function(transport, element) {
486 new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
487 },
488 onFailure: function(transport) {
489 alert("Error communicating with the server: " + transport.responseText.stripTags());
490 },
491 callback: function(form) {
492 return Form.serialize(form);
493 },
494 handleLineBreaks: true,
495 loadingText: 'Loading...',
496 savingClassName: 'inplaceeditor-saving',
497 loadingClassName: 'inplaceeditor-loading',
498 formClassName: 'inplaceeditor-form',
499 highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
500 highlightendcolor: "#FFFFFF",
501 externalControl: null,
502 submitOnBlur: false,
503 ajaxOptions: {},
504 evalScripts: false
505 }, options || {});
506
507 if(!this.options.formId && this.element.id) {
508 this.options.formId = this.element.id + "-inplaceeditor";
509 if ($(this.options.formId)) {
510 // there's already a form with that name, don't specify an id
511 this.options.formId = null;
512 }
482 this.element = element = $(element);
483 this.prepareOptions();
484 this._controls = { };
485 arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
486 Object.extend(this.options, options || { });
487 if (!this.options.formId && this.element.id) {
488 this.options.formId = this.element.id + '-inplaceeditor';
489 if ($(this.options.formId))
490 this.options.formId = '';
513 491 }
514
515 if (this.options.externalControl) {
492 if (this.options.externalControl)
516 493 this.options.externalControl = $(this.options.externalControl);
517 }
518
519 this.originalBackground = Element.getStyle(this.element, 'background-color');
520 if (!this.originalBackground) {
521 this.originalBackground = "transparent";
522 }
523
494 if (!this.options.externalControl)
495 this.options.externalControlOnly = false;
496 this._originalBackground = this.element.getStyle('background-color') || 'transparent';
524 497 this.element.title = this.options.clickToEditText;
525
526 this.onclickListener = this.enterEditMode.bindAsEventListener(this);
527 this.mouseoverListener = this.enterHover.bindAsEventListener(this);
528 this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
529 Event.observe(this.element, 'click', this.onclickListener);
530 Event.observe(this.element, 'mouseover', this.mouseoverListener);
531 Event.observe(this.element, 'mouseout', this.mouseoutListener);
532 if (this.options.externalControl) {
533 Event.observe(this.options.externalControl, 'click', this.onclickListener);
534 Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
535 Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
498 this._boundCancelHandler = this.handleFormCancellation.bind(this);
499 this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
500 this._boundFailureHandler = this.handleAJAXFailure.bind(this);
501 this._boundSubmitHandler = this.handleFormSubmission.bind(this);
502 this._boundWrapperHandler = this.wrapUp.bind(this);
503 this.registerListeners();
504 },
505 checkForEscapeOrReturn: function(e) {
506 if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
507 if (Event.KEY_ESC == e.keyCode)
508 this.handleFormCancellation(e);
509 else if (Event.KEY_RETURN == e.keyCode)
510 this.handleFormSubmission(e);
511 },
512 createControl: function(mode, handler, extraClasses) {
513 var control = this.options[mode + 'Control'];
514 var text = this.options[mode + 'Text'];
515 if ('button' == control) {
516 var btn = document.createElement('input');
517 btn.type = 'submit';
518 btn.value = text;
519 btn.className = 'editor_' + mode + '_button';
520 if ('cancel' == mode)
521 btn.onclick = this._boundCancelHandler;
522 this._form.appendChild(btn);
523 this._controls[mode] = btn;
524 } else if ('link' == control) {
525 var link = document.createElement('a');
526 link.href = '#';
527 link.appendChild(document.createTextNode(text));
528 link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
529 link.className = 'editor_' + mode + '_link';
530 if (extraClasses)
531 link.className += ' ' + extraClasses;
532 this._form.appendChild(link);
533 this._controls[mode] = link;
536 534 }
537 535 },
538 enterEditMode: function(evt) {
539 if (this.saving) return;
540 if (this.editing) return;
541 this.editing = true;
542 this.onEnterEditMode();
543 if (this.options.externalControl) {
544 Element.hide(this.options.externalControl);
545 }
546 Element.hide(this.element);
547 this.createForm();
548 this.element.parentNode.insertBefore(this.form, this.element);
549 if (!this.options.loadTextURL) Field.scrollFreeActivate(this.editField);
550 // stop the event to avoid a page refresh in Safari
551 if (evt) {
552 Event.stop(evt);
553 }
554 return false;
555 },
556 createForm: function() {
557 this.form = document.createElement("form");
558 this.form.id = this.options.formId;
559 Element.addClassName(this.form, this.options.formClassName)
560 this.form.onsubmit = this.onSubmit.bind(this);
561
562 this.createEditField();
563
564 if (this.options.textarea) {
565 var br = document.createElement("br");
566 this.form.appendChild(br);
567 }
568
569 if (this.options.okButton) {
570 okButton = document.createElement("input");
571 okButton.type = "submit";
572 okButton.value = this.options.okText;
573 okButton.className = 'editor_ok_button';
574 this.form.appendChild(okButton);
575 }
576
577 if (this.options.cancelLink) {
578 cancelLink = document.createElement("a");
579 cancelLink.href = "#";
580 cancelLink.appendChild(document.createTextNode(this.options.cancelText));
581 cancelLink.onclick = this.onclickCancel.bind(this);
582 cancelLink.className = 'editor_cancel';
583 this.form.appendChild(cancelLink);
584 }
585 },
586 hasHTMLLineBreaks: function(string) {
587 if (!this.options.handleLineBreaks) return false;
588 return string.match(/<br/i) || string.match(/<p>/i);
589 },
590 convertHTMLLineBreaks: function(string) {
591 return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
592 },
593 536 createEditField: function() {
594 var text;
595 if(this.options.loadTextURL) {
596 text = this.options.loadingText;
597 } else {
598 text = this.getText();
599 }
600
601 var obj = this;
602
603 if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
604 this.options.textarea = false;
605 var textField = document.createElement("input");
606 textField.obj = this;
607 textField.type = "text";
608 textField.name = this.options.paramName;
609 textField.value = text;
610 textField.style.backgroundColor = this.options.highlightcolor;
611 textField.className = 'editor_field';
537 var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
538 var fld;
539 if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
540 fld = document.createElement('input');
541 fld.type = 'text';
612 542 var size = this.options.size || this.options.cols || 0;
613 if (size != 0) textField.size = size;
614 if (this.options.submitOnBlur)
615 textField.onblur = this.onSubmit.bind(this);
616 this.editField = textField;
543 if (0 < size) fld.size = size;
617 544 } else {
618 this.options.textarea = true;
619 var textArea = document.createElement("textarea");
620 textArea.obj = this;
621 textArea.name = this.options.paramName;
622 textArea.value = this.convertHTMLLineBreaks(text);
623 textArea.rows = this.options.rows;
624 textArea.cols = this.options.cols || 40;
625 textArea.className = 'editor_field';
626 if (this.options.submitOnBlur)
627 textArea.onblur = this.onSubmit.bind(this);
628 this.editField = textArea;
545 fld = document.createElement('textarea');
546 fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
547 fld.cols = this.options.cols || 40;
629 548 }
630
631 if(this.options.loadTextURL) {
549 fld.name = this.options.paramName;
550 fld.value = text; // No HTML breaks conversion anymore
551 fld.className = 'editor_field';
552 if (this.options.submitOnBlur)
553 fld.onblur = this._boundSubmitHandler;
554 this._controls.editor = fld;
555 if (this.options.loadTextURL)
632 556 this.loadExternalText();
633 }
634 this.form.appendChild(this.editField);
557 this._form.appendChild(this._controls.editor);
558 },
559 createForm: function() {
560 var ipe = this;
561 function addText(mode, condition) {
562 var text = ipe.options['text' + mode + 'Controls'];
563 if (!text || condition === false) return;
564 ipe._form.appendChild(document.createTextNode(text));
565 };
566 this._form = $(document.createElement('form'));
567 this._form.id = this.options.formId;
568 this._form.addClassName(this.options.formClassName);
569 this._form.onsubmit = this._boundSubmitHandler;
570 this.createEditField();
571 if ('textarea' == this._controls.editor.tagName.toLowerCase())
572 this._form.appendChild(document.createElement('br'));
573 if (this.options.onFormCustomization)
574 this.options.onFormCustomization(this, this._form);
575 addText('Before', this.options.okControl || this.options.cancelControl);
576 this.createControl('ok', this._boundSubmitHandler);
577 addText('Between', this.options.okControl && this.options.cancelControl);
578 this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
579 addText('After', this.options.okControl || this.options.cancelControl);
580 },
581 destroy: function() {
582 if (this._oldInnerHTML)
583 this.element.innerHTML = this._oldInnerHTML;
584 this.leaveEditMode();
585 this.unregisterListeners();
586 },
587 enterEditMode: function(e) {
588 if (this._saving || this._editing) return;
589 this._editing = true;
590 this.triggerCallback('onEnterEditMode');
591 if (this.options.externalControl)
592 this.options.externalControl.hide();
593 this.element.hide();
594 this.createForm();
595 this.element.parentNode.insertBefore(this._form, this.element);
596 if (!this.options.loadTextURL)
597 this.postProcessEditField();
598 if (e) Event.stop(e);
599 },
600 enterHover: function(e) {
601 if (this.options.hoverClassName)
602 this.element.addClassName(this.options.hoverClassName);
603 if (this._saving) return;
604 this.triggerCallback('onEnterHover');
635 605 },
636 606 getText: function() {
637 607 return this.element.innerHTML;
638 608 },
639 loadExternalText: function() {
640 Element.addClassName(this.form, this.options.loadingClassName);
641 this.editField.disabled = true;
642 new Ajax.Request(
643 this.options.loadTextURL,
644 Object.extend({
645 asynchronous: true,
646 onComplete: this.onLoadedExternalText.bind(this)
647 }, this.options.ajaxOptions)
648 );
649 },
650 onLoadedExternalText: function(transport) {
651 Element.removeClassName(this.form, this.options.loadingClassName);
652 this.editField.disabled = false;
653 this.editField.value = transport.responseText.stripTags();
654 Field.scrollFreeActivate(this.editField);
655 },
656 onclickCancel: function() {
657 this.onComplete();
658 this.leaveEditMode();
659 return false;
660 },
661 onFailure: function(transport) {
662 this.options.onFailure(transport);
663 if (this.oldInnerHTML) {
664 this.element.innerHTML = this.oldInnerHTML;
665 this.oldInnerHTML = null;
609 handleAJAXFailure: function(transport) {
610 this.triggerCallback('onFailure', transport);
611 if (this._oldInnerHTML) {
612 this.element.innerHTML = this._oldInnerHTML;
613 this._oldInnerHTML = null;
666 614 }
667 return false;
668 615 },
669 onSubmit: function() {
670 // onLoading resets these so we need to save them away for the Ajax call
671 var form = this.form;
672 var value = this.editField.value;
673
674 // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
675 // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
676 // to be displayed indefinitely
677 this.onLoading();
678
679 if (this.options.evalScripts) {
680 new Ajax.Request(
681 this.url, Object.extend({
682 parameters: this.options.callback(form, value),
683 onComplete: this.onComplete.bind(this),
684 onFailure: this.onFailure.bind(this),
685 asynchronous:true,
686 evalScripts:true
687 }, this.options.ajaxOptions));
688 } else {
689 new Ajax.Updater(
690 { success: this.element,
691 // don't update on failure (this could be an option)
692 failure: null },
693 this.url, Object.extend({
694 parameters: this.options.callback(form, value),
695 onComplete: this.onComplete.bind(this),
696 onFailure: this.onFailure.bind(this)
697 }, this.options.ajaxOptions));
698 }
699 // stop the event to avoid a page refresh in Safari
700 if (arguments.length > 1) {
701 Event.stop(arguments[0]);
616 handleFormCancellation: function(e) {
617 this.wrapUp();
618 if (e) Event.stop(e);
619 },
620 handleFormSubmission: function(e) {
621 var form = this._form;
622 var value = $F(this._controls.editor);
623 this.prepareSubmission();
624 var params = this.options.callback(form, value) || '';
625 if (Object.isString(params))
626 params = params.toQueryParams();
627 params.editorId = this.element.id;
628 if (this.options.htmlResponse) {
629 var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
630 Object.extend(options, {
631 parameters: params,
632 onComplete: this._boundWrapperHandler,
633 onFailure: this._boundFailureHandler
634 });
635 new Ajax.Updater({ success: this.element }, this.url, options);
636 } else {
637 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
638 Object.extend(options, {
639 parameters: params,
640 onComplete: this._boundWrapperHandler,
641 onFailure: this._boundFailureHandler
642 });
643 new Ajax.Request(this.url, options);
702 644 }
703 return false;
645 if (e) Event.stop(e);
646 },
647 leaveEditMode: function() {
648 this.element.removeClassName(this.options.savingClassName);
649 this.removeForm();
650 this.leaveHover();
651 this.element.style.backgroundColor = this._originalBackground;
652 this.element.show();
653 if (this.options.externalControl)
654 this.options.externalControl.show();
655 this._saving = false;
656 this._editing = false;
657 this._oldInnerHTML = null;
658 this.triggerCallback('onLeaveEditMode');
659 },
660 leaveHover: function(e) {
661 if (this.options.hoverClassName)
662 this.element.removeClassName(this.options.hoverClassName);
663 if (this._saving) return;
664 this.triggerCallback('onLeaveHover');
704 665 },
705 onLoading: function() {
706 this.saving = true;
666 loadExternalText: function() {
667 this._form.addClassName(this.options.loadingClassName);
668 this._controls.editor.disabled = true;
669 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
670 Object.extend(options, {
671 parameters: 'editorId=' + encodeURIComponent(this.element.id),
672 onComplete: Prototype.emptyFunction,
673 onSuccess: function(transport) {
674 this._form.removeClassName(this.options.loadingClassName);
675 var text = transport.responseText;
676 if (this.options.stripLoadedTextTags)
677 text = text.stripTags();
678 this._controls.editor.value = text;
679 this._controls.editor.disabled = false;
680 this.postProcessEditField();
681 }.bind(this),
682 onFailure: this._boundFailureHandler
683 });
684 new Ajax.Request(this.options.loadTextURL, options);
685 },
686 postProcessEditField: function() {
687 var fpc = this.options.fieldPostCreation;
688 if (fpc)
689 $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
690 },
691 prepareOptions: function() {
692 this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
693 Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
694 [this._extraDefaultOptions].flatten().compact().each(function(defs) {
695 Object.extend(this.options, defs);
696 }.bind(this));
697 },
698 prepareSubmission: function() {
699 this._saving = true;
707 700 this.removeForm();
708 701 this.leaveHover();
709 702 this.showSaving();
710 703 },
704 registerListeners: function() {
705 this._listeners = { };
706 var listener;
707 $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
708 listener = this[pair.value].bind(this);
709 this._listeners[pair.key] = listener;
710 if (!this.options.externalControlOnly)
711 this.element.observe(pair.key, listener);
712 if (this.options.externalControl)
713 this.options.externalControl.observe(pair.key, listener);
714 }.bind(this));
715 },
716 removeForm: function() {
717 if (!this._form) return;
718 this._form.remove();
719 this._form = null;
720 this._controls = { };
721 },
711 722 showSaving: function() {
712 this.oldInnerHTML = this.element.innerHTML;
723 this._oldInnerHTML = this.element.innerHTML;
713 724 this.element.innerHTML = this.options.savingText;
714 Element.addClassName(this.element, this.options.savingClassName);
715 this.element.style.backgroundColor = this.originalBackground;
716 Element.show(this.element);
725 this.element.addClassName(this.options.savingClassName);
726 this.element.style.backgroundColor = this._originalBackground;
727 this.element.show();
717 728 },
718 removeForm: function() {
719 if(this.form) {
720 if (this.form.parentNode) Element.remove(this.form);
721 this.form = null;
729 triggerCallback: function(cbName, arg) {
730 if ('function' == typeof this.options[cbName]) {
731 this.options[cbName](this, arg);
722 732 }
723 733 },
724 enterHover: function() {
725 if (this.saving) return;
726 this.element.style.backgroundColor = this.options.highlightcolor;
727 if (this.effect) {
728 this.effect.cancel();
729 }
730 Element.addClassName(this.element, this.options.hoverClassName)
734 unregisterListeners: function() {
735 $H(this._listeners).each(function(pair) {
736 if (!this.options.externalControlOnly)
737 this.element.stopObserving(pair.key, pair.value);
738 if (this.options.externalControl)
739 this.options.externalControl.stopObserving(pair.key, pair.value);
740 }.bind(this));
731 741 },
732 leaveHover: function() {
733 if (this.options.backgroundColor) {
734 this.element.style.backgroundColor = this.oldBackground;
735 }
736 Element.removeClassName(this.element, this.options.hoverClassName)
737 if (this.saving) return;
738 this.effect = new Effect.Highlight(this.element, {
739 startcolor: this.options.highlightcolor,
740 endcolor: this.options.highlightendcolor,
741 restorecolor: this.originalBackground
742 wrapUp: function(transport) {
743 this.leaveEditMode();
744 // Can't use triggerCallback due to backward compatibility: requires
745 // binding + direct element
746 this._boundComplete(transport, this.element);
747 }
748 });
749
750 Object.extend(Ajax.InPlaceEditor.prototype, {
751 dispose: Ajax.InPlaceEditor.prototype.destroy
752 });
753
754 Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
755 initialize: function($super, element, url, options) {
756 this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
757 $super(element, url, options);
758 },
759
760 createEditField: function() {
761 var list = document.createElement('select');
762 list.name = this.options.paramName;
763 list.size = 1;
764 this._controls.editor = list;
765 this._collection = this.options.collection || [];
766 if (this.options.loadCollectionURL)
767 this.loadCollection();
768 else
769 this.checkForExternalText();
770 this._form.appendChild(this._controls.editor);
771 },
772
773 loadCollection: function() {
774 this._form.addClassName(this.options.loadingClassName);
775 this.showLoadingText(this.options.loadingCollectionText);
776 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
777 Object.extend(options, {
778 parameters: 'editorId=' + encodeURIComponent(this.element.id),
779 onComplete: Prototype.emptyFunction,
780 onSuccess: function(transport) {
781 var js = transport.responseText.strip();
782 if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
783 throw 'Server returned an invalid collection representation.';
784 this._collection = eval(js);
785 this.checkForExternalText();
786 }.bind(this),
787 onFailure: this.onFailure
742 788 });
789 new Ajax.Request(this.options.loadCollectionURL, options);
743 790 },
744 leaveEditMode: function() {
745 Element.removeClassName(this.element, this.options.savingClassName);
746 this.removeForm();
747 this.leaveHover();
748 this.element.style.backgroundColor = this.originalBackground;
749 Element.show(this.element);
750 if (this.options.externalControl) {
751 Element.show(this.options.externalControl);
791
792 showLoadingText: function(text) {
793 this._controls.editor.disabled = true;
794 var tempOption = this._controls.editor.firstChild;
795 if (!tempOption) {
796 tempOption = document.createElement('option');
797 tempOption.value = '';
798 this._controls.editor.appendChild(tempOption);
799 tempOption.selected = true;
752 800 }
753 this.editing = false;
754 this.saving = false;
755 this.oldInnerHTML = null;
756 this.onLeaveEditMode();
801 tempOption.update((text || '').stripScripts().stripTags());
757 802 },
758 onComplete: function(transport) {
759 this.leaveEditMode();
760 this.options.onComplete.bind(this)(transport, this.element);
803
804 checkForExternalText: function() {
805 this._text = this.getText();
806 if (this.options.loadTextURL)
807 this.loadExternalText();
808 else
809 this.buildOptionList();
761 810 },
762 onEnterEditMode: function() {},
763 onLeaveEditMode: function() {},
764 dispose: function() {
765 if (this.oldInnerHTML) {
766 this.element.innerHTML = this.oldInnerHTML;
767 }
768 this.leaveEditMode();
769 Event.stopObserving(this.element, 'click', this.onclickListener);
770 Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
771 Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
772 if (this.options.externalControl) {
773 Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
774 Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
775 Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
776 }
811
812 loadExternalText: function() {
813 this.showLoadingText(this.options.loadingText);
814 var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
815 Object.extend(options, {
816 parameters: 'editorId=' + encodeURIComponent(this.element.id),
817 onComplete: Prototype.emptyFunction,
818 onSuccess: function(transport) {
819 this._text = transport.responseText.strip();
820 this.buildOptionList();
821 }.bind(this),
822 onFailure: this.onFailure
823 });
824 new Ajax.Request(this.options.loadTextURL, options);
825 },
826
827 buildOptionList: function() {
828 this._form.removeClassName(this.options.loadingClassName);
829 this._collection = this._collection.map(function(entry) {
830 return 2 === entry.length ? entry : [entry, entry].flatten();
831 });
832 var marker = ('value' in this.options) ? this.options.value : this._text;
833 var textFound = this._collection.any(function(entry) {
834 return entry[0] == marker;
835 }.bind(this));
836 this._controls.editor.update('');
837 var option;
838 this._collection.each(function(entry, index) {
839 option = document.createElement('option');
840 option.value = entry[0];
841 option.selected = textFound ? entry[0] == marker : 0 == index;
842 option.appendChild(document.createTextNode(entry[1]));
843 this._controls.editor.appendChild(option);
844 }.bind(this));
845 this._controls.editor.disabled = false;
846 Field.scrollFreeActivate(this._controls.editor);
777 847 }
778 };
848 });
779 849
780 Ajax.InPlaceCollectionEditor = Class.create();
781 Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
782 Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
783 createEditField: function() {
784 if (!this.cached_selectTag) {
785 var selectTag = document.createElement("select");
786 var collection = this.options.collection || [];
787 var optionTag;
788 collection.each(function(e,i) {
789 optionTag = document.createElement("option");
790 optionTag.value = (e instanceof Array) ? e[0] : e;
791 if((typeof this.options.value == 'undefined') &&
792 ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
793 if(this.options.value==optionTag.value) optionTag.selected = true;
794 optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
795 selectTag.appendChild(optionTag);
796 }.bind(this));
797 this.cached_selectTag = selectTag;
798 }
850 //**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
851 //**** This only exists for a while, in order to let ****
852 //**** users adapt to the new API. Read up on the new ****
853 //**** API and convert your code to it ASAP! ****
854
855 Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
856 if (!options) return;
857 function fallback(name, expr) {
858 if (name in options || expr === undefined) return;
859 options[name] = expr;
860 };
861 fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
862 options.cancelLink == options.cancelButton == false ? false : undefined)));
863 fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
864 options.okLink == options.okButton == false ? false : undefined)));
865 fallback('highlightColor', options.highlightcolor);
866 fallback('highlightEndColor', options.highlightendcolor);
867 };
799 868
800 this.editField = this.cached_selectTag;
801 if(this.options.loadTextURL) this.loadExternalText();
802 this.form.appendChild(this.editField);
803 this.options.callback = function(form, value) {
804 return "value=" + encodeURIComponent(value);
869 Object.extend(Ajax.InPlaceEditor, {
870 DefaultOptions: {
871 ajaxOptions: { },
872 autoRows: 3, // Use when multi-line w/ rows == 1
873 cancelControl: 'link', // 'link'|'button'|false
874 cancelText: 'cancel',
875 clickToEditText: 'Click to edit',
876 externalControl: null, // id|elt
877 externalControlOnly: false,
878 fieldPostCreation: 'activate', // 'activate'|'focus'|false
879 formClassName: 'inplaceeditor-form',
880 formId: null, // id|elt
881 highlightColor: '#ffff99',
882 highlightEndColor: '#ffffff',
883 hoverClassName: '',
884 htmlResponse: true,
885 loadingClassName: 'inplaceeditor-loading',
886 loadingText: 'Loading...',
887 okControl: 'button', // 'link'|'button'|false
888 okText: 'ok',
889 paramName: 'value',
890 rows: 1, // If 1 and multi-line, uses autoRows
891 savingClassName: 'inplaceeditor-saving',
892 savingText: 'Saving...',
893 size: 0,
894 stripLoadedTextTags: false,
895 submitOnBlur: false,
896 textAfterControls: '',
897 textBeforeControls: '',
898 textBetweenControls: ''
899 },
900 DefaultCallbacks: {
901 callback: function(form) {
902 return Form.serialize(form);
903 },
904 onComplete: function(transport, element) {
905 // For backward compatibility, this one is bound to the IPE, and passes
906 // the element directly. It was too often customized, so we don't break it.
907 new Effect.Highlight(element, {
908 startcolor: this.options.highlightColor, keepBackgroundImage: true });
909 },
910 onEnterEditMode: null,
911 onEnterHover: function(ipe) {
912 ipe.element.style.backgroundColor = ipe.options.highlightColor;
913 if (ipe._effect)
914 ipe._effect.cancel();
915 },
916 onFailure: function(transport, ipe) {
917 alert('Error communication with the server: ' + transport.responseText.stripTags());
918 },
919 onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
920 onLeaveEditMode: null,
921 onLeaveHover: function(ipe) {
922 ipe._effect = new Effect.Highlight(ipe.element, {
923 startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
924 restorecolor: ipe._originalBackground, keepBackgroundImage: true
925 });
805 926 }
927 },
928 Listeners: {
929 click: 'enterEditMode',
930 keydown: 'checkForEscapeOrReturn',
931 mouseover: 'enterHover',
932 mouseout: 'leaveHover'
806 933 }
807 934 });
808 935
936 Ajax.InPlaceCollectionEditor.DefaultOptions = {
937 loadingCollectionText: 'Loading options...'
938 };
939
809 940 // Delayed observer, like Form.Element.Observer,
810 941 // but waits for delay after last key input
811 942 // Ideal for live-search fields
812 943
813 Form.Element.DelayedObserver = Class.create();
814 Form.Element.DelayedObserver.prototype = {
944 Form.Element.DelayedObserver = Class.create({
815 945 initialize: function(element, delay, callback) {
816 946 this.delay = delay || 0.5;
817 947 this.element = $(element);
818 948 this.callback = callback;
819 949 this.timer = null;
820 950 this.lastValue = $F(this.element);
821 951 Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
822 952 },
823 953 delayedListener: function(event) {
824 954 if(this.lastValue == $F(this.element)) return;
825 955 if(this.timer) clearTimeout(this.timer);
826 956 this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
827 957 this.lastValue = $F(this.element);
828 958 },
829 959 onTimerEvent: function() {
830 960 this.timer = null;
831 961 this.callback(this.element, $F(this.element));
832 962 }
833 };
963 });
@@ -1,942 +1,972
1 // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 // (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
1 // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 // (c) 2005-2007 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
3 3 //
4 4 // script.aculo.us is freely distributable under the terms of an MIT-style license.
5 5 // For details, see the script.aculo.us web site: http://script.aculo.us/
6 6
7 if(typeof Effect == 'undefined')
7 if(Object.isUndefined(Effect))
8 8 throw("dragdrop.js requires including script.aculo.us' effects.js library");
9 9
10 10 var Droppables = {
11 11 drops: [],
12 12
13 13 remove: function(element) {
14 14 this.drops = this.drops.reject(function(d) { return d.element==$(element) });
15 15 },
16 16
17 17 add: function(element) {
18 18 element = $(element);
19 19 var options = Object.extend({
20 20 greedy: true,
21 21 hoverclass: null,
22 22 tree: false
23 }, arguments[1] || {});
23 }, arguments[1] || { });
24 24
25 25 // cache containers
26 26 if(options.containment) {
27 27 options._containers = [];
28 28 var containment = options.containment;
29 if((typeof containment == 'object') &&
30 (containment.constructor == Array)) {
29 if(Object.isArray(containment)) {
31 30 containment.each( function(c) { options._containers.push($(c)) });
32 31 } else {
33 32 options._containers.push($(containment));
34 33 }
35 34 }
36 35
37 36 if(options.accept) options.accept = [options.accept].flatten();
38 37
39 38 Element.makePositioned(element); // fix IE
40 39 options.element = element;
41 40
42 41 this.drops.push(options);
43 42 },
44 43
45 44 findDeepestChild: function(drops) {
46 45 deepest = drops[0];
47 46
48 47 for (i = 1; i < drops.length; ++i)
49 48 if (Element.isParent(drops[i].element, deepest.element))
50 49 deepest = drops[i];
51 50
52 51 return deepest;
53 52 },
54 53
55 54 isContained: function(element, drop) {
56 55 var containmentNode;
57 56 if(drop.tree) {
58 57 containmentNode = element.treeNode;
59 58 } else {
60 59 containmentNode = element.parentNode;
61 60 }
62 61 return drop._containers.detect(function(c) { return containmentNode == c });
63 62 },
64 63
65 64 isAffected: function(point, element, drop) {
66 65 return (
67 66 (drop.element!=element) &&
68 67 ((!drop._containers) ||
69 68 this.isContained(element, drop)) &&
70 69 ((!drop.accept) ||
71 70 (Element.classNames(element).detect(
72 71 function(v) { return drop.accept.include(v) } ) )) &&
73 72 Position.within(drop.element, point[0], point[1]) );
74 73 },
75 74
76 75 deactivate: function(drop) {
77 76 if(drop.hoverclass)
78 77 Element.removeClassName(drop.element, drop.hoverclass);
79 78 this.last_active = null;
80 79 },
81 80
82 81 activate: function(drop) {
83 82 if(drop.hoverclass)
84 83 Element.addClassName(drop.element, drop.hoverclass);
85 84 this.last_active = drop;
86 85 },
87 86
88 87 show: function(point, element) {
89 88 if(!this.drops.length) return;
90 var affected = [];
89 var drop, affected = [];
91 90
92 if(this.last_active) this.deactivate(this.last_active);
93 91 this.drops.each( function(drop) {
94 92 if(Droppables.isAffected(point, element, drop))
95 93 affected.push(drop);
96 94 });
97 95
98 if(affected.length>0) {
96 if(affected.length>0)
99 97 drop = Droppables.findDeepestChild(affected);
98
99 if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
100 if (drop) {
100 101 Position.within(drop.element, point[0], point[1]);
101 102 if(drop.onHover)
102 103 drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
103 104
104 Droppables.activate(drop);
105 if (drop != this.last_active) Droppables.activate(drop);
105 106 }
106 107 },
107 108
108 109 fire: function(event, element) {
109 110 if(!this.last_active) return;
110 111 Position.prepare();
111 112
112 113 if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
113 if (this.last_active.onDrop)
114 this.last_active.onDrop(element, this.last_active.element, event);
114 if (this.last_active.onDrop) {
115 this.last_active.onDrop(element, this.last_active.element, event);
116 return true;
117 }
115 118 },
116 119
117 120 reset: function() {
118 121 if(this.last_active)
119 122 this.deactivate(this.last_active);
120 123 }
121 124 }
122 125
123 126 var Draggables = {
124 127 drags: [],
125 128 observers: [],
126 129
127 130 register: function(draggable) {
128 131 if(this.drags.length == 0) {
129 132 this.eventMouseUp = this.endDrag.bindAsEventListener(this);
130 133 this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
131 134 this.eventKeypress = this.keyPress.bindAsEventListener(this);
132 135
133 136 Event.observe(document, "mouseup", this.eventMouseUp);
134 137 Event.observe(document, "mousemove", this.eventMouseMove);
135 138 Event.observe(document, "keypress", this.eventKeypress);
136 139 }
137 140 this.drags.push(draggable);
138 141 },
139 142
140 143 unregister: function(draggable) {
141 144 this.drags = this.drags.reject(function(d) { return d==draggable });
142 145 if(this.drags.length == 0) {
143 146 Event.stopObserving(document, "mouseup", this.eventMouseUp);
144 147 Event.stopObserving(document, "mousemove", this.eventMouseMove);
145 148 Event.stopObserving(document, "keypress", this.eventKeypress);
146 149 }
147 150 },
148 151
149 152 activate: function(draggable) {
150 153 if(draggable.options.delay) {
151 154 this._timeout = setTimeout(function() {
152 155 Draggables._timeout = null;
153 156 window.focus();
154 157 Draggables.activeDraggable = draggable;
155 158 }.bind(this), draggable.options.delay);
156 159 } else {
157 160 window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
158 161 this.activeDraggable = draggable;
159 162 }
160 163 },
161 164
162 165 deactivate: function() {
163 166 this.activeDraggable = null;
164 167 },
165 168
166 169 updateDrag: function(event) {
167 170 if(!this.activeDraggable) return;
168 171 var pointer = [Event.pointerX(event), Event.pointerY(event)];
169 172 // Mozilla-based browsers fire successive mousemove events with
170 173 // the same coordinates, prevent needless redrawing (moz bug?)
171 174 if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
172 175 this._lastPointer = pointer;
173 176
174 177 this.activeDraggable.updateDrag(event, pointer);
175 178 },
176 179
177 180 endDrag: function(event) {
178 181 if(this._timeout) {
179 182 clearTimeout(this._timeout);
180 183 this._timeout = null;
181 184 }
182 185 if(!this.activeDraggable) return;
183 186 this._lastPointer = null;
184 187 this.activeDraggable.endDrag(event);
185 188 this.activeDraggable = null;
186 189 },
187 190
188 191 keyPress: function(event) {
189 192 if(this.activeDraggable)
190 193 this.activeDraggable.keyPress(event);
191 194 },
192 195
193 196 addObserver: function(observer) {
194 197 this.observers.push(observer);
195 198 this._cacheObserverCallbacks();
196 199 },
197 200
198 201 removeObserver: function(element) { // element instead of observer fixes mem leaks
199 202 this.observers = this.observers.reject( function(o) { return o.element==element });
200 203 this._cacheObserverCallbacks();
201 204 },
202 205
203 206 notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
204 207 if(this[eventName+'Count'] > 0)
205 208 this.observers.each( function(o) {
206 209 if(o[eventName]) o[eventName](eventName, draggable, event);
207 210 });
208 211 if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
209 212 },
210 213
211 214 _cacheObserverCallbacks: function() {
212 215 ['onStart','onEnd','onDrag'].each( function(eventName) {
213 216 Draggables[eventName+'Count'] = Draggables.observers.select(
214 217 function(o) { return o[eventName]; }
215 218 ).length;
216 219 });
217 220 }
218 221 }
219 222
220 223 /*--------------------------------------------------------------------------*/
221 224
222 var Draggable = Class.create();
223 Draggable._dragging = {};
224
225 Draggable.prototype = {
225 var Draggable = Class.create({
226 226 initialize: function(element) {
227 227 var defaults = {
228 228 handle: false,
229 229 reverteffect: function(element, top_offset, left_offset) {
230 230 var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
231 231 new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
232 232 queue: {scope:'_draggable', position:'end'}
233 233 });
234 234 },
235 235 endeffect: function(element) {
236 var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
236 var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
237 237 new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
238 238 queue: {scope:'_draggable', position:'end'},
239 239 afterFinish: function(){
240 240 Draggable._dragging[element] = false
241 241 }
242 242 });
243 243 },
244 244 zindex: 1000,
245 245 revert: false,
246 quiet: false,
246 247 scroll: false,
247 248 scrollSensitivity: 20,
248 249 scrollSpeed: 15,
249 250 snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
250 251 delay: 0
251 252 };
252 253
253 if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
254 if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
254 255 Object.extend(defaults, {
255 256 starteffect: function(element) {
256 257 element._opacity = Element.getOpacity(element);
257 258 Draggable._dragging[element] = true;
258 259 new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
259 260 }
260 261 });
261 262
262 var options = Object.extend(defaults, arguments[1] || {});
263 var options = Object.extend(defaults, arguments[1] || { });
263 264
264 265 this.element = $(element);
265 266
266 if(options.handle && (typeof options.handle == 'string'))
267 if(options.handle && Object.isString(options.handle))
267 268 this.handle = this.element.down('.'+options.handle, 0);
268 269
269 270 if(!this.handle) this.handle = $(options.handle);
270 271 if(!this.handle) this.handle = this.element;
271 272
272 273 if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
273 274 options.scroll = $(options.scroll);
274 275 this._isScrollChild = Element.childOf(this.element, options.scroll);
275 276 }
276 277
277 278 Element.makePositioned(this.element); // fix IE
278 279
279 this.delta = this.currentDelta();
280 280 this.options = options;
281 281 this.dragging = false;
282 282
283 283 this.eventMouseDown = this.initDrag.bindAsEventListener(this);
284 284 Event.observe(this.handle, "mousedown", this.eventMouseDown);
285 285
286 286 Draggables.register(this);
287 287 },
288 288
289 289 destroy: function() {
290 290 Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
291 291 Draggables.unregister(this);
292 292 },
293 293
294 294 currentDelta: function() {
295 295 return([
296 296 parseInt(Element.getStyle(this.element,'left') || '0'),
297 297 parseInt(Element.getStyle(this.element,'top') || '0')]);
298 298 },
299 299
300 300 initDrag: function(event) {
301 if(typeof Draggable._dragging[this.element] != 'undefined' &&
301 if(!Object.isUndefined(Draggable._dragging[this.element]) &&
302 302 Draggable._dragging[this.element]) return;
303 303 if(Event.isLeftClick(event)) {
304 304 // abort on form elements, fixes a Firefox issue
305 305 var src = Event.element(event);
306 if(src.tagName && (
307 src.tagName=='INPUT' ||
308 src.tagName=='SELECT' ||
309 src.tagName=='OPTION' ||
310 src.tagName=='BUTTON' ||
311 src.tagName=='TEXTAREA')) return;
306 if((tag_name = src.tagName.toUpperCase()) && (
307 tag_name=='INPUT' ||
308 tag_name=='SELECT' ||
309 tag_name=='OPTION' ||
310 tag_name=='BUTTON' ||
311 tag_name=='TEXTAREA')) return;
312 312
313 313 var pointer = [Event.pointerX(event), Event.pointerY(event)];
314 314 var pos = Position.cumulativeOffset(this.element);
315 315 this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
316 316
317 317 Draggables.activate(this);
318 318 Event.stop(event);
319 319 }
320 320 },
321 321
322 322 startDrag: function(event) {
323 323 this.dragging = true;
324 if(!this.delta)
325 this.delta = this.currentDelta();
324 326
325 327 if(this.options.zindex) {
326 328 this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
327 329 this.element.style.zIndex = this.options.zindex;
328 330 }
329 331
330 332 if(this.options.ghosting) {
331 333 this._clone = this.element.cloneNode(true);
332 Position.absolutize(this.element);
334 this.element._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
335 if (!this.element._originallyAbsolute)
336 Position.absolutize(this.element);
333 337 this.element.parentNode.insertBefore(this._clone, this.element);
334 338 }
335 339
336 340 if(this.options.scroll) {
337 341 if (this.options.scroll == window) {
338 342 var where = this._getWindowScroll(this.options.scroll);
339 343 this.originalScrollLeft = where.left;
340 344 this.originalScrollTop = where.top;
341 345 } else {
342 346 this.originalScrollLeft = this.options.scroll.scrollLeft;
343 347 this.originalScrollTop = this.options.scroll.scrollTop;
344 348 }
345 349 }
346 350
347 351 Draggables.notify('onStart', this, event);
348 352
349 353 if(this.options.starteffect) this.options.starteffect(this.element);
350 354 },
351 355
352 356 updateDrag: function(event, pointer) {
353 357 if(!this.dragging) this.startDrag(event);
354 Position.prepare();
355 Droppables.show(pointer, this.element);
358
359 if(!this.options.quiet){
360 Position.prepare();
361 Droppables.show(pointer, this.element);
362 }
363
356 364 Draggables.notify('onDrag', this, event);
357 365
358 366 this.draw(pointer);
359 367 if(this.options.change) this.options.change(this);
360 368
361 369 if(this.options.scroll) {
362 370 this.stopScrolling();
363 371
364 372 var p;
365 373 if (this.options.scroll == window) {
366 374 with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
367 375 } else {
368 376 p = Position.page(this.options.scroll);
369 377 p[0] += this.options.scroll.scrollLeft + Position.deltaX;
370 378 p[1] += this.options.scroll.scrollTop + Position.deltaY;
371 379 p.push(p[0]+this.options.scroll.offsetWidth);
372 380 p.push(p[1]+this.options.scroll.offsetHeight);
373 381 }
374 382 var speed = [0,0];
375 383 if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
376 384 if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
377 385 if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
378 386 if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
379 387 this.startScrolling(speed);
380 388 }
381 389
382 390 // fix AppleWebKit rendering
383 if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
391 if(Prototype.Browser.WebKit) window.scrollBy(0,0);
384 392
385 393 Event.stop(event);
386 394 },
387 395
388 396 finishDrag: function(event, success) {
389 397 this.dragging = false;
398
399 if(this.options.quiet){
400 Position.prepare();
401 var pointer = [Event.pointerX(event), Event.pointerY(event)];
402 Droppables.show(pointer, this.element);
403 }
390 404
391 405 if(this.options.ghosting) {
392 Position.relativize(this.element);
406 if (!this.element._originallyAbsolute)
407 Position.relativize(this.element);
408 delete this.element._originallyAbsolute;
393 409 Element.remove(this._clone);
394 410 this._clone = null;
395 411 }
396 412
397 if(success) Droppables.fire(event, this.element);
413 var dropped = false;
414 if(success) {
415 dropped = Droppables.fire(event, this.element);
416 if (!dropped) dropped = false;
417 }
418 if(dropped && this.options.onDropped) this.options.onDropped(this.element);
398 419 Draggables.notify('onEnd', this, event);
399 420
400 421 var revert = this.options.revert;
401 if(revert && typeof revert == 'function') revert = revert(this.element);
422 if(revert && Object.isFunction(revert)) revert = revert(this.element);
402 423
403 424 var d = this.currentDelta();
404 425 if(revert && this.options.reverteffect) {
405 this.options.reverteffect(this.element,
406 d[1]-this.delta[1], d[0]-this.delta[0]);
426 if (dropped == 0 || revert != 'failure')
427 this.options.reverteffect(this.element,
428 d[1]-this.delta[1], d[0]-this.delta[0]);
407 429 } else {
408 430 this.delta = d;
409 431 }
410 432
411 433 if(this.options.zindex)
412 434 this.element.style.zIndex = this.originalZ;
413 435
414 436 if(this.options.endeffect)
415 437 this.options.endeffect(this.element);
416 438
417 439 Draggables.deactivate(this);
418 440 Droppables.reset();
419 441 },
420 442
421 443 keyPress: function(event) {
422 444 if(event.keyCode!=Event.KEY_ESC) return;
423 445 this.finishDrag(event, false);
424 446 Event.stop(event);
425 447 },
426 448
427 449 endDrag: function(event) {
428 450 if(!this.dragging) return;
429 451 this.stopScrolling();
430 452 this.finishDrag(event, true);
431 453 Event.stop(event);
432 454 },
433 455
434 456 draw: function(point) {
435 457 var pos = Position.cumulativeOffset(this.element);
436 458 if(this.options.ghosting) {
437 459 var r = Position.realOffset(this.element);
438 460 pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
439 461 }
440 462
441 463 var d = this.currentDelta();
442 464 pos[0] -= d[0]; pos[1] -= d[1];
443 465
444 466 if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
445 467 pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
446 468 pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
447 469 }
448 470
449 471 var p = [0,1].map(function(i){
450 472 return (point[i]-pos[i]-this.offset[i])
451 473 }.bind(this));
452 474
453 475 if(this.options.snap) {
454 if(typeof this.options.snap == 'function') {
476 if(Object.isFunction(this.options.snap)) {
455 477 p = this.options.snap(p[0],p[1],this);
456 478 } else {
457 if(this.options.snap instanceof Array) {
479 if(Object.isArray(this.options.snap)) {
458 480 p = p.map( function(v, i) {
459 return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
481 return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this))
460 482 } else {
461 483 p = p.map( function(v) {
462 return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
484 return (v/this.options.snap).round()*this.options.snap }.bind(this))
463 485 }
464 486 }}
465 487
466 488 var style = this.element.style;
467 489 if((!this.options.constraint) || (this.options.constraint=='horizontal'))
468 490 style.left = p[0] + "px";
469 491 if((!this.options.constraint) || (this.options.constraint=='vertical'))
470 492 style.top = p[1] + "px";
471 493
472 494 if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
473 495 },
474 496
475 497 stopScrolling: function() {
476 498 if(this.scrollInterval) {
477 499 clearInterval(this.scrollInterval);
478 500 this.scrollInterval = null;
479 501 Draggables._lastScrollPointer = null;
480 502 }
481 503 },
482 504
483 505 startScrolling: function(speed) {
484 506 if(!(speed[0] || speed[1])) return;
485 507 this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
486 508 this.lastScrolled = new Date();
487 509 this.scrollInterval = setInterval(this.scroll.bind(this), 10);
488 510 },
489 511
490 512 scroll: function() {
491 513 var current = new Date();
492 514 var delta = current - this.lastScrolled;
493 515 this.lastScrolled = current;
494 516 if(this.options.scroll == window) {
495 517 with (this._getWindowScroll(this.options.scroll)) {
496 518 if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
497 519 var d = delta / 1000;
498 520 this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
499 521 }
500 522 }
501 523 } else {
502 524 this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
503 525 this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
504 526 }
505 527
506 528 Position.prepare();
507 529 Droppables.show(Draggables._lastPointer, this.element);
508 530 Draggables.notify('onDrag', this);
509 531 if (this._isScrollChild) {
510 532 Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
511 533 Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
512 534 Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
513 535 if (Draggables._lastScrollPointer[0] < 0)
514 536 Draggables._lastScrollPointer[0] = 0;
515 537 if (Draggables._lastScrollPointer[1] < 0)
516 538 Draggables._lastScrollPointer[1] = 0;
517 539 this.draw(Draggables._lastScrollPointer);
518 540 }
519 541
520 542 if(this.options.change) this.options.change(this);
521 543 },
522 544
523 545 _getWindowScroll: function(w) {
524 546 var T, L, W, H;
525 547 with (w.document) {
526 548 if (w.document.documentElement && documentElement.scrollTop) {
527 549 T = documentElement.scrollTop;
528 550 L = documentElement.scrollLeft;
529 551 } else if (w.document.body) {
530 552 T = body.scrollTop;
531 553 L = body.scrollLeft;
532 554 }
533 555 if (w.innerWidth) {
534 556 W = w.innerWidth;
535 557 H = w.innerHeight;
536 558 } else if (w.document.documentElement && documentElement.clientWidth) {
537 559 W = documentElement.clientWidth;
538 560 H = documentElement.clientHeight;
539 561 } else {
540 562 W = body.offsetWidth;
541 563 H = body.offsetHeight
542 564 }
543 565 }
544 566 return { top: T, left: L, width: W, height: H };
545 567 }
546 }
568 });
569
570 Draggable._dragging = { };
547 571
548 572 /*--------------------------------------------------------------------------*/
549 573
550 var SortableObserver = Class.create();
551 SortableObserver.prototype = {
574 var SortableObserver = Class.create({
552 575 initialize: function(element, observer) {
553 576 this.element = $(element);
554 577 this.observer = observer;
555 578 this.lastValue = Sortable.serialize(this.element);
556 579 },
557 580
558 581 onStart: function() {
559 582 this.lastValue = Sortable.serialize(this.element);
560 583 },
561 584
562 585 onEnd: function() {
563 586 Sortable.unmark();
564 587 if(this.lastValue != Sortable.serialize(this.element))
565 588 this.observer(this.element)
566 589 }
567 }
590 });
568 591
569 592 var Sortable = {
570 593 SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
571 594
572 sortables: {},
595 sortables: { },
573 596
574 597 _findRootElement: function(element) {
575 while (element.tagName != "BODY") {
598 while (element.tagName.toUpperCase() != "BODY") {
576 599 if(element.id && Sortable.sortables[element.id]) return element;
577 600 element = element.parentNode;
578 601 }
579 602 },
580 603
581 604 options: function(element) {
582 605 element = Sortable._findRootElement($(element));
583 606 if(!element) return;
584 607 return Sortable.sortables[element.id];
585 608 },
586 609
587 610 destroy: function(element){
588 611 var s = Sortable.options(element);
589 612
590 613 if(s) {
591 614 Draggables.removeObserver(s.element);
592 615 s.droppables.each(function(d){ Droppables.remove(d) });
593 616 s.draggables.invoke('destroy');
594 617
595 618 delete Sortable.sortables[s.element.id];
596 619 }
597 620 },
598 621
599 622 create: function(element) {
600 623 element = $(element);
601 624 var options = Object.extend({
602 625 element: element,
603 626 tag: 'li', // assumes li children, override with tag: 'tagname'
604 627 dropOnEmpty: false,
605 628 tree: false,
606 629 treeTag: 'ul',
607 630 overlap: 'vertical', // one of 'vertical', 'horizontal'
608 631 constraint: 'vertical', // one of 'vertical', 'horizontal', false
609 632 containment: element, // also takes array of elements (or id's); or false
610 633 handle: false, // or a CSS class
611 634 only: false,
612 635 delay: 0,
613 636 hoverclass: null,
614 637 ghosting: false,
638 quiet: false,
615 639 scroll: false,
616 640 scrollSensitivity: 20,
617 641 scrollSpeed: 15,
618 642 format: this.SERIALIZE_RULE,
643
644 // these take arrays of elements or ids and can be
645 // used for better initialization performance
646 elements: false,
647 handles: false,
648
619 649 onChange: Prototype.emptyFunction,
620 650 onUpdate: Prototype.emptyFunction
621 }, arguments[1] || {});
651 }, arguments[1] || { });
622 652
623 653 // clear any old sortable with same element
624 654 this.destroy(element);
625 655
626 656 // build options for the draggables
627 657 var options_for_draggable = {
628 658 revert: true,
659 quiet: options.quiet,
629 660 scroll: options.scroll,
630 661 scrollSpeed: options.scrollSpeed,
631 662 scrollSensitivity: options.scrollSensitivity,
632 663 delay: options.delay,
633 664 ghosting: options.ghosting,
634 665 constraint: options.constraint,
635 666 handle: options.handle };
636 667
637 668 if(options.starteffect)
638 669 options_for_draggable.starteffect = options.starteffect;
639 670
640 671 if(options.reverteffect)
641 672 options_for_draggable.reverteffect = options.reverteffect;
642 673 else
643 674 if(options.ghosting) options_for_draggable.reverteffect = function(element) {
644 675 element.style.top = 0;
645 676 element.style.left = 0;
646 677 };
647 678
648 679 if(options.endeffect)
649 680 options_for_draggable.endeffect = options.endeffect;
650 681
651 682 if(options.zindex)
652 683 options_for_draggable.zindex = options.zindex;
653 684
654 685 // build options for the droppables
655 686 var options_for_droppable = {
656 687 overlap: options.overlap,
657 688 containment: options.containment,
658 689 tree: options.tree,
659 690 hoverclass: options.hoverclass,
660 691 onHover: Sortable.onHover
661 692 }
662 693
663 694 var options_for_tree = {
664 695 onHover: Sortable.onEmptyHover,
665 696 overlap: options.overlap,
666 697 containment: options.containment,
667 698 hoverclass: options.hoverclass
668 699 }
669 700
670 701 // fix for gecko engine
671 702 Element.cleanWhitespace(element);
672 703
673 704 options.draggables = [];
674 705 options.droppables = [];
675 706
676 707 // drop on empty handling
677 708 if(options.dropOnEmpty || options.tree) {
678 709 Droppables.add(element, options_for_tree);
679 710 options.droppables.push(element);
680 711 }
681 712
682 (this.findElements(element, options) || []).each( function(e) {
683 // handles are per-draggable
684 var handle = options.handle ?
685 $(e).down('.'+options.handle,0) : e;
713 (options.elements || this.findElements(element, options) || []).each( function(e,i) {
714 var handle = options.handles ? $(options.handles[i]) :
715 (options.handle ? $(e).select('.' + options.handle)[0] : e);
686 716 options.draggables.push(
687 717 new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
688 718 Droppables.add(e, options_for_droppable);
689 719 if(options.tree) e.treeNode = element;
690 720 options.droppables.push(e);
691 721 });
692 722
693 723 if(options.tree) {
694 724 (Sortable.findTreeElements(element, options) || []).each( function(e) {
695 725 Droppables.add(e, options_for_tree);
696 726 e.treeNode = element;
697 727 options.droppables.push(e);
698 728 });
699 729 }
700 730
701 731 // keep reference
702 732 this.sortables[element.id] = options;
703 733
704 734 // for onupdate
705 735 Draggables.addObserver(new SortableObserver(element, options.onUpdate));
706 736
707 737 },
708 738
709 739 // return all suitable-for-sortable elements in a guaranteed order
710 740 findElements: function(element, options) {
711 741 return Element.findChildren(
712 742 element, options.only, options.tree ? true : false, options.tag);
713 743 },
714 744
715 745 findTreeElements: function(element, options) {
716 746 return Element.findChildren(
717 747 element, options.only, options.tree ? true : false, options.treeTag);
718 748 },
719 749
720 750 onHover: function(element, dropon, overlap) {
721 751 if(Element.isParent(dropon, element)) return;
722 752
723 753 if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
724 754 return;
725 755 } else if(overlap>0.5) {
726 756 Sortable.mark(dropon, 'before');
727 757 if(dropon.previousSibling != element) {
728 758 var oldParentNode = element.parentNode;
729 759 element.style.visibility = "hidden"; // fix gecko rendering
730 760 dropon.parentNode.insertBefore(element, dropon);
731 761 if(dropon.parentNode!=oldParentNode)
732 762 Sortable.options(oldParentNode).onChange(element);
733 763 Sortable.options(dropon.parentNode).onChange(element);
734 764 }
735 765 } else {
736 766 Sortable.mark(dropon, 'after');
737 767 var nextElement = dropon.nextSibling || null;
738 768 if(nextElement != element) {
739 769 var oldParentNode = element.parentNode;
740 770 element.style.visibility = "hidden"; // fix gecko rendering
741 771 dropon.parentNode.insertBefore(element, nextElement);
742 772 if(dropon.parentNode!=oldParentNode)
743 773 Sortable.options(oldParentNode).onChange(element);
744 774 Sortable.options(dropon.parentNode).onChange(element);
745 775 }
746 776 }
747 777 },
748 778
749 779 onEmptyHover: function(element, dropon, overlap) {
750 780 var oldParentNode = element.parentNode;
751 781 var droponOptions = Sortable.options(dropon);
752 782
753 783 if(!Element.isParent(dropon, element)) {
754 784 var index;
755 785
756 786 var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
757 787 var child = null;
758 788
759 789 if(children) {
760 790 var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
761 791
762 792 for (index = 0; index < children.length; index += 1) {
763 793 if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
764 794 offset -= Element.offsetSize (children[index], droponOptions.overlap);
765 795 } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
766 796 child = index + 1 < children.length ? children[index + 1] : null;
767 797 break;
768 798 } else {
769 799 child = children[index];
770 800 break;
771 801 }
772 802 }
773 803 }
774 804
775 805 dropon.insertBefore(element, child);
776 806
777 807 Sortable.options(oldParentNode).onChange(element);
778 808 droponOptions.onChange(element);
779 809 }
780 810 },
781 811
782 812 unmark: function() {
783 813 if(Sortable._marker) Sortable._marker.hide();
784 814 },
785 815
786 816 mark: function(dropon, position) {
787 817 // mark on ghosting only
788 818 var sortable = Sortable.options(dropon.parentNode);
789 819 if(sortable && !sortable.ghosting) return;
790 820
791 821 if(!Sortable._marker) {
792 822 Sortable._marker =
793 823 ($('dropmarker') || Element.extend(document.createElement('DIV'))).
794 824 hide().addClassName('dropmarker').setStyle({position:'absolute'});
795 825 document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
796 826 }
797 827 var offsets = Position.cumulativeOffset(dropon);
798 828 Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
799 829
800 830 if(position=='after')
801 831 if(sortable.overlap == 'horizontal')
802 832 Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
803 833 else
804 834 Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
805 835
806 836 Sortable._marker.show();
807 837 },
808 838
809 839 _tree: function(element, options, parent) {
810 840 var children = Sortable.findElements(element, options) || [];
811 841
812 842 for (var i = 0; i < children.length; ++i) {
813 843 var match = children[i].id.match(options.format);
814 844
815 845 if (!match) continue;
816 846
817 847 var child = {
818 848 id: encodeURIComponent(match ? match[1] : null),
819 849 element: element,
820 850 parent: parent,
821 851 children: [],
822 852 position: parent.children.length,
823 853 container: $(children[i]).down(options.treeTag)
824 854 }
825 855
826 856 /* Get the element containing the children and recurse over it */
827 857 if (child.container)
828 858 this._tree(child.container, options, child)
829 859
830 860 parent.children.push (child);
831 861 }
832 862
833 863 return parent;
834 864 },
835 865
836 866 tree: function(element) {
837 867 element = $(element);
838 868 var sortableOptions = this.options(element);
839 869 var options = Object.extend({
840 870 tag: sortableOptions.tag,
841 871 treeTag: sortableOptions.treeTag,
842 872 only: sortableOptions.only,
843 873 name: element.id,
844 874 format: sortableOptions.format
845 }, arguments[1] || {});
875 }, arguments[1] || { });
846 876
847 877 var root = {
848 878 id: null,
849 879 parent: null,
850 880 children: [],
851 881 container: element,
852 882 position: 0
853 883 }
854 884
855 885 return Sortable._tree(element, options, root);
856 886 },
857 887
858 888 /* Construct a [i] index for a particular node */
859 889 _constructIndex: function(node) {
860 890 var index = '';
861 891 do {
862 892 if (node.id) index = '[' + node.position + ']' + index;
863 893 } while ((node = node.parent) != null);
864 894 return index;
865 895 },
866 896
867 897 sequence: function(element) {
868 898 element = $(element);
869 var options = Object.extend(this.options(element), arguments[1] || {});
899 var options = Object.extend(this.options(element), arguments[1] || { });
870 900
871 901 return $(this.findElements(element, options) || []).map( function(item) {
872 902 return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
873 903 });
874 904 },
875 905
876 906 setSequence: function(element, new_sequence) {
877 907 element = $(element);
878 var options = Object.extend(this.options(element), arguments[2] || {});
908 var options = Object.extend(this.options(element), arguments[2] || { });
879 909
880 var nodeMap = {};
910 var nodeMap = { };
881 911 this.findElements(element, options).each( function(n) {
882 912 if (n.id.match(options.format))
883 913 nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
884 914 n.parentNode.removeChild(n);
885 915 });
886 916
887 917 new_sequence.each(function(ident) {
888 918 var n = nodeMap[ident];
889 919 if (n) {
890 920 n[1].appendChild(n[0]);
891 921 delete nodeMap[ident];
892 922 }
893 923 });
894 924 },
895 925
896 926 serialize: function(element) {
897 927 element = $(element);
898 var options = Object.extend(Sortable.options(element), arguments[1] || {});
928 var options = Object.extend(Sortable.options(element), arguments[1] || { });
899 929 var name = encodeURIComponent(
900 930 (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
901 931
902 932 if (options.tree) {
903 933 return Sortable.tree(element, arguments[1]).children.map( function (item) {
904 934 return [name + Sortable._constructIndex(item) + "[id]=" +
905 935 encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
906 936 }).flatten().join('&');
907 937 } else {
908 938 return Sortable.sequence(element, arguments[1]).map( function(item) {
909 939 return name + "[]=" + encodeURIComponent(item);
910 940 }).join('&');
911 941 }
912 942 }
913 943 }
914 944
915 945 // Returns true if child is contained within element
916 946 Element.isParent = function(child, element) {
917 947 if (!child.parentNode || child == element) return false;
918 948 if (child.parentNode == element) return true;
919 949 return Element.isParent(child.parentNode, element);
920 950 }
921 951
922 Element.findChildren = function(element, only, recursive, tagName) {
952 Element.findChildren = function(element, only, recursive, tagName) {
923 953 if(!element.hasChildNodes()) return null;
924 954 tagName = tagName.toUpperCase();
925 955 if(only) only = [only].flatten();
926 956 var elements = [];
927 957 $A(element.childNodes).each( function(e) {
928 958 if(e.tagName && e.tagName.toUpperCase()==tagName &&
929 959 (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
930 960 elements.push(e);
931 961 if(recursive) {
932 962 var grandchildren = Element.findChildren(e, only, recursive, tagName);
933 963 if(grandchildren) elements.push(grandchildren);
934 964 }
935 965 });
936 966
937 967 return (elements.length>0 ? elements.flatten() : []);
938 968 }
939 969
940 970 Element.offsetSize = function (element, type) {
941 971 return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
942 972 }
This diff has been collapsed as it changes many lines, (760 lines changed) Show them Hide them
@@ -1,1088 +1,1120
1 // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
1 // Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 2 // Contributors:
3 3 // Justin Palmer (http://encytemedia.com/)
4 4 // Mark Pilgrim (http://diveintomark.org/)
5 5 // Martin Bialasinki
6 6 //
7 7 // script.aculo.us is freely distributable under the terms of an MIT-style license.
8 8 // For details, see the script.aculo.us web site: http://script.aculo.us/
9 9
10 10 // converts rgb() and #xxx to #xxxxxx format,
11 11 // returns self (or first argument) if not convertable
12 12 String.prototype.parseColor = function() {
13 13 var color = '#';
14 if(this.slice(0,4) == 'rgb(') {
14 if (this.slice(0,4) == 'rgb(') {
15 15 var cols = this.slice(4,this.length-1).split(',');
16 16 var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
17 17 } else {
18 if(this.slice(0,1) == '#') {
19 if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
20 if(this.length==7) color = this.toLowerCase();
18 if (this.slice(0,1) == '#') {
19 if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
20 if (this.length==7) color = this.toLowerCase();
21 21 }
22 22 }
23 return(color.length==7 ? color : (arguments[0] || this));
24 }
23 return (color.length==7 ? color : (arguments[0] || this));
24 };
25 25
26 26 /*--------------------------------------------------------------------------*/
27 27
28 28 Element.collectTextNodes = function(element) {
29 29 return $A($(element).childNodes).collect( function(node) {
30 30 return (node.nodeType==3 ? node.nodeValue :
31 31 (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
32 32 }).flatten().join('');
33 }
33 };
34 34
35 35 Element.collectTextNodesIgnoreClass = function(element, className) {
36 36 return $A($(element).childNodes).collect( function(node) {
37 37 return (node.nodeType==3 ? node.nodeValue :
38 38 ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
39 39 Element.collectTextNodesIgnoreClass(node, className) : ''));
40 40 }).flatten().join('');
41 }
41 };
42 42
43 43 Element.setContentZoom = function(element, percent) {
44 44 element = $(element);
45 45 element.setStyle({fontSize: (percent/100) + 'em'});
46 if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
46 if (Prototype.Browser.WebKit) window.scrollBy(0,0);
47 47 return element;
48 }
49
50 Element.getOpacity = function(element){
51 element = $(element);
52 var opacity;
53 if (opacity = element.getStyle('opacity'))
54 return parseFloat(opacity);
55 if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
56 if(opacity[1]) return parseFloat(opacity[1]) / 100;
57 return 1.0;
58 }
48 };
59 49
60 Element.setOpacity = function(element, value){
61 element= $(element);
62 if (value == 1){
63 element.setStyle({ opacity:
64 (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
65 0.999999 : 1.0 });
66 if(/MSIE/.test(navigator.userAgent) && !window.opera)
67 element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
68 } else {
69 if(value < 0.00001) value = 0;
70 element.setStyle({opacity: value});
71 if(/MSIE/.test(navigator.userAgent) && !window.opera)
72 element.setStyle(
73 { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
74 'alpha(opacity='+value*100+')' });
75 }
76 return element;
77 }
78
79 Element.getInlineOpacity = function(element){
50 Element.getInlineOpacity = function(element){
80 51 return $(element).style.opacity || '';
81 }
52 };
82 53
83 54 Element.forceRerendering = function(element) {
84 55 try {
85 56 element = $(element);
86 57 var n = document.createTextNode(' ');
87 58 element.appendChild(n);
88 59 element.removeChild(n);
89 60 } catch(e) { }
90 61 };
91 62
92 63 /*--------------------------------------------------------------------------*/
93 64
94 Array.prototype.call = function() {
95 var args = arguments;
96 this.each(function(f){ f.apply(this, args) });
97 }
98
99 /*--------------------------------------------------------------------------*/
100
101 65 var Effect = {
102 66 _elementDoesNotExistError: {
103 67 name: 'ElementDoesNotExistError',
104 68 message: 'The specified DOM element does not exist, but is required for this effect to operate'
105 69 },
70 Transitions: {
71 linear: Prototype.K,
72 sinoidal: function(pos) {
73 return (-Math.cos(pos*Math.PI)/2) + 0.5;
74 },
75 reverse: function(pos) {
76 return 1-pos;
77 },
78 flicker: function(pos) {
79 var pos = ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
80 return pos > 1 ? 1 : pos;
81 },
82 wobble: function(pos) {
83 return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
84 },
85 pulse: function(pos, pulses) {
86 pulses = pulses || 5;
87 return (
88 ((pos % (1/pulses)) * pulses).round() == 0 ?
89 ((pos * pulses * 2) - (pos * pulses * 2).floor()) :
90 1 - ((pos * pulses * 2) - (pos * pulses * 2).floor())
91 );
92 },
93 spring: function(pos) {
94 return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
95 },
96 none: function(pos) {
97 return 0;
98 },
99 full: function(pos) {
100 return 1;
101 }
102 },
103 DefaultOptions: {
104 duration: 1.0, // seconds
105 fps: 100, // 100= assume 66fps max.
106 sync: false, // true for combining
107 from: 0.0,
108 to: 1.0,
109 delay: 0.0,
110 queue: 'parallel'
111 },
106 112 tagifyText: function(element) {
107 if(typeof Builder == 'undefined')
108 throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
109
110 113 var tagifyStyle = 'position:relative';
111 if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
114 if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';
112 115
113 116 element = $(element);
114 117 $A(element.childNodes).each( function(child) {
115 if(child.nodeType==3) {
118 if (child.nodeType==3) {
116 119 child.nodeValue.toArray().each( function(character) {
117 120 element.insertBefore(
118 Builder.node('span',{style: tagifyStyle},
121 new Element('span', {style: tagifyStyle}).update(
119 122 character == ' ' ? String.fromCharCode(160) : character),
120 123 child);
121 124 });
122 125 Element.remove(child);
123 126 }
124 127 });
125 128 },
126 129 multiple: function(element, effect) {
127 130 var elements;
128 if(((typeof element == 'object') ||
129 (typeof element == 'function')) &&
131 if (((typeof element == 'object') ||
132 Object.isFunction(element)) &&
130 133 (element.length))
131 134 elements = element;
132 135 else
133 136 elements = $(element).childNodes;
134 137
135 138 var options = Object.extend({
136 139 speed: 0.1,
137 140 delay: 0.0
138 }, arguments[2] || {});
141 }, arguments[2] || { });
139 142 var masterDelay = options.delay;
140 143
141 144 $A(elements).each( function(element, index) {
142 145 new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
143 146 });
144 147 },
145 148 PAIRS: {
146 149 'slide': ['SlideDown','SlideUp'],
147 150 'blind': ['BlindDown','BlindUp'],
148 151 'appear': ['Appear','Fade']
149 152 },
150 153 toggle: function(element, effect) {
151 154 element = $(element);
152 155 effect = (effect || 'appear').toLowerCase();
153 156 var options = Object.extend({
154 157 queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
155 }, arguments[2] || {});
158 }, arguments[2] || { });
156 159 Effect[element.visible() ?
157 160 Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
158 161 }
159 162 };
160 163
161 var Effect2 = Effect; // deprecated
162
163 /* ------------- transitions ------------- */
164
165 Effect.Transitions = {
166 linear: Prototype.K,
167 sinoidal: function(pos) {
168 return (-Math.cos(pos*Math.PI)/2) + 0.5;
169 },
170 reverse: function(pos) {
171 return 1-pos;
172 },
173 flicker: function(pos) {
174 return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
175 },
176 wobble: function(pos) {
177 return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
178 },
179 pulse: function(pos, pulses) {
180 pulses = pulses || 5;
181 return (
182 Math.round((pos % (1/pulses)) * pulses) == 0 ?
183 ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) :
184 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
185 );
186 },
187 none: function(pos) {
188 return 0;
189 },
190 full: function(pos) {
191 return 1;
192 }
193 };
164 Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;
194 165
195 166 /* ------------- core effects ------------- */
196 167
197 Effect.ScopedQueue = Class.create();
198 Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
168 Effect.ScopedQueue = Class.create(Enumerable, {
199 169 initialize: function() {
200 170 this.effects = [];
201 this.interval = null;
171 this.interval = null;
202 172 },
203 173 _each: function(iterator) {
204 174 this.effects._each(iterator);
205 175 },
206 176 add: function(effect) {
207 177 var timestamp = new Date().getTime();
208 178
209 var position = (typeof effect.options.queue == 'string') ?
179 var position = Object.isString(effect.options.queue) ?
210 180 effect.options.queue : effect.options.queue.position;
211 181
212 182 switch(position) {
213 183 case 'front':
214 184 // move unstarted effects after this effect
215 185 this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
216 186 e.startOn += effect.finishOn;
217 187 e.finishOn += effect.finishOn;
218 188 });
219 189 break;
220 190 case 'with-last':
221 191 timestamp = this.effects.pluck('startOn').max() || timestamp;
222 192 break;
223 193 case 'end':
224 194 // start effect after last queued effect has finished
225 195 timestamp = this.effects.pluck('finishOn').max() || timestamp;
226 196 break;
227 197 }
228 198
229 199 effect.startOn += timestamp;
230 200 effect.finishOn += timestamp;
231 201
232 if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
202 if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
233 203 this.effects.push(effect);
234 204
235 if(!this.interval)
236 this.interval = setInterval(this.loop.bind(this), 40);
205 if (!this.interval)
206 this.interval = setInterval(this.loop.bind(this), 15);
237 207 },
238 208 remove: function(effect) {
239 209 this.effects = this.effects.reject(function(e) { return e==effect });
240 if(this.effects.length == 0) {
210 if (this.effects.length == 0) {
241 211 clearInterval(this.interval);
242 212 this.interval = null;
243 213 }
244 214 },
245 215 loop: function() {
246 216 var timePos = new Date().getTime();
247 this.effects.invoke('loop', timePos);
217 for(var i=0, len=this.effects.length;i<len;i++)
218 this.effects[i] && this.effects[i].loop(timePos);
248 219 }
249 220 });
250 221
251 222 Effect.Queues = {
252 223 instances: $H(),
253 224 get: function(queueName) {
254 if(typeof queueName != 'string') return queueName;
225 if (!Object.isString(queueName)) return queueName;
255 226
256 if(!this.instances[queueName])
257 this.instances[queueName] = new Effect.ScopedQueue();
258
259 return this.instances[queueName];
227 return this.instances.get(queueName) ||
228 this.instances.set(queueName, new Effect.ScopedQueue());
260 229 }
261 }
230 };
262 231 Effect.Queue = Effect.Queues.get('global');
263 232
264 Effect.DefaultOptions = {
265 transition: Effect.Transitions.sinoidal,
266 duration: 1.0, // seconds
267 fps: 25.0, // max. 25fps due to Effect.Queue implementation
268 sync: false, // true for combining
269 from: 0.0,
270 to: 1.0,
271 delay: 0.0,
272 queue: 'parallel'
273 }
274
275 Effect.Base = function() {};
276 Effect.Base.prototype = {
233 Effect.Base = Class.create({
277 234 position: null,
278 235 start: function(options) {
279 this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
236 function codeForEvent(options,eventName){
237 return (
238 (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
239 (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
240 );
241 }
242 if (options && options.transition === false) options.transition = Effect.Transitions.linear;
243 this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
280 244 this.currentFrame = 0;
281 245 this.state = 'idle';
282 246 this.startOn = this.options.delay*1000;
283 this.finishOn = this.startOn + (this.options.duration*1000);
247 this.finishOn = this.startOn+(this.options.duration*1000);
248 this.fromToDelta = this.options.to-this.options.from;
249 this.totalTime = this.finishOn-this.startOn;
250 this.totalFrames = this.options.fps*this.options.duration;
251
252 eval('this.render = function(pos){ '+
253 'if (this.state=="idle"){this.state="running";'+
254 codeForEvent(this.options,'beforeSetup')+
255 (this.setup ? 'this.setup();':'')+
256 codeForEvent(this.options,'afterSetup')+
257 '};if (this.state=="running"){'+
258 'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';'+
259 'this.position=pos;'+
260 codeForEvent(this.options,'beforeUpdate')+
261 (this.update ? 'this.update(pos);':'')+
262 codeForEvent(this.options,'afterUpdate')+
263 '}}');
264
284 265 this.event('beforeStart');
285 if(!this.options.sync)
286 Effect.Queues.get(typeof this.options.queue == 'string' ?
266 if (!this.options.sync)
267 Effect.Queues.get(Object.isString(this.options.queue) ?
287 268 'global' : this.options.queue.scope).add(this);
288 269 },
289 270 loop: function(timePos) {
290 if(timePos >= this.startOn) {
291 if(timePos >= this.finishOn) {
271 if (timePos >= this.startOn) {
272 if (timePos >= this.finishOn) {
292 273 this.render(1.0);
293 274 this.cancel();
294 275 this.event('beforeFinish');
295 if(this.finish) this.finish();
276 if (this.finish) this.finish();
296 277 this.event('afterFinish');
297 278 return;
298 279 }
299 var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
300 var frame = Math.round(pos * this.options.fps * this.options.duration);
301 if(frame > this.currentFrame) {
280 var pos = (timePos - this.startOn) / this.totalTime,
281 frame = (pos * this.totalFrames).round();
282 if (frame > this.currentFrame) {
302 283 this.render(pos);
303 284 this.currentFrame = frame;
304 285 }
305 286 }
306 287 },
307 render: function(pos) {
308 if(this.state == 'idle') {
309 this.state = 'running';
310 this.event('beforeSetup');
311 if(this.setup) this.setup();
312 this.event('afterSetup');
313 }
314 if(this.state == 'running') {
315 if(this.options.transition) pos = this.options.transition(pos);
316 pos *= (this.options.to-this.options.from);
317 pos += this.options.from;
318 this.position = pos;
319 this.event('beforeUpdate');
320 if(this.update) this.update(pos);
321 this.event('afterUpdate');
322 }
323 },
324 288 cancel: function() {
325 if(!this.options.sync)
326 Effect.Queues.get(typeof this.options.queue == 'string' ?
289 if (!this.options.sync)
290 Effect.Queues.get(Object.isString(this.options.queue) ?
327 291 'global' : this.options.queue.scope).remove(this);
328 292 this.state = 'finished';
329 293 },
330 294 event: function(eventName) {
331 if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
332 if(this.options[eventName]) this.options[eventName](this);
295 if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
296 if (this.options[eventName]) this.options[eventName](this);
333 297 },
334 298 inspect: function() {
335 return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
299 var data = $H();
300 for(property in this)
301 if (!Object.isFunction(this[property])) data.set(property, this[property]);
302 return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
336 303 }
337 }
304 });
338 305
339 Effect.Parallel = Class.create();
340 Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
306 Effect.Parallel = Class.create(Effect.Base, {
341 307 initialize: function(effects) {
342 308 this.effects = effects || [];
343 309 this.start(arguments[1]);
344 310 },
345 311 update: function(position) {
346 312 this.effects.invoke('render', position);
347 313 },
348 314 finish: function(position) {
349 315 this.effects.each( function(effect) {
350 316 effect.render(1.0);
351 317 effect.cancel();
352 318 effect.event('beforeFinish');
353 if(effect.finish) effect.finish(position);
319 if (effect.finish) effect.finish(position);
354 320 effect.event('afterFinish');
355 321 });
356 322 }
357 323 });
358 324
359 Effect.Event = Class.create();
360 Object.extend(Object.extend(Effect.Event.prototype, Effect.Base.prototype), {
325 Effect.Tween = Class.create(Effect.Base, {
326 initialize: function(object, from, to) {
327 object = Object.isString(object) ? $(object) : object;
328 var args = $A(arguments), method = args.last(),
329 options = args.length == 5 ? args[3] : null;
330 this.method = Object.isFunction(method) ? method.bind(object) :
331 Object.isFunction(object[method]) ? object[method].bind(object) :
332 function(value) { object[method] = value };
333 this.start(Object.extend({ from: from, to: to }, options || { }));
334 },
335 update: function(position) {
336 this.method(position);
337 }
338 });
339
340 Effect.Event = Class.create(Effect.Base, {
361 341 initialize: function() {
362 var options = Object.extend({
363 duration: 0
364 }, arguments[0] || {});
365 this.start(options);
342 this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
366 343 },
367 344 update: Prototype.emptyFunction
368 345 });
369 346
370 Effect.Opacity = Class.create();
371 Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
347 Effect.Opacity = Class.create(Effect.Base, {
372 348 initialize: function(element) {
373 349 this.element = $(element);
374 if(!this.element) throw(Effect._elementDoesNotExistError);
350 if (!this.element) throw(Effect._elementDoesNotExistError);
375 351 // make this work on IE on elements without 'layout'
376 if(/MSIE/.test(navigator.userAgent) && !window.opera && (!this.element.currentStyle.hasLayout))
352 if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
377 353 this.element.setStyle({zoom: 1});
378 354 var options = Object.extend({
379 355 from: this.element.getOpacity() || 0.0,
380 356 to: 1.0
381 }, arguments[1] || {});
357 }, arguments[1] || { });
382 358 this.start(options);
383 359 },
384 360 update: function(position) {
385 361 this.element.setOpacity(position);
386 362 }
387 363 });
388 364
389 Effect.Move = Class.create();
390 Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
365 Effect.Move = Class.create(Effect.Base, {
391 366 initialize: function(element) {
392 367 this.element = $(element);
393 if(!this.element) throw(Effect._elementDoesNotExistError);
368 if (!this.element) throw(Effect._elementDoesNotExistError);
394 369 var options = Object.extend({
395 370 x: 0,
396 371 y: 0,
397 372 mode: 'relative'
398 }, arguments[1] || {});
373 }, arguments[1] || { });
399 374 this.start(options);
400 375 },
401 376 setup: function() {
402 // Bug in Opera: Opera returns the "real" position of a static element or
403 // relative element that does not have top/left explicitly set.
404 // ==> Always set top and left for position relative elements in your stylesheets
405 // (to 0 if you do not need them)
406 377 this.element.makePositioned();
407 378 this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
408 379 this.originalTop = parseFloat(this.element.getStyle('top') || '0');
409 if(this.options.mode == 'absolute') {
410 // absolute movement, so we need to calc deltaX and deltaY
380 if (this.options.mode == 'absolute') {
411 381 this.options.x = this.options.x - this.originalLeft;
412 382 this.options.y = this.options.y - this.originalTop;
413 383 }
414 384 },
415 385 update: function(position) {
416 386 this.element.setStyle({
417 left: Math.round(this.options.x * position + this.originalLeft) + 'px',
418 top: Math.round(this.options.y * position + this.originalTop) + 'px'
387 left: (this.options.x * position + this.originalLeft).round() + 'px',
388 top: (this.options.y * position + this.originalTop).round() + 'px'
419 389 });
420 390 }
421 391 });
422 392
423 393 // for backwards compatibility
424 394 Effect.MoveBy = function(element, toTop, toLeft) {
425 395 return new Effect.Move(element,
426 Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
396 Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
427 397 };
428 398
429 Effect.Scale = Class.create();
430 Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
399 Effect.Scale = Class.create(Effect.Base, {
431 400 initialize: function(element, percent) {
432 401 this.element = $(element);
433 if(!this.element) throw(Effect._elementDoesNotExistError);
402 if (!this.element) throw(Effect._elementDoesNotExistError);
434 403 var options = Object.extend({
435 404 scaleX: true,
436 405 scaleY: true,
437 406 scaleContent: true,
438 407 scaleFromCenter: false,
439 scaleMode: 'box', // 'box' or 'contents' or {} with provided values
408 scaleMode: 'box', // 'box' or 'contents' or { } with provided values
440 409 scaleFrom: 100.0,
441 410 scaleTo: percent
442 }, arguments[2] || {});
411 }, arguments[2] || { });
443 412 this.start(options);
444 413 },
445 414 setup: function() {
446 415 this.restoreAfterFinish = this.options.restoreAfterFinish || false;
447 416 this.elementPositioning = this.element.getStyle('position');
448 417
449 this.originalStyle = {};
418 this.originalStyle = { };
450 419 ['top','left','width','height','fontSize'].each( function(k) {
451 420 this.originalStyle[k] = this.element.style[k];
452 421 }.bind(this));
453 422
454 423 this.originalTop = this.element.offsetTop;
455 424 this.originalLeft = this.element.offsetLeft;
456 425
457 426 var fontSize = this.element.getStyle('font-size') || '100%';
458 427 ['em','px','%','pt'].each( function(fontSizeType) {
459 if(fontSize.indexOf(fontSizeType)>0) {
428 if (fontSize.indexOf(fontSizeType)>0) {
460 429 this.fontSize = parseFloat(fontSize);
461 430 this.fontSizeType = fontSizeType;
462 431 }
463 432 }.bind(this));
464 433
465 434 this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
466 435
467 436 this.dims = null;
468 if(this.options.scaleMode=='box')
437 if (this.options.scaleMode=='box')
469 438 this.dims = [this.element.offsetHeight, this.element.offsetWidth];
470 if(/^content/.test(this.options.scaleMode))
439 if (/^content/.test(this.options.scaleMode))
471 440 this.dims = [this.element.scrollHeight, this.element.scrollWidth];
472 if(!this.dims)
441 if (!this.dims)
473 442 this.dims = [this.options.scaleMode.originalHeight,
474 443 this.options.scaleMode.originalWidth];
475 444 },
476 445 update: function(position) {
477 446 var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
478 if(this.options.scaleContent && this.fontSize)
447 if (this.options.scaleContent && this.fontSize)
479 448 this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
480 449 this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
481 450 },
482 451 finish: function(position) {
483 if(this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
452 if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
484 453 },
485 454 setDimensions: function(height, width) {
486 var d = {};
487 if(this.options.scaleX) d.width = Math.round(width) + 'px';
488 if(this.options.scaleY) d.height = Math.round(height) + 'px';
489 if(this.options.scaleFromCenter) {
455 var d = { };
456 if (this.options.scaleX) d.width = width.round() + 'px';
457 if (this.options.scaleY) d.height = height.round() + 'px';
458 if (this.options.scaleFromCenter) {
490 459 var topd = (height - this.dims[0])/2;
491 460 var leftd = (width - this.dims[1])/2;
492 if(this.elementPositioning == 'absolute') {
493 if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
494 if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
461 if (this.elementPositioning == 'absolute') {
462 if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
463 if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
495 464 } else {
496 if(this.options.scaleY) d.top = -topd + 'px';
497 if(this.options.scaleX) d.left = -leftd + 'px';
465 if (this.options.scaleY) d.top = -topd + 'px';
466 if (this.options.scaleX) d.left = -leftd + 'px';
498 467 }
499 468 }
500 469 this.element.setStyle(d);
501 470 }
502 471 });
503 472
504 Effect.Highlight = Class.create();
505 Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
473 Effect.Highlight = Class.create(Effect.Base, {
506 474 initialize: function(element) {
507 475 this.element = $(element);
508 if(!this.element) throw(Effect._elementDoesNotExistError);
509 var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
476 if (!this.element) throw(Effect._elementDoesNotExistError);
477 var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
510 478 this.start(options);
511 479 },
512 480 setup: function() {
513 481 // Prevent executing on elements not in the layout flow
514 if(this.element.getStyle('display')=='none') { this.cancel(); return; }
482 if (this.element.getStyle('display')=='none') { this.cancel(); return; }
515 483 // Disable background image during the effect
516 this.oldStyle = {
517 backgroundImage: this.element.getStyle('background-image') };
518 this.element.setStyle({backgroundImage: 'none'});
519 if(!this.options.endcolor)
484 this.oldStyle = { };
485 if (!this.options.keepBackgroundImage) {
486 this.oldStyle.backgroundImage = this.element.getStyle('background-image');
487 this.element.setStyle({backgroundImage: 'none'});
488 }
489 if (!this.options.endcolor)
520 490 this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
521 if(!this.options.restorecolor)
491 if (!this.options.restorecolor)
522 492 this.options.restorecolor = this.element.getStyle('background-color');
523 493 // init color calculations
524 494 this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
525 495 this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
526 496 },
527 497 update: function(position) {
528 498 this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
529 return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
499 return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
530 500 },
531 501 finish: function() {
532 502 this.element.setStyle(Object.extend(this.oldStyle, {
533 503 backgroundColor: this.options.restorecolor
534 504 }));
535 505 }
536 506 });
537 507
538 Effect.ScrollTo = Class.create();
539 Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
540 initialize: function(element) {
541 this.element = $(element);
542 this.start(arguments[1] || {});
543 },
544 setup: function() {
545 Position.prepare();
546 var offsets = Position.cumulativeOffset(this.element);
547 if(this.options.offset) offsets[1] += this.options.offset;
548 var max = window.innerHeight ?
549 window.height - window.innerHeight :
550 document.body.scrollHeight -
551 (document.documentElement.clientHeight ?
552 document.documentElement.clientHeight : document.body.clientHeight);
553 this.scrollStart = Position.deltaY;
554 this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
555 },
556 update: function(position) {
557 Position.prepare();
558 window.scrollTo(Position.deltaX,
559 this.scrollStart + (position*this.delta));
560 }
561 });
508 Effect.ScrollTo = function(element) {
509 var options = arguments[1] || { },
510 scrollOffsets = document.viewport.getScrollOffsets(),
511 elementOffsets = $(element).cumulativeOffset(),
512 max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();
513
514 if (options.offset) elementOffsets[1] += options.offset;
515
516 return new Effect.Tween(null,
517 scrollOffsets.top,
518 elementOffsets[1] > max ? max : elementOffsets[1],
519 options,
520 function(p){ scrollTo(scrollOffsets.left, p.round()) }
521 );
522 };
562 523
563 524 /* ------------- combination effects ------------- */
564 525
565 526 Effect.Fade = function(element) {
566 527 element = $(element);
567 528 var oldOpacity = element.getInlineOpacity();
568 529 var options = Object.extend({
569 from: element.getOpacity() || 1.0,
570 to: 0.0,
571 afterFinishInternal: function(effect) {
572 if(effect.options.to!=0) return;
573 effect.element.hide().setStyle({opacity: oldOpacity});
574 }}, arguments[1] || {});
530 from: element.getOpacity() || 1.0,
531 to: 0.0,
532 afterFinishInternal: function(effect) {
533 if (effect.options.to!=0) return;
534 effect.element.hide().setStyle({opacity: oldOpacity});
535 }
536 }, arguments[1] || { });
575 537 return new Effect.Opacity(element,options);
576 }
538 };
577 539
578 540 Effect.Appear = function(element) {
579 541 element = $(element);
580 542 var options = Object.extend({
581 543 from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
582 544 to: 1.0,
583 545 // force Safari to render floated elements properly
584 546 afterFinishInternal: function(effect) {
585 547 effect.element.forceRerendering();
586 548 },
587 549 beforeSetup: function(effect) {
588 550 effect.element.setOpacity(effect.options.from).show();
589 }}, arguments[1] || {});
551 }}, arguments[1] || { });
590 552 return new Effect.Opacity(element,options);
591 }
553 };
592 554
593 555 Effect.Puff = function(element) {
594 556 element = $(element);
595 557 var oldStyle = {
596 558 opacity: element.getInlineOpacity(),
597 559 position: element.getStyle('position'),
598 560 top: element.style.top,
599 561 left: element.style.left,
600 562 width: element.style.width,
601 563 height: element.style.height
602 564 };
603 565 return new Effect.Parallel(
604 566 [ new Effect.Scale(element, 200,
605 567 { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
606 568 new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
607 569 Object.extend({ duration: 1.0,
608 570 beforeSetupInternal: function(effect) {
609 571 Position.absolutize(effect.effects[0].element)
610 572 },
611 573 afterFinishInternal: function(effect) {
612 574 effect.effects[0].element.hide().setStyle(oldStyle); }
613 }, arguments[1] || {})
575 }, arguments[1] || { })
614 576 );
615 }
577 };
616 578
617 579 Effect.BlindUp = function(element) {
618 580 element = $(element);
619 581 element.makeClipping();
620 582 return new Effect.Scale(element, 0,
621 583 Object.extend({ scaleContent: false,
622 584 scaleX: false,
623 585 restoreAfterFinish: true,
624 586 afterFinishInternal: function(effect) {
625 587 effect.element.hide().undoClipping();
626 588 }
627 }, arguments[1] || {})
589 }, arguments[1] || { })
628 590 );
629 }
591 };
630 592
631 593 Effect.BlindDown = function(element) {
632 594 element = $(element);
633 595 var elementDimensions = element.getDimensions();
634 596 return new Effect.Scale(element, 100, Object.extend({
635 597 scaleContent: false,
636 598 scaleX: false,
637 599 scaleFrom: 0,
638 600 scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
639 601 restoreAfterFinish: true,
640 602 afterSetup: function(effect) {
641 603 effect.element.makeClipping().setStyle({height: '0px'}).show();
642 604 },
643 605 afterFinishInternal: function(effect) {
644 606 effect.element.undoClipping();
645 607 }
646 }, arguments[1] || {}));
647 }
608 }, arguments[1] || { }));
609 };
648 610
649 611 Effect.SwitchOff = function(element) {
650 612 element = $(element);
651 613 var oldOpacity = element.getInlineOpacity();
652 614 return new Effect.Appear(element, Object.extend({
653 615 duration: 0.4,
654 616 from: 0,
655 617 transition: Effect.Transitions.flicker,
656 618 afterFinishInternal: function(effect) {
657 619 new Effect.Scale(effect.element, 1, {
658 620 duration: 0.3, scaleFromCenter: true,
659 621 scaleX: false, scaleContent: false, restoreAfterFinish: true,
660 622 beforeSetup: function(effect) {
661 623 effect.element.makePositioned().makeClipping();
662 624 },
663 625 afterFinishInternal: function(effect) {
664 626 effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
665 627 }
666 628 })
667 629 }
668 }, arguments[1] || {}));
669 }
630 }, arguments[1] || { }));
631 };
670 632
671 633 Effect.DropOut = function(element) {
672 634 element = $(element);
673 635 var oldStyle = {
674 636 top: element.getStyle('top'),
675 637 left: element.getStyle('left'),
676 638 opacity: element.getInlineOpacity() };
677 639 return new Effect.Parallel(
678 640 [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
679 641 new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
680 642 Object.extend(
681 643 { duration: 0.5,
682 644 beforeSetup: function(effect) {
683 645 effect.effects[0].element.makePositioned();
684 646 },
685 647 afterFinishInternal: function(effect) {
686 648 effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
687 649 }
688 }, arguments[1] || {}));
689 }
650 }, arguments[1] || { }));
651 };
690 652
691 653 Effect.Shake = function(element) {
692 654 element = $(element);
655 var options = Object.extend({
656 distance: 20,
657 duration: 0.5
658 }, arguments[1] || {});
659 var distance = parseFloat(options.distance);
660 var split = parseFloat(options.duration) / 10.0;
693 661 var oldStyle = {
694 662 top: element.getStyle('top'),
695 663 left: element.getStyle('left') };
696 return new Effect.Move(element,
697 { x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
664 return new Effect.Move(element,
665 { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) {
698 666 new Effect.Move(effect.element,
699 { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
667 { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
700 668 new Effect.Move(effect.element,
701 { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
669 { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
702 670 new Effect.Move(effect.element,
703 { x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
671 { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
704 672 new Effect.Move(effect.element,
705 { x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
673 { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) {
706 674 new Effect.Move(effect.element,
707 { x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
675 { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
708 676 effect.element.undoPositioned().setStyle(oldStyle);
709 677 }}) }}) }}) }}) }}) }});
710 }
678 };
711 679
712 680 Effect.SlideDown = function(element) {
713 681 element = $(element).cleanWhitespace();
714 682 // SlideDown need to have the content of the element wrapped in a container element with fixed height!
715 683 var oldInnerBottom = element.down().getStyle('bottom');
716 684 var elementDimensions = element.getDimensions();
717 685 return new Effect.Scale(element, 100, Object.extend({
718 686 scaleContent: false,
719 687 scaleX: false,
720 688 scaleFrom: window.opera ? 0 : 1,
721 689 scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
722 690 restoreAfterFinish: true,
723 691 afterSetup: function(effect) {
724 692 effect.element.makePositioned();
725 693 effect.element.down().makePositioned();
726 if(window.opera) effect.element.setStyle({top: ''});
694 if (window.opera) effect.element.setStyle({top: ''});
727 695 effect.element.makeClipping().setStyle({height: '0px'}).show();
728 696 },
729 697 afterUpdateInternal: function(effect) {
730 698 effect.element.down().setStyle({bottom:
731 699 (effect.dims[0] - effect.element.clientHeight) + 'px' });
732 700 },
733 701 afterFinishInternal: function(effect) {
734 702 effect.element.undoClipping().undoPositioned();
735 703 effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
736 }, arguments[1] || {})
704 }, arguments[1] || { })
737 705 );
738 }
706 };
739 707
740 708 Effect.SlideUp = function(element) {
741 709 element = $(element).cleanWhitespace();
742 710 var oldInnerBottom = element.down().getStyle('bottom');
711 var elementDimensions = element.getDimensions();
743 712 return new Effect.Scale(element, window.opera ? 0 : 1,
744 713 Object.extend({ scaleContent: false,
745 714 scaleX: false,
746 715 scaleMode: 'box',
747 716 scaleFrom: 100,
717 scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
748 718 restoreAfterFinish: true,
749 beforeStartInternal: function(effect) {
719 afterSetup: function(effect) {
750 720 effect.element.makePositioned();
751 721 effect.element.down().makePositioned();
752 if(window.opera) effect.element.setStyle({top: ''});
722 if (window.opera) effect.element.setStyle({top: ''});
753 723 effect.element.makeClipping().show();
754 724 },
755 725 afterUpdateInternal: function(effect) {
756 726 effect.element.down().setStyle({bottom:
757 727 (effect.dims[0] - effect.element.clientHeight) + 'px' });
758 728 },
759 729 afterFinishInternal: function(effect) {
760 effect.element.hide().undoClipping().undoPositioned().setStyle({bottom: oldInnerBottom});
761 effect.element.down().undoPositioned();
730 effect.element.hide().undoClipping().undoPositioned();
731 effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
762 732 }
763 }, arguments[1] || {})
733 }, arguments[1] || { })
764 734 );
765 }
735 };
766 736
767 737 // Bug in opera makes the TD containing this element expand for a instance after finish
768 738 Effect.Squish = function(element) {
769 739 return new Effect.Scale(element, window.opera ? 1 : 0, {
770 740 restoreAfterFinish: true,
771 741 beforeSetup: function(effect) {
772 742 effect.element.makeClipping();
773 743 },
774 744 afterFinishInternal: function(effect) {
775 745 effect.element.hide().undoClipping();
776 746 }
777 747 });
778 }
748 };
779 749
780 750 Effect.Grow = function(element) {
781 751 element = $(element);
782 752 var options = Object.extend({
783 753 direction: 'center',
784 754 moveTransition: Effect.Transitions.sinoidal,
785 755 scaleTransition: Effect.Transitions.sinoidal,
786 756 opacityTransition: Effect.Transitions.full
787 }, arguments[1] || {});
757 }, arguments[1] || { });
788 758 var oldStyle = {
789 759 top: element.style.top,
790 760 left: element.style.left,
791 761 height: element.style.height,
792 762 width: element.style.width,
793 763 opacity: element.getInlineOpacity() };
794 764
795 765 var dims = element.getDimensions();
796 766 var initialMoveX, initialMoveY;
797 767 var moveX, moveY;
798 768
799 769 switch (options.direction) {
800 770 case 'top-left':
801 771 initialMoveX = initialMoveY = moveX = moveY = 0;
802 772 break;
803 773 case 'top-right':
804 774 initialMoveX = dims.width;
805 775 initialMoveY = moveY = 0;
806 776 moveX = -dims.width;
807 777 break;
808 778 case 'bottom-left':
809 779 initialMoveX = moveX = 0;
810 780 initialMoveY = dims.height;
811 781 moveY = -dims.height;
812 782 break;
813 783 case 'bottom-right':
814 784 initialMoveX = dims.width;
815 785 initialMoveY = dims.height;
816 786 moveX = -dims.width;
817 787 moveY = -dims.height;
818 788 break;
819 789 case 'center':
820 790 initialMoveX = dims.width / 2;
821 791 initialMoveY = dims.height / 2;
822 792 moveX = -dims.width / 2;
823 793 moveY = -dims.height / 2;
824 794 break;
825 795 }
826 796
827 797 return new Effect.Move(element, {
828 798 x: initialMoveX,
829 799 y: initialMoveY,
830 800 duration: 0.01,
831 801 beforeSetup: function(effect) {
832 802 effect.element.hide().makeClipping().makePositioned();
833 803 },
834 804 afterFinishInternal: function(effect) {
835 805 new Effect.Parallel(
836 806 [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
837 807 new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
838 808 new Effect.Scale(effect.element, 100, {
839 809 scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
840 810 sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
841 811 ], Object.extend({
842 812 beforeSetup: function(effect) {
843 813 effect.effects[0].element.setStyle({height: '0px'}).show();
844 814 },
845 815 afterFinishInternal: function(effect) {
846 816 effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
847 817 }
848 818 }, options)
849 819 )
850 820 }
851 821 });
852 }
822 };
853 823
854 824 Effect.Shrink = function(element) {
855 825 element = $(element);
856 826 var options = Object.extend({
857 827 direction: 'center',
858 828 moveTransition: Effect.Transitions.sinoidal,
859 829 scaleTransition: Effect.Transitions.sinoidal,
860 830 opacityTransition: Effect.Transitions.none
861 }, arguments[1] || {});
831 }, arguments[1] || { });
862 832 var oldStyle = {
863 833 top: element.style.top,
864 834 left: element.style.left,
865 835 height: element.style.height,
866 836 width: element.style.width,
867 837 opacity: element.getInlineOpacity() };
868 838
869 839 var dims = element.getDimensions();
870 840 var moveX, moveY;
871 841
872 842 switch (options.direction) {
873 843 case 'top-left':
874 844 moveX = moveY = 0;
875 845 break;
876 846 case 'top-right':
877 847 moveX = dims.width;
878 848 moveY = 0;
879 849 break;
880 850 case 'bottom-left':
881 851 moveX = 0;
882 852 moveY = dims.height;
883 853 break;
884 854 case 'bottom-right':
885 855 moveX = dims.width;
886 856 moveY = dims.height;
887 857 break;
888 858 case 'center':
889 859 moveX = dims.width / 2;
890 860 moveY = dims.height / 2;
891 861 break;
892 862 }
893 863
894 864 return new Effect.Parallel(
895 865 [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
896 866 new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
897 867 new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
898 868 ], Object.extend({
899 869 beforeStartInternal: function(effect) {
900 870 effect.effects[0].element.makePositioned().makeClipping();
901 871 },
902 872 afterFinishInternal: function(effect) {
903 873 effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
904 874 }, options)
905 875 );
906 }
876 };
907 877
908 878 Effect.Pulsate = function(element) {
909 879 element = $(element);
910 var options = arguments[1] || {};
880 var options = arguments[1] || { };
911 881 var oldOpacity = element.getInlineOpacity();
912 882 var transition = options.transition || Effect.Transitions.sinoidal;
913 883 var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos, options.pulses)) };
914 884 reverser.bind(transition);
915 885 return new Effect.Opacity(element,
916 886 Object.extend(Object.extend({ duration: 2.0, from: 0,
917 887 afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
918 888 }, options), {transition: reverser}));
919 }
889 };
920 890
921 891 Effect.Fold = function(element) {
922 892 element = $(element);
923 893 var oldStyle = {
924 894 top: element.style.top,
925 895 left: element.style.left,
926 896 width: element.style.width,
927 897 height: element.style.height };
928 898 element.makeClipping();
929 899 return new Effect.Scale(element, 5, Object.extend({
930 900 scaleContent: false,
931 901 scaleX: false,
932 902 afterFinishInternal: function(effect) {
933 903 new Effect.Scale(element, 1, {
934 904 scaleContent: false,
935 905 scaleY: false,
936 906 afterFinishInternal: function(effect) {
937 907 effect.element.hide().undoClipping().setStyle(oldStyle);
938 908 } });
939 }}, arguments[1] || {}));
909 }}, arguments[1] || { }));
940 910 };
941 911
942 Effect.Morph = Class.create();
943 Object.extend(Object.extend(Effect.Morph.prototype, Effect.Base.prototype), {
912 Effect.Morph = Class.create(Effect.Base, {
944 913 initialize: function(element) {
945 914 this.element = $(element);
946 if(!this.element) throw(Effect._elementDoesNotExistError);
915 if (!this.element) throw(Effect._elementDoesNotExistError);
947 916 var options = Object.extend({
948 style: ''
949 }, arguments[1] || {});
917 style: { }
918 }, arguments[1] || { });
919
920 if (!Object.isString(options.style)) this.style = $H(options.style);
921 else {
922 if (options.style.include(':'))
923 this.style = options.style.parseStyle();
924 else {
925 this.element.addClassName(options.style);
926 this.style = $H(this.element.getStyles());
927 this.element.removeClassName(options.style);
928 var css = this.element.getStyles();
929 this.style = this.style.reject(function(style) {
930 return style.value == css[style.key];
931 });
932 options.afterFinishInternal = function(effect) {
933 effect.element.addClassName(effect.options.style);
934 effect.transforms.each(function(transform) {
935 effect.element.style[transform.style] = '';
936 });
937 }
938 }
939 }
950 940 this.start(options);
951 941 },
942
952 943 setup: function(){
953 944 function parseColor(color){
954 if(!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
945 if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
955 946 color = color.parseColor();
956 947 return $R(0,2).map(function(i){
957 948 return parseInt( color.slice(i*2+1,i*2+3), 16 )
958 949 });
959 950 }
960 this.transforms = this.options.style.parseStyle().map(function(property){
961 var originalValue = this.element.getStyle(property[0]);
962 return $H({
963 style: property[0],
964 originalValue: property[1].unit=='color' ?
965 parseColor(originalValue) : parseFloat(originalValue || 0),
966 targetValue: property[1].unit=='color' ?
967 parseColor(property[1].value) : property[1].value,
968 unit: property[1].unit
969 });
951 this.transforms = this.style.map(function(pair){
952 var property = pair[0], value = pair[1], unit = null;
953
954 if (value.parseColor('#zzzzzz') != '#zzzzzz') {
955 value = value.parseColor();
956 unit = 'color';
957 } else if (property == 'opacity') {
958 value = parseFloat(value);
959 if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
960 this.element.setStyle({zoom: 1});
961 } else if (Element.CSS_LENGTH.test(value)) {
962 var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
963 value = parseFloat(components[1]);
964 unit = (components.length == 3) ? components[2] : null;
965 }
966
967 var originalValue = this.element.getStyle(property);
968 return {
969 style: property.camelize(),
970 originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
971 targetValue: unit=='color' ? parseColor(value) : value,
972 unit: unit
973 };
970 974 }.bind(this)).reject(function(transform){
971 975 return (
972 976 (transform.originalValue == transform.targetValue) ||
973 977 (
974 978 transform.unit != 'color' &&
975 979 (isNaN(transform.originalValue) || isNaN(transform.targetValue))
976 980 )
977 981 )
978 982 });
979 983 },
980 984 update: function(position) {
981 var style = $H(), value = null;
982 this.transforms.each(function(transform){
983 value = transform.unit=='color' ?
984 $R(0,2).inject('#',function(m,v,i){
985 return m+(Math.round(transform.originalValue[i]+
986 (transform.targetValue[i] - transform.originalValue[i])*position)).toColorPart() }) :
987 transform.originalValue + Math.round(
988 ((transform.targetValue - transform.originalValue) * position) * 1000)/1000 + transform.unit;
989 style[transform.style] = value;
990 });
991 this.element.setStyle(style);
985 var style = { }, transform, i = this.transforms.length;
986 while(i--)
987 style[(transform = this.transforms[i]).style] =
988 transform.unit=='color' ? '#'+
989 (Math.round(transform.originalValue[0]+
990 (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
991 (Math.round(transform.originalValue[1]+
992 (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
993 (Math.round(transform.originalValue[2]+
994 (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
995 (transform.originalValue +
996 (transform.targetValue - transform.originalValue) * position).toFixed(3) +
997 (transform.unit === null ? '' : transform.unit);
998 this.element.setStyle(style, true);
992 999 }
993 1000 });
994 1001
995 Effect.Transform = Class.create();
996 Object.extend(Effect.Transform.prototype, {
1002 Effect.Transform = Class.create({
997 1003 initialize: function(tracks){
998 1004 this.tracks = [];
999 this.options = arguments[1] || {};
1005 this.options = arguments[1] || { };
1000 1006 this.addTracks(tracks);
1001 1007 },
1002 1008 addTracks: function(tracks){
1003 1009 tracks.each(function(track){
1004 var data = $H(track).values().first();
1010 track = $H(track);
1011 var data = track.values().first();
1005 1012 this.tracks.push($H({
1006 ids: $H(track).keys().first(),
1013 ids: track.keys().first(),
1007 1014 effect: Effect.Morph,
1008 1015 options: { style: data }
1009 1016 }));
1010 1017 }.bind(this));
1011 1018 return this;
1012 1019 },
1013 1020 play: function(){
1014 1021 return new Effect.Parallel(
1015 1022 this.tracks.map(function(track){
1016 var elements = [$(track.ids) || $$(track.ids)].flatten();
1017 return elements.map(function(e){ return new track.effect(e, Object.extend({ sync:true }, track.options)) });
1023 var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
1024 var elements = [$(ids) || $$(ids)].flatten();
1025 return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
1018 1026 }).flatten(),
1019 1027 this.options
1020 1028 );
1021 1029 }
1022 1030 });
1023 1031
1024 Element.CSS_PROPERTIES = ['azimuth', 'backgroundAttachment', 'backgroundColor', 'backgroundImage',
1025 'backgroundPosition', 'backgroundRepeat', 'borderBottomColor', 'borderBottomStyle',
1026 'borderBottomWidth', 'borderCollapse', 'borderLeftColor', 'borderLeftStyle', 'borderLeftWidth',
1027 'borderRightColor', 'borderRightStyle', 'borderRightWidth', 'borderSpacing', 'borderTopColor',
1028 'borderTopStyle', 'borderTopWidth', 'bottom', 'captionSide', 'clear', 'clip', 'color', 'content',
1029 'counterIncrement', 'counterReset', 'cssFloat', 'cueAfter', 'cueBefore', 'cursor', 'direction',
1030 'display', 'elevation', 'emptyCells', 'fontFamily', 'fontSize', 'fontSizeAdjust', 'fontStretch',
1031 'fontStyle', 'fontVariant', 'fontWeight', 'height', 'left', 'letterSpacing', 'lineHeight',
1032 'listStyleImage', 'listStylePosition', 'listStyleType', 'marginBottom', 'marginLeft', 'marginRight',
1033 'marginTop', 'markerOffset', 'marks', 'maxHeight', 'maxWidth', 'minHeight', 'minWidth', 'opacity',
1034 'orphans', 'outlineColor', 'outlineOffset', 'outlineStyle', 'outlineWidth', 'overflowX', 'overflowY',
1035 'paddingBottom', 'paddingLeft', 'paddingRight', 'paddingTop', 'page', 'pageBreakAfter', 'pageBreakBefore',
1036 'pageBreakInside', 'pauseAfter', 'pauseBefore', 'pitch', 'pitchRange', 'position', 'quotes',
1037 'richness', 'right', 'size', 'speakHeader', 'speakNumeral', 'speakPunctuation', 'speechRate', 'stress',
1038 'tableLayout', 'textAlign', 'textDecoration', 'textIndent', 'textShadow', 'textTransform', 'top',
1039 'unicodeBidi', 'verticalAlign', 'visibility', 'voiceFamily', 'volume', 'whiteSpace', 'widows',
1040 'width', 'wordSpacing', 'zIndex'];
1032 Element.CSS_PROPERTIES = $w(
1033 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
1034 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
1035 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
1036 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
1037 'fontSize fontWeight height left letterSpacing lineHeight ' +
1038 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
1039 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
1040 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
1041 'right textIndent top width wordSpacing zIndex');
1041 1042
1042 1043 Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;
1043 1044
1045 String.__parseStyleElement = document.createElement('div');
1044 1046 String.prototype.parseStyle = function(){
1045 var element = Element.extend(document.createElement('div'));
1046 element.innerHTML = '<div style="' + this + '"></div>';
1047 var style = element.down().style, styleRules = $H();
1047 var style, styleRules = $H();
1048 if (Prototype.Browser.WebKit)
1049 style = new Element('div',{style:this}).style;
1050 else {
1051 String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
1052 style = String.__parseStyleElement.childNodes[0].style;
1053 }
1048 1054
1049 1055 Element.CSS_PROPERTIES.each(function(property){
1050 if(style[property]) styleRules[property] = style[property];
1056 if (style[property]) styleRules.set(property, style[property]);
1051 1057 });
1052 1058
1053 var result = $H();
1054
1055 styleRules.each(function(pair){
1056 var property = pair[0], value = pair[1], unit = null;
1057
1058 if(value.parseColor('#zzzzzz') != '#zzzzzz') {
1059 value = value.parseColor();
1060 unit = 'color';
1061 } else if(Element.CSS_LENGTH.test(value))
1062 var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/),
1063 value = parseFloat(components[1]), unit = (components.length == 3) ? components[2] : null;
1064
1065 result[property.underscore().dasherize()] = $H({ value:value, unit:unit });
1066 }.bind(this));
1067
1068 return result;
1059 if (Prototype.Browser.IE && this.include('opacity'))
1060 styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);
1061
1062 return styleRules;
1069 1063 };
1070 1064
1071 Element.morph = function(element, style) {
1072 new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || {}));
1073 return element;
1065 if (document.defaultView && document.defaultView.getComputedStyle) {
1066 Element.getStyles = function(element) {
1067 var css = document.defaultView.getComputedStyle($(element), null);
1068 return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
1069 styles[property] = css[property];
1070 return styles;
1071 });
1072 };
1073 } else {
1074 Element.getStyles = function(element) {
1075 element = $(element);
1076 var css = element.currentStyle, styles;
1077 styles = Element.CSS_PROPERTIES.inject({ }, function(hash, property) {
1078 hash.set(property, css[property]);
1079 return hash;
1080 });
1081 if (!styles.opacity) styles.set('opacity', element.getOpacity());
1082 return styles;
1083 };
1074 1084 };
1075 1085
1076 ['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
1077 'collectTextNodes','collectTextNodesIgnoreClass','morph'].each(
1078 function(f) { Element.Methods[f] = Element[f]; }
1086 Effect.Methods = {
1087 morph: function(element, style) {
1088 element = $(element);
1089 new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
1090 return element;
1091 },
1092 visualEffect: function(element, effect, options) {
1093 element = $(element)
1094 var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
1095 new Effect[klass](element, options);
1096 return element;
1097 },
1098 highlight: function(element, options) {
1099 element = $(element);
1100 new Effect.Highlight(element, options);
1101 return element;
1102 }
1103 };
1104
1105 $w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
1106 'pulsate shake puff squish switchOff dropOut').each(
1107 function(effect) {
1108 Effect.Methods[effect] = function(element, options){
1109 element = $(element);
1110 Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
1111 return element;
1112 }
1113 }
1079 1114 );
1080 1115
1081 Element.Methods.visualEffect = function(element, effect, options) {
1082 s = effect.gsub(/_/, '-').camelize();
1083 effect_class = s.charAt(0).toUpperCase() + s.substring(1);
1084 new Effect[effect_class](element, options);
1085 return $(element);
1086 };
1116 $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
1117 function(f) { Effect.Methods[f] = Element[f]; }
1118 );
1087 1119
1088 Element.addMethods(); No newline at end of file
1120 Element.addMethods(Effect.Methods);
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now