1var Observer = new Class({ 2 Implements: [Options, Events], 3 options: { 4 periodical: false, 5 delay: 1000 6 }, 7 initialize: function (el, onFired, options) { 8 this.element = document.id(el) || $document.id(el); 9 this.addEvent('onFired', onFired); 10 this.setOptions(options); 11 this.bound = this.changed.bind(this); 12 this.resume(); 13 }, 14 changed: function () { 15 var value = this.element.get('value'); 16 if ($equals(this.value, value)) return; 17 this.clear(); 18 this.value = value; 19 this.timeout = this.onFired.delay(this.options.delay, this); 20 }, 21 setValue: function (value) { 22 this.value = value; 23 this.element.set('value', value); 24 return this.clear(); 25 }, 26 onFired: function () { 27 this.fireEvent('onFired', [this.value, this.element]); 28 }, 29 clear: function () { 30 clearTimeout(this.timeout || null); 31 return this; 32 }, 33 pause: function () { 34 if (this.timer) clearTimeout(this.timer); 35 else this.element.removeEvent('keyup', this.bound); 36 return this.clear(); 37 }, 38 resume: function () { 39 this.value = this.element.get('value'); 40 if (this.options.periodical) this.timer = this.changed.periodical(this.options.periodical, this); 41 else this.element.addEvent('keyup', this.bound); 42 return this; 43 } 44}); 45var $equals = function (obj1, obj2) { 46 return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2)); 47 }; 48var Autocompleter = new Class({ 49 Implements: [Options, Events], 50 options: { 51 minLength: 1, 52 markQuery: true, 53 width: 'inherit', 54 maxChoices: 10, 55 injectChoice: null, 56 customChoices: null, 57 emptyChoices: null, 58 visibleChoices: true, 59 className: 'autocompleter-choices', 60 zIndex: 1000, 61 delay: 400, 62 observerOptions: {}, 63 fxOptions: {}, 64 autoSubmit: false, 65 overflow: false, 66 overflowMargin: 25, 67 selectFirst: false, 68 filter: null, 69 filterCase: false, 70 filterSubset: false, 71 forceSelect: false, 72 selectMode: true, 73 choicesMatch: null, 74 multiple: false, 75 separator: ', ', 76 separatorSplit: /\s*[,;]\s*/, 77 autoTrim: false, 78 allowDupes: false, 79 cache: true, 80 relative: false 81 }, 82 initialize: function (element, options) { 83 this.element = document.id(element); 84 this.setOptions(options); 85 this.build(); 86 this.observer = new Observer(this.element, this.prefetch.bind(this), Object.merge({}, { 87 'delay': this.options.delay 88 }, this.options.observerOptions)); 89 this.queryValue = null; 90 if (this.options.filter) this.filter = this.options.filter.bind(this); 91 var mode = this.options.selectMode; 92 this.typeAhead = (mode == 'type-ahead'); 93 this.selectMode = (mode === true) ? 'selection' : mode; 94 this.cached = []; 95 }, 96 build: function () { 97 if (document.id(this.options.customChoices)) { 98 this.choices = this.options.customChoices; 99 } else { 100 this.choices = new Element('ul', { 101 'class': this.options.className, 102 'styles': { 103 'zIndex': this.options.zIndex 104 } 105 }).inject(document.body); 106 this.relative = false; 107 if (this.options.relative) { 108 this.choices.inject(this.element, 'after'); 109 this.relative = this.element.getOffsetParent(); 110 } 111 this.fix = new OverlayFix(this.choices); 112 } 113 if (!this.options.separator.test(this.options.separatorSplit)) { 114 this.options.separatorSplit = this.options.separator; 115 } 116 this.fx = (!this.options.fxOptions) ? null : new Fx.Tween(this.choices, Ob
116ject.merge({}, { 117 'property': 'opacity', 118 'link': 'cancel', 119 'duration': 200 120 }, this.options.fxOptions)).addEvent('onStart', Chain.prototype.clearChain).set(0); 121 this.element.setProperty('autocomplete', 'off').addEvent((Browser.ie || Browser.safari || Browser.chrome) ? 'keydown' : 'keypress', this.onCommand.bind(this)).addEvent('click', this.onCommand.bind(this, [false])).addEvent('focus', this.toggleFocus.create({ 122 bind: this, 123 arguments: true, 124 delay: 100 125 })).addEvent('blur', this.toggleFocus.create({ 126 bind: this, 127 arguments: false, 128 delay: 100 129 })); 130 }, 131 destroy: function () { 132 if (this.fix) this.fix.destroy(); 133 this.choices = this.selected = this.choices.destroy(); 134 }, 135 toggleFocus: function (state) { 136 this.focussed = state; 137 if (!state) this.hideChoices(true); 138 this.fireEvent((state) ? 'onFocus' : 'onBlur', [this.element]); 139 }, 140 onCommand: function (e) { 141 if (!e && this.focussed) return this.prefetch(); 142 if (e && e.key && !e.shift) { 143 switch (e.key) { 144 case 'enter': 145 if (this.element.value != this.opted) return true; 146 if (this.selected && this.visible) { 147 this.choiceSelect(this.selected); 148 return !!(this.options.autoSubmit); 149 } 150 break; 151 case 'up': 152 case 'down': 153 if (!this.prefetch() && this.queryValue !== null) { 154 var up = (e.key == 'up'); 155 this.choiceOver((this.selected || this.choices)[(this.selected) ? ((up) ? 'getPrevious' : 'getNext') : ((up) ? 'getLast' : 'getFirst')](this.options.choicesMatch), true); 156 } 157 return false; 158 case 'esc': 159 case 'tab': 160 this.hideChoices(true); 161 break; 162 } 163 } 164 return true; 165 }, 166 setSelection: function (finish) { 167 var input = this.selected.inputValue, 168 value = input; 169 var start = this.queryValue.length, 170 end = input.length; 171 if (input.substr(0, start).toLowerCase() != this.queryValue.toLowerCase()) start = 0; 172 if (this.options.multiple) { 173 var split = this.options.separatorSplit; 174 value = this.element.value; 175 start += this.queryIndex; 176 end += this.queryIndex; 177 var old = value.substr(this.queryIndex).split(split, 1)[0]; 178 value = value.substr(0, this.queryIndex) + input + value.substr(this.queryIndex + old.length); 179 if (finish) { 180 var tokens = value.split(this.options.separatorSplit).filter(function (entry) { 181 return this.test(entry); 182 }, /[^\s,]+/); 183 if (!this.options.allowDupes) tokens = [].combine(tokens); 184 var sep = this.options.separator; 185 value = tokens.join(sep) + sep; 186 end = value.length; 187 } 188 } 189 this.observer.setValue(value); 190 this.opted = value; 191 if (finish || this.selectMode == 'pick') start = end; 192 this.element.selectRange(start, end); 193 this.fireEvent('onSelection', [this.element, this.selected, value, input]); 194 }, 195 showChoices: function () { 196 var match = this.options.choicesMatch, 197 first = this.choices.getFirst(match); 198 this.selected = this.selectedValue = null; 199 if (this.fix) { 200 var pos = this.element.getCoordinates(this.relative), 201 width = this.options.width || 'auto'; 202 this.choices.setStyles({ 203 'left': pos.left, 204 'top': pos.bottom, 205 'width': (width === true || width == 'inherit') ? pos.width : width 206 }); 207 } 208 if (!first) return; 209 if (!this.visible) { 210 this.visible = true; 211 this.choices.setStyle('display', ''); 212 if (this.fx) this.fx.start(1); 213 this.fireEvent('onShow', [this.element, this.choices]); 214 } 215 if (this.options.selectFirst || this.typeAhead || first.inputValue == this.queryValue) this.choiceOver(first, this.typeAhead); 216 var items = this.choices.getChildren(match), 217 max = this.options.maxChoices; 218 var styles = { 219 'overflowY': 'hidden', 220 'height': '' 221 }; 222 this.overflown = false; 223 if (items.length > max) { 224 var item = items[max - 1]; 225 styles.overflowY = 'scroll'; 226 styles.height = item.getCoordinates(this.choices).bottom; 227 this.overflown = true; 228 }; 229 this.choices.setStyles(styles); 230 this.fix.show(); 231 if (this.options.visibleChoices) { 232 var scroll = document.getScroll(), 233 size = document.getSize(), 234 coords = this.choices.getCoordinates(); 235 if (coords.right > scroll.x + size.x) scroll.x = coords.right - size.x; 236 if (coords.bottom > scroll.y + size.y) scroll.y = coords.bottom - size.y; 237 window.scrollTo(Math.min(scroll.x, coords.left), Math.min(scroll.y, coords.top)); 238 } 239 }, 240 // TODO: No $arguments in MT 1.3 241 hideChoices: function (clear) { 242 if (clear) { 243 var value = this.element.value; 244 if (this.options.forceSelect) value = this.opted; 245 if (this.options.autoTrim) { 246 value = value.split(this.options.separatorSplit).filter($arguments(0)).join(this.options.separator); 247 } 248 this.observer.setValue(value); 249 } 250 if (!this.visible) return; 251 this.visible = false;
252 if (this.selected) this.selected.removeClass('autocompleter-selected'); 253 this.observer.clear(); 254 var hide = function () { 255 this.choices.setStyle('display', 'none'); 256 this.fix.hide(); 257 }.bind(this); 258 if (this.fx) this.fx.start(0).chain(hide); 259 else hide(); 260 this.fireEvent('onHide', [this.element, this.choices]); 261 }, 262 prefetch: function () { 263 var value = this.element.value, 264 query = value; 265 if (this.options.multiple) { 266 var split = this.options.separatorSplit; 267 var values = value.split(split); 268 var index = this.element.getSelectedRange().start; 269 var toIndex = value.substr(0, index).split(split); 270 var last = toIndex.length - 1; 271 index -= toIndex[last].length; 272 query = values[last]; 273 } 274 if (query.length < this.options.minLength) { 275 this.hideChoices(); 276 } else { 277 if (query === this.queryValue || (this.visible && query == this.selectedValue)) { 278 if (this.visible) return false; 279 this.showChoices(); 280 } else { 281 this.queryValue = query; 282 this.queryIndex = index; 283 if (!this.fetchCached()) this.query(); 284 } 285 } 286 return true; 287 }, 288 fetchCached: function () { 289 return false; 290 if (!this.options.cache || !this.cached || !this.cached.length || this.cached.length >= this.options.maxChoices || this.queryValue) return false; 291 this.update(this.filter(this.cached)); 292 return true; 293 }, 294 update: function (tokens) { 295 this.choices.empty(); 296 this.cached = tokens; 297 var type = tokens && typeOf(tokens); 298 if (!type || (type == 'array' && !tokens.length) || (type == 'hash' && !tokens.getLength())) { 299 (this.options.emptyChoices || this.hideChoices).call(this); 300 } else { 301 if (this.options.maxChoices < tokens.length && !this.options.overflow) tokens.length = this.options.maxChoices; 302 tokens.each(this.options.injectChoice || 303 function (token) { 304 var choice = new Element('li', { 305 'html': this.markQueryValue(token) 306 }); 307 choice.inputValue = token; 308 this.addChoiceEvents(choice).inject(this.choices); 309 }, this); 310 this.showChoices(); 311 } 312 }, 313 choiceOver: function (choice, selection) { 314 if (!choice || choice == this.selected) return; 315 if (this.selected) this.selected.removeClass('autocompleter-selected'); 316 this.selected = choice.addClass('autocompleter-selected'); 317 this.fireEvent('onSelect', [this.element, this.selected, selection]); 318 if (!this.selectMode) this.opted = this.element.value; 319 if (!selection) return; 320 this.selectedValue = this.selected.inputValue; 321 if (this.overflown) { 322 var coords = this.selected.getCoordinates(this.choices), 323 margin = this.options.overflowMargin, 324 top = this.choices.scrollTop, 325 height = this.choices.offsetHeight, 326 bottom = top + height; 327 if (coords.top - margin < top && top) this.choices.scrollTop = Math.max(coords.top - margin, 0); 328 else if (coords.bottom + margin > bottom) this.choices.scrollTop = Math.min(coords.bottom - height + margin, bottom); 329 } 330 if (this.selectMode) this.setSelection(); 331 }, 332 choiceSelect: function (choice) { 333 if (choice) this.choiceOver(choice); 334 this.setSelection(true); 335 this.queryValue = false; 336 this.hideChoices(); 337 }, 338 filter: function (tokens) { 339 return (tokens || this.tokens).filter(function (token) { 340 return this.test(token); 341 }, new RegExp(((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp(), (this.options.filterCase) ? '' : 'i')); 342 }, 343 markQueryValue: function (str) { 344 return (!this.options.markQuery || !this.queryValue) ? str : str.replace(new RegExp('(' + ((this.options.filterSubset) ? '' : '^') + this.queryValue.escapeRegExp() + ')', (this.options.filterCase) ? '' : 'i'), '<span class="autocompleter-queried">$1</span>'); 345 }, 346 addChoiceEvents: function (el) { 347 return el.addEvents({ 348 'mouseover': this.choiceOver.bind(this, el), 349 'click': this.choiceSelect.bind(this, el) 350 }); 351 } 352}); 353var OverlayFix = new Class({ 354 initialize: function (el) { 355 if (Browser.ie) { 356 this.element = document.id(el); 357 this.relative = this.element.getOffsetParent(); 358 this.fix = new Element('iframe', { 359 'frameborder': '0', 360 'scrolling': 'no', 361 'src': 'javascript:false;', 362 'styles': { 363 'position': 'absolute', 364 'border': 'none', 365 'display': 'none', 366 'filter': 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)' 367 } 368 }).inject(this.element, 'after'); 369 } 370 }, 371 show: function () { 372 if (this.fix) { 373 var coords = this.element.getCoordinates(this.relative); 374 delete coords.right; 375 delete coords.bottom; 376 this.fix.setStyles(Object.append(coords, {
377 'display': '', 378 'zIndex': (this.element.getStyle('zIndex') || 1) - 1 379 })); 380 } 381 return this; 382 }, 383 hide: function () { 384 if (this.fix) this.fix.setStyle('display', 'none'); 385 return this; 386 }, 387 destroy: function () { 388 if (this.fix) this.fix = this.fix.destroy(); 389 } 390}); 391Element.implement({ 392 getSelectedRange: function () { 393 if (!Browser.ie) return { 394 start: this.selectionStart, 395 end: this.selectionEnd 396 }; 397 var pos = { 398 start: 0, 399 end: 0 400 }; 401 var range = this.getDocument().selection.createRange(); 402 if (!range || range.parentElement() != this) return pos; 403 var dup = range.duplicate(); 404 if (this.type == 'text') { 405 pos.start = 0 - dup.moveStart('character', -100000); 406 pos.end = pos.start + range.text.length; 407 } else { 408 var value = this.value; 409 var offset = value.length - value.match(/[\n\r]*$/)[0].length; 410 dup.moveToElementText(this); 411 dup.setEndPoint('StartToEnd', range); 412 pos.end = offset - dup.text.length; 413 dup.setEndPoint('StartToStart', range); 414 pos.start = offset - dup.text.length; 415 } 416 return pos; 417 }, 418 selectRange: function (start, end) { 419 if (Browser.ie) { 420 var diff = this.value.substr(start, end - start).replace(/\r/g, '').length; 421 start = this.value.substr(0, start).replace(/\r/g, '').length; 422 var range = this.createTextRange(); 423 range.collapse(true); 424 range.moveEnd('character', start + diff); 425 range.moveStart('character', start); 426 range.select(); 427 } else { 428 this.focus(); 429 this.setSelectionRange(start, end); 430 } 431 return this; 432 } 433}); 434Autocompleter.Base = Autocompleter; 435Autocompleter.Request = new Class({ 436 Extends: Autocompleter, 437 options: { 438 postData: {}, 439 ajaxOptions: {}, 440 postVar: 'value' 441 }, 442 query: function () { 443 var data = this.options.postData.unlink || {}; 444 data[this.options.postVar] = this.queryValue; 445 var indicator = document.id(this.options.indicator); 446 if (indicator) indicator.setStyle('display', ''); 447 var cls = this.options.indicatorClass; 448 if (cls) this.element.addClass(cls); 449 this.fireEvent('onRequest', [this.element, this.request, data, this.queryValue]); 450 this.request.send({ 451 'data': data 452 }); 453 }, 454 queryResponse: function () { 455 var indicator = document.id(this.options.indicator); 456 if (indicator) indicator.setStyle('display', 'none'); 457 var cls = this.options.indicatorClass; 458 if (cls) this.element.removeClass(cls); 459 return this.fireEvent('onComplete', [this.element, this.request]); 460 } 461}); 462Autocompleter.Request.JSON = new Class({ 463 Extends: Autocompleter.Request, 464 initialize: function (el, url, options) { 465 this.parent(el, options); 466 this.request = new Request.JSON(Object.merge({}, { 467 'url': url, 468 'link': 'cancel' 469 }, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this)); 470 }, 471 queryResponse: function (response) { 472 this.parent(); 473 this.update(response); 474 } 475}); 476Autocompleter.Request.HTML = new Class({ 477 Extends: Autocompleter.Request, 478 initialize: function (el, url, options) { 479 this.parent(el, options); 480 this.request = new Request.HTML(Object.merge({}, { 481 'url': url, 482 'link': 'cancel', 483 'update': this.choices 484 }, this.options.ajaxOptions)).addEvent('onComplete', this.queryResponse.bind(this)); 485 }, 486 queryResponse: function (tree, elements) { 487 this.parent(); 488 if (!elements || !elements.length) { 489 this.hideChoices(); 490 } else { 491 this.choices.getChildren(this.options.choicesMatch).each(this.options.injectChoice || 492 function (choice) { 493 var value = choice.innerHTML; 494 choice.inputValue = value; 495 this.addChoiceEvents(choice.set('html', this.markQueryValue(value))); 496 }, this); 497 this.showChoices(); 498 } 499 } 500}); 501Autocompleter.Ajax = { 502 Base: Autocompleter.Request, 503 Json: Autocompleter.Request.JSON, 504 Xhtml: Autocompleter.Request.HTML 505};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.