PageRenderTime 29ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/media/com_finder/js/autocompleter.js

https://bitbucket.org/pastor399/newcastleunifc
JavaScript | 505 lines | 504 code | 0 blank | 1 comment | 128 complexity | f6dc724b60e00fa0b14a00d23fb877bf MD5 | raw file
  1. var 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. });
  45. var $equals = function (obj1, obj2) {
  46. return (obj1 == obj2 || JSON.encode(obj1) == JSON.encode(obj2));
  47. };
  48. var 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, Object.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.pass({
  122. bind: this,
  123. arguments: true,
  124. delay: 100
  125. })).addEvent('blur', this.toggleFocus.pass({
  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. });
  353. var 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. });
  391. Element.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. });
  434. Autocompleter.Base = Autocompleter;
  435. Autocompleter.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. });
  462. Autocompleter.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. });
  476. Autocompleter.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. });
  501. Autocompleter.Ajax = {
  502. Base: Autocompleter.Request,
  503. Json: Autocompleter.Request.JSON,
  504. Xhtml: Autocompleter.Request.HTML
  505. };