Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 86
- Log:
Initial import of I2, an Instiki clone.
- Author:
- adh
- Date:
- Mon Oct 16 10:40:36 +0100 2006
- Size:
- 15793 Bytes
1 | // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) |
2 | // (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan) |
3 | // |
4 | // Permission is hereby granted, free of charge, to any person obtaining |
5 | // a copy of this software and associated documentation files (the |
6 | // "Software"), to deal in the Software without restriction, including |
7 | // without limitation the rights to use, copy, modify, merge, publish, |
8 | // distribute, sublicense, and/or sell copies of the Software, and to |
9 | // permit persons to whom the Software is furnished to do so, subject to |
10 | // the following conditions: |
11 | // |
12 | // The above copyright notice and this permission notice shall be |
13 | // included in all copies or substantial portions of the Software. |
14 | // |
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
16 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
17 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
18 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
19 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
20 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
21 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
22 | |
23 | Element.collectTextNodesIgnoreClass = function(element, ignoreclass) { |
24 | var children = $(element).childNodes; |
25 | var text = ""; |
26 | var classtest = new RegExp("^([^ ]+ )*" + ignoreclass+ "( [^ ]+)*$","i"); |
27 | |
28 | for (var i = 0; i < children.length; i++) { |
29 | if(children[i].nodeType==3) { |
30 | text+=children[i].nodeValue; |
31 | } else { |
32 | if((!children[i].className.match(classtest)) && children[i].hasChildNodes()) |
33 | text += Element.collectTextNodesIgnoreClass(children[i], ignoreclass); |
34 | } |
35 | } |
36 | |
37 | return text; |
38 | } |
39 | |
40 | // Autocompleter.Base handles all the autocompletion functionality |
41 | // that's independent of the data source for autocompletion. This |
42 | // includes drawing the autocompletion menu, observing keyboard |
43 | // and mouse events, and similar. |
44 | // |
45 | // Specific autocompleters need to provide, at the very least, |
46 | // a getUpdatedChoices function that will be invoked every time |
47 | // the text inside the monitored textbox changes. This method |
48 | // should get the text for which to provide autocompletion by |
49 | // invoking this.getEntry(), NOT by directly accessing |
50 | // this.element.value. This is to allow incremental tokenized |
51 | // autocompletion. Specific auto-completion logic (AJAX, etc) |
52 | // belongs in getUpdatedChoices. |
53 | // |
54 | // Tokenized incremental autocompletion is enabled automatically |
55 | // when an autocompleter is instantiated with the 'tokens' option |
56 | // in the options parameter, e.g.: |
57 | // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' }); |
58 | // will incrementally autocomplete with a comma as the token. |
59 | // Additionally, ',' in the above example can be replaced with |
60 | // a token array, e.g. { tokens: new Array (',', '\n') } which |
61 | // enables autocompletion on multiple tokens. This is most |
62 | // useful when one of the tokens is \n (a newline), as it |
63 | // allows smart autocompletion after linebreaks. |
64 | |
65 | var Autocompleter = {} |
66 | Autocompleter.Base = function() {}; |
67 | Autocompleter.Base.prototype = { |
68 | base_initialize: function(element, update, options) { |
69 | this.element = $(element); |
70 | this.update = $(update); |
71 | this.has_focus = false; |
72 | this.changed = false; |
73 | this.active = false; |
74 | this.index = 0; |
75 | this.entry_count = 0; |
76 | |
77 | if (this.setOptions) |
78 | this.setOptions(options); |
79 | else |
80 | this.options = {} |
81 | |
82 | this.options.tokens = this.options.tokens || new Array(); |
83 | this.options.frequency = this.options.frequency || 0.4; |
84 | this.options.min_chars = this.options.min_chars || 1; |
85 | this.options.onShow = this.options.onShow || |
86 | function(element, update){ |
87 | if(!update.style.position || update.style.position=='absolute') { |
88 | update.style.position = 'absolute'; |
89 | var offsets = Position.cumulativeOffset(element); |
90 | update.style.left = offsets[0] + 'px'; |
91 | update.style.top = (offsets[1] + element.offsetHeight) + 'px'; |
92 | update.style.width = element.offsetWidth + 'px'; |
93 | } |
94 | new Effect.Appear(update,{duration:0.15}); |
95 | }; |
96 | this.options.onHide = this.options.onHide || |
97 | function(element, update){ new Effect.Fade(update,{duration:0.15}) }; |
98 | |
99 | if(this.options.indicator) |
100 | this.indicator = $(this.options.indicator); |
101 | |
102 | if (typeof(this.options.tokens) == 'string') |
103 | this.options.tokens = new Array(this.options.tokens); |
104 | |
105 | this.observer = null; |
106 | |
107 | Element.hide(this.update); |
108 | |
109 | Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this)); |
110 | Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this)); |
111 | }, |
112 | |
113 | show: function() { |
114 | if(this.update.style.display=='none') this.options.onShow(this.element, this.update); |
115 | if(!this.iefix && (navigator.appVersion.indexOf('MSIE')>0) && this.update.style.position=='absolute') { |
116 | new Insertion.After(this.update, |
117 | '<iframe id="' + this.update.id + '_iefix" '+ |
118 | 'style="display:none;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' + |
119 | 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>'); |
120 | this.iefix = $(this.update.id+'_iefix'); |
121 | } |
122 | if(this.iefix) { |
123 | Position.clone(this.update, this.iefix); |
124 | this.iefix.style.zIndex = 1; |
125 | this.update.style.zIndex = 2; |
126 | Element.show(this.iefix); |
127 | } |
128 | }, |
129 | |
130 | hide: function() { |
131 | if(this.update.style.display=='') this.options.onHide(this.element, this.update); |
132 | if(this.iefix) Element.hide(this.iefix); |
133 | }, |
134 | |
135 | startIndicator: function() { |
136 | if(this.indicator) Element.show(this.indicator); |
137 | }, |
138 | |
139 | stopIndicator: function() { |
140 | if(this.indicator) Element.hide(this.indicator); |
141 | }, |
142 | |
143 | onKeyPress: function(event) { |
144 | if(this.active) |
145 | switch(event.keyCode) { |
146 | case Event.KEY_TAB: |
147 | case Event.KEY_RETURN: |
148 | this.select_entry(); |
149 | Event.stop(event); |
150 | case Event.KEY_ESC: |
151 | this.hide(); |
152 | this.active = false; |
153 | return; |
154 | case Event.KEY_LEFT: |
155 | case Event.KEY_RIGHT: |
156 | return; |
157 | case Event.KEY_UP: |
158 | this.mark_previous(); |
159 | this.render(); |
160 | if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); |
161 | return; |
162 | case Event.KEY_DOWN: |
163 | this.mark_next(); |
164 | this.render(); |
165 | if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event); |
166 | return; |
167 | } |
168 | else |
169 | if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) |
170 | return; |
171 | |
172 | this.changed = true; |
173 | this.has_focus = true; |
174 | |
175 | if(this.observer) clearTimeout(this.observer); |
176 | this.observer = |
177 | setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000); |
178 | }, |
179 | |
180 | onHover: function(event) { |
181 | var element = Event.findElement(event, 'LI'); |
182 | if(this.index != element.autocompleteIndex) |
183 | { |
184 | this.index = element.autocompleteIndex; |
185 | this.render(); |
186 | } |
187 | Event.stop(event); |
188 | }, |
189 | |
190 | onClick: function(event) { |
191 | var element = Event.findElement(event, 'LI'); |
192 | this.index = element.autocompleteIndex; |
193 | this.select_entry(); |
194 | Event.stop(event); |
195 | }, |
196 | |
197 | onBlur: function(event) { |
198 | // needed to make click events working |
199 | setTimeout(this.hide.bind(this), 250); |
200 | this.has_focus = false; |
201 | this.active = false; |
202 | }, |
203 | |
204 | render: function() { |
205 | if(this.entry_count > 0) { |
206 | for (var i = 0; i < this.entry_count; i++) |
207 | this.index==i ? |
208 | Element.addClassName(this.get_entry(i),"selected") : |
209 | Element.removeClassName(this.get_entry(i),"selected"); |
210 | |
211 | if(this.has_focus) { |
212 | if(this.get_current_entry().scrollIntoView) |
213 | this.get_current_entry().scrollIntoView(false); |
214 | |
215 | this.show(); |
216 | this.active = true; |
217 | } |
218 | } else this.hide(); |
219 | }, |
220 | |
221 | mark_previous: function() { |
222 | if(this.index > 0) this.index-- |
223 | else this.index = this.entry_count-1; |
224 | }, |
225 | |
226 | mark_next: function() { |
227 | if(this.index < this.entry_count-1) this.index++ |
228 | else this.index = 0; |
229 | }, |
230 | |
231 | get_entry: function(index) { |
232 | return this.update.firstChild.childNodes[index]; |
233 | }, |
234 | |
235 | get_current_entry: function() { |
236 | return this.get_entry(this.index); |
237 | }, |
238 | |
239 | select_entry: function() { |
240 | this.active = false; |
241 | value = Element.collectTextNodesIgnoreClass(this.get_current_entry(), 'informal').unescapeHTML(); |
242 | this.updateElement(value); |
243 | this.element.focus(); |
244 | }, |
245 | |
246 | updateElement: function(value) { |
247 | var last_token_pos = this.findLastToken(); |
248 | if (last_token_pos != -1) { |
249 | var new_value = this.element.value.substr(0, last_token_pos + 1); |
250 | var whitespace = this.element.value.substr(last_token_pos + 1).match(/^\s+/); |
251 | if (whitespace) |
252 | new_value += whitespace[0]; |
253 | this.element.value = new_value + value; |
254 | } else { |
255 | this.element.value = value; |
256 | } |
257 | }, |
258 | |
259 | updateChoices: function(choices) { |
260 | if(!this.changed && this.has_focus) { |
261 | this.update.innerHTML = choices; |
262 | Element.cleanWhitespace(this.update); |
263 | Element.cleanWhitespace(this.update.firstChild); |
264 | |
265 | if(this.update.firstChild && this.update.firstChild.childNodes) { |
266 | this.entry_count = |
267 | this.update.firstChild.childNodes.length; |
268 | for (var i = 0; i < this.entry_count; i++) { |
269 | entry = this.get_entry(i); |
270 | entry.autocompleteIndex = i; |
271 | this.addObservers(entry); |
272 | } |
273 | } else { |
274 | this.entry_count = 0; |
275 | } |
276 | |
277 | this.stopIndicator(); |
278 | |
279 | this.index = 0; |
280 | this.render(); |
281 | } |
282 | }, |
283 | |
284 | addObservers: function(element) { |
285 | Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this)); |
286 | Event.observe(element, "click", this.onClick.bindAsEventListener(this)); |
287 | }, |
288 | |
289 | onObserverEvent: function() { |
290 | this.changed = false; |
291 | if(this.getEntry().length>=this.options.min_chars) { |
292 | this.startIndicator(); |
293 | this.getUpdatedChoices(); |
294 | } else { |
295 | this.active = false; |
296 | this.hide(); |
297 | } |
298 | }, |
299 | |
300 | getEntry: function() { |
301 | var token_pos = this.findLastToken(); |
302 | if (token_pos != -1) |
303 | var ret = this.element.value.substr(token_pos + 1).replace(/^\s+/,'').replace(/\s+$/,''); |
304 | else |
305 | var ret = this.element.value; |
306 | |
307 | return /\n/.test(ret) ? '' : ret; |
308 | }, |
309 | |
310 | findLastToken: function() { |
311 | var last_token_pos = -1; |
312 | |
313 | for (var i=0; i<this.options.tokens.length; i++) { |
314 | var this_token_pos = this.element.value.lastIndexOf(this.options.tokens[i]); |
315 | if (this_token_pos > last_token_pos) |
316 | last_token_pos = this_token_pos; |
317 | } |
318 | return last_token_pos; |
319 | } |
320 | } |
321 | |
322 | Ajax.Autocompleter = Class.create(); |
323 | Ajax.Autocompleter.prototype = Object.extend(new Autocompleter.Base(), |
324 | Object.extend(new Ajax.Base(), { |
325 | initialize: function(element, update, url, options) { |
326 | this.base_initialize(element, update, options); |
327 | this.options.asynchronous = true; |
328 | this.options.onComplete = this.onComplete.bind(this) |
329 | this.options.method = 'post'; |
330 | this.options.defaultParams = this.options.parameters || null; |
331 | this.url = url; |
332 | }, |
333 | |
334 | getUpdatedChoices: function() { |
335 | entry = encodeURIComponent(this.element.name) + '=' + |
336 | encodeURIComponent(this.getEntry()); |
337 | |
338 | this.options.parameters = this.options.callback ? |
339 | this.options.callback(this.element, entry) : entry; |
340 | |
341 | if(this.options.defaultParams) |
342 | this.options.parameters += '&' + this.options.defaultParams; |
343 | |
344 | new Ajax.Request(this.url, this.options); |
345 | }, |
346 | |
347 | onComplete: function(request) { |
348 | this.updateChoices(request.responseText); |
349 | } |
350 | |
351 | })); |
352 | |
353 | // The local array autocompleter. Used when you'd prefer to |
354 | // inject an array of autocompletion options into the page, rather |
355 | // than sending out Ajax queries, which can be quite slow sometimes. |
356 | // |
357 | // The constructor takes four parameters. The first two are, as usual, |
358 | // the id of the monitored textbox, and id of the autocompletion menu. |
359 | // The third is the array you want to autocomplete from, and the fourth |
360 | // is the options block. |
361 | // |
362 | // Extra local autocompletion options: |
363 | // - choices - How many autocompletion choices to offer |
364 | // |
365 | // - partial_search - If false, the autocompleter will match entered |
366 | // text only at the beginning of strings in the |
367 | // autocomplete array. Defaults to true, which will |
368 | // match text at the beginning of any *word* in the |
369 | // strings in the autocomplete array. If you want to |
370 | // search anywhere in the string, additionally set |
371 | // the option full_search to true (default: off). |
372 | // |
373 | // - full_search - Search anywhere in autocomplete array strings. |
374 | // |
375 | // - partial_chars - How many characters to enter before triggering |
376 | // a partial match (unlike min_chars, which defines |
377 | // how many characters are required to do any match |
378 | // at all). Defaults to 2. |
379 | // |
380 | // - ignore_case - Whether to ignore case when autocompleting. |
381 | // Defaults to true. |
382 | // |
383 | // It's possible to pass in a custom function as the 'selector' |
384 | // option, if you prefer to write your own autocompletion logic. |
385 | // In that case, the other options above will not apply unless |
386 | // you support them. |
387 | |
388 | Autocompleter.Local = Class.create(); |
389 | Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), { |
390 | initialize: function(element, update, array, options) { |
391 | this.base_initialize(element, update, options); |
392 | this.options.array = array; |
393 | }, |
394 | |
395 | getUpdatedChoices: function() { |
396 | this.updateChoices(this.options.selector(this)); |
397 | }, |
398 | |
399 | setOptions: function(options) { |
400 | this.options = Object.extend({ |
401 | choices: 10, |
402 | partial_search: true, |
403 | partial_chars: 2, |
404 | ignore_case: true, |
405 | full_search: false, |
406 | selector: function(instance) { |
407 | var ret = new Array(); // Beginning matches |
408 | var partial = new Array(); // Inside matches |
409 | var entry = instance.getEntry(); |
410 | var count = 0; |
411 | |
412 | for (var i = 0; i < instance.options.array.length && |
413 | ret.length < instance.options.choices ; i++) { |
414 | var elem = instance.options.array[i]; |
415 | var found_pos = instance.options.ignore_case ? |
416 | elem.toLowerCase().indexOf(entry.toLowerCase()) : |
417 | elem.indexOf(entry); |
418 | |
419 | while (found_pos != -1) { |
420 | if (found_pos == 0 && elem.length != entry.length) { |
421 | ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" + |
422 | elem.substr(entry.length) + "</li>"); |
423 | break; |
424 | } else if (entry.length >= instance.options.partial_chars && |
425 | instance.options.partial_search && found_pos != -1) { |
426 | if (instance.options.full_search || /\s/.test(elem.substr(found_pos-1,1))) { |
427 | partial.push("<li>" + elem.substr(0, found_pos) + "<strong>" + |
428 | elem.substr(found_pos, entry.length) + "</strong>" + elem.substr( |
429 | found_pos + entry.length) + "</li>"); |
430 | break; |
431 | } |
432 | } |
433 | |
434 | found_pos = instance.options.ignore_case ? |
435 | elem.toLowerCase().indexOf(entry.toLowerCase(), found_pos + 1) : |
436 | elem.indexOf(entry, found_pos + 1); |
437 | |
438 | } |
439 | } |
440 | if (partial.length) |
441 | ret = ret.concat(partial.slice(0, instance.options.choices - ret.length)) |
442 | return "<ul>" + ret.join('') + "</ul>"; |
443 | } |
444 | }, options || {}); |
445 | } |
446 | }); |