PageRenderTime 66ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/thirdparty/scriptaculous/controls.js

http://github.com/silverstripe/sapphire
JavaScript | 750 lines | 590 code | 75 blank | 85 comment | 115 complexity | ea707e5d4d198cd2078fd936f6147ee9 MD5 | raw file
Possible License(s): BSD-3-Clause, MIT, CC-BY-3.0, GPL-2.0, AGPL-1.0, LGPL-2.1
  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. // (c) 2005 Jon Tirsen (http://www.tirsen.com)
  4. // Contributors:
  5. // Richard Livsey
  6. // Rahul Bhargava
  7. // Rob Wills
  8. //
  9. // See scriptaculous.js for full license.
  10. // Autocompleter.Base handles all the autocompletion functionality
  11. // that's independent of the data source for autocompletion. This
  12. // includes drawing the autocompletion menu, observing keyboard
  13. // and mouse events, and similar.
  14. //
  15. // Specific autocompleters need to provide, at the very least,
  16. // a getUpdatedChoices function that will be invoked every time
  17. // the text inside the monitored textbox changes. This method
  18. // should get the text for which to provide autocompletion by
  19. // invoking this.getToken(), NOT by directly accessing
  20. // this.element.value. This is to allow incremental tokenized
  21. // autocompletion. Specific auto-completion logic (AJAX, etc)
  22. // belongs in getUpdatedChoices.
  23. //
  24. // Tokenized incremental autocompletion is enabled automatically
  25. // when an autocompleter is instantiated with the 'tokens' option
  26. // in the options parameter, e.g.:
  27. // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
  28. // will incrementally autocomplete with a comma as the token.
  29. // Additionally, ',' in the above example can be replaced with
  30. // a token array, e.g. { tokens: [',', '\n'] } which
  31. // enables autocompletion on multiple tokens. This is most
  32. // useful when one of the tokens is \n (a newline), as it
  33. // allows smart autocompletion after linebreaks.
  34. var Autocompleter = {}
  35. Autocompleter.Base = function() {};
  36. Autocompleter.Base.prototype = {
  37. baseInitialize: function(element, update, options) {
  38. this.element = $(element);
  39. this.update = $(update);
  40. this.hasFocus = false;
  41. this.changed = false;
  42. this.active = false;
  43. this.index = 0;
  44. this.entryCount = 0;
  45. if (this.setOptions)
  46. this.setOptions(options);
  47. else
  48. this.options = options || {};
  49. this.options.paramName = this.options.paramName || this.element.name;
  50. this.options.tokens = this.options.tokens || [];
  51. this.options.frequency = this.options.frequency || 0.4;
  52. this.options.minChars = this.options.minChars || 1;
  53. this.options.onShow = this.options.onShow ||
  54. function(element, update){
  55. if(!update.style.position || update.style.position=='absolute') {
  56. update.style.position = 'absolute';
  57. Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
  58. }
  59. Effect.Appear(update,{duration:0.15});
  60. };
  61. this.options.onHide = this.options.onHide ||
  62. function(element, update){ new Effect.Fade(update,{duration:0.15}) };
  63. if (typeof(this.options.tokens) == 'string')
  64. this.options.tokens = new Array(this.options.tokens);
  65. this.observer = null;
  66. this.element.setAttribute('autocomplete','off');
  67. Element.hide(this.update);
  68. Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
  69. Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
  70. },
  71. show: function() {
  72. if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
  73. if(!this.iefix &&
  74. (navigator.appVersion.indexOf('MSIE')>0) &&
  75. (navigator.userAgent.indexOf('Opera')<0) &&
  76. (Element.getStyle(this.update, 'position')=='absolute')) {
  77. new Insertion.After(this.update,
  78. '<iframe id="' + this.update.id + '_iefix" '+
  79. 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
  80. 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
  81. this.iefix = $(this.update.id+'_iefix');
  82. }
  83. if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  84. },
  85. fixIEOverlapping: function() {
  86. Position.clone(this.update, this.iefix);
  87. this.iefix.style.zIndex = 1;
  88. this.update.style.zIndex = 2;
  89. Element.show(this.iefix);
  90. },
  91. hide: function() {
  92. this.stopIndicator();
  93. if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
  94. if(this.iefix) Element.hide(this.iefix);
  95. },
  96. startIndicator: function() {
  97. if(this.options.indicator) Element.show(this.options.indicator);
  98. },
  99. stopIndicator: function() {
  100. if(this.options.indicator) Element.hide(this.options.indicator);
  101. },
  102. onKeyPress: function(event) {
  103. if(this.active)
  104. switch(event.keyCode) {
  105. case Event.KEY_TAB:
  106. case Event.KEY_RETURN:
  107. this.selectEntry();
  108. Event.stop(event);
  109. case Event.KEY_ESC:
  110. this.hide();
  111. this.active = false;
  112. Event.stop(event);
  113. return;
  114. case Event.KEY_LEFT:
  115. case Event.KEY_RIGHT:
  116. return;
  117. case Event.KEY_UP:
  118. this.markPrevious();
  119. this.render();
  120. if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
  121. return;
  122. case Event.KEY_DOWN:
  123. this.markNext();
  124. this.render();
  125. if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
  126. return;
  127. }
  128. else
  129. if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
  130. return;
  131. this.changed = true;
  132. this.hasFocus = true;
  133. if(this.observer) clearTimeout(this.observer);
  134. this.observer =
  135. setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  136. },
  137. onHover: function(event) {
  138. var element = Event.findElement(event, 'LI');
  139. if(this.index != element.autocompleteIndex)
  140. {
  141. this.index = element.autocompleteIndex;
  142. this.render();
  143. }
  144. Event.stop(event);
  145. },
  146. onClick: function(event) {
  147. var element = Event.findElement(event, 'LI');
  148. this.index = element.autocompleteIndex;
  149. this.selectEntry();
  150. this.hide();
  151. },
  152. onBlur: function(event) {
  153. // needed to make click events working
  154. setTimeout(this.hide.bind(this), 250);
  155. this.hasFocus = false;
  156. this.active = false;
  157. },
  158. render: function() {
  159. if(this.entryCount > 0) {
  160. for (var i = 0; i < this.entryCount; i++)
  161. this.index==i ?
  162. Element.addClassName(this.getEntry(i),"selected") :
  163. Element.removeClassName(this.getEntry(i),"selected");
  164. if(this.hasFocus) {
  165. this.show();
  166. this.active = true;
  167. }
  168. } else {
  169. this.active = false;
  170. this.hide();
  171. }
  172. },
  173. markPrevious: function() {
  174. if(this.index > 0) this.index--
  175. else this.index = this.entryCount-1;
  176. },
  177. markNext: function() {
  178. if(this.index < this.entryCount-1) this.index++
  179. else this.index = 0;
  180. },
  181. getEntry: function(index) {
  182. return this.update.firstChild.childNodes[index];
  183. },
  184. getCurrentEntry: function() {
  185. return this.getEntry(this.index);
  186. },
  187. selectEntry: function() {
  188. this.active = false;
  189. this.updateElement(this.getCurrentEntry());
  190. },
  191. updateElement: function(selectedElement) {
  192. if (this.options.updateElement) {
  193. this.options.updateElement(selectedElement);
  194. return;
  195. }
  196. var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
  197. var lastTokenPos = this.findLastToken();
  198. if (lastTokenPos != -1) {
  199. var newValue = this.element.value.substr(0, lastTokenPos + 1);
  200. var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
  201. if (whitespace)
  202. newValue += whitespace[0];
  203. this.element.value = newValue + value;
  204. } else {
  205. this.element.value = value;
  206. }
  207. this.element.focus();
  208. if (this.options.afterUpdateElement)
  209. this.options.afterUpdateElement(this.element, selectedElement);
  210. },
  211. updateChoices: function(choices) {
  212. if(!this.changed && this.hasFocus) {
  213. this.update.innerHTML = choices;
  214. Element.cleanWhitespace(this.update);
  215. Element.cleanWhitespace(this.update.firstChild);
  216. if(this.update.firstChild && this.update.firstChild.childNodes) {
  217. this.entryCount =
  218. this.update.firstChild.childNodes.length;
  219. for (var i = 0; i < this.entryCount; i++) {
  220. var entry = this.getEntry(i);
  221. entry.autocompleteIndex = i;
  222. this.addObservers(entry);
  223. }
  224. } else {
  225. this.entryCount = 0;
  226. }
  227. this.stopIndicator();
  228. this.index = 0;
  229. this.render();
  230. }
  231. },
  232. addObservers: function(element) {
  233. Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
  234. Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  235. },
  236. onObserverEvent: function() {
  237. this.changed = false;
  238. if(this.getToken().length>=this.options.minChars) {
  239. this.startIndicator();
  240. this.getUpdatedChoices();
  241. } else {
  242. this.active = false;
  243. this.hide();
  244. }
  245. },
  246. getToken: function() {
  247. var tokenPos = this.findLastToken();
  248. if (tokenPos != -1)
  249. var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
  250. else
  251. var ret = this.element.value;
  252. return /\n/.test(ret) ? '' : ret;
  253. },
  254. findLastToken: function() {
  255. var lastTokenPos = -1;
  256. for (var i=0; i<this.options.tokens.length; i++) {
  257. var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
  258. if (thisTokenPos > lastTokenPos)
  259. lastTokenPos = thisTokenPos;
  260. }
  261. return lastTokenPos;
  262. }
  263. }
  264. Ajax.Autocompleter = Class.create();
  265. Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
  266. initialize: function(element, update, url, options) {
  267. this.baseInitialize(element, update, options);
  268. this.options.asynchronous = true;
  269. this.options.onComplete = this.onComplete.bind(this);
  270. this.options.defaultParams = this.options.parameters || null;
  271. this.url = url;
  272. },
  273. getUpdatedChoices: function() {
  274. entry = encodeURIComponent(this.options.paramName) + '=' +
  275. encodeURIComponent(this.getToken());
  276. this.options.parameters = this.options.callback ?
  277. this.options.callback(this.element, entry) : entry;
  278. if(this.options.defaultParams)
  279. this.options.parameters += '&' + this.options.defaultParams;
  280. new Ajax.Request(this.url, this.options);
  281. },
  282. onComplete: function(request) {
  283. this.updateChoices(request.responseText);
  284. }
  285. });
  286. // The local array autocompleter. Used when you'd prefer to
  287. // inject an array of autocompletion options into the page, rather
  288. // than sending out Ajax queries, which can be quite slow sometimes.
  289. //
  290. // The constructor takes four parameters. The first two are, as usual,
  291. // the id of the monitored textbox, and id of the autocompletion menu.
  292. // The third is the array you want to autocomplete from, and the fourth
  293. // is the options block.
  294. //
  295. // Extra local autocompletion options:
  296. // - choices - How many autocompletion choices to offer
  297. //
  298. // - partialSearch - If false, the autocompleter will match entered
  299. // text only at the beginning of strings in the
  300. // autocomplete array. Defaults to true, which will
  301. // match text at the beginning of any *word* in the
  302. // strings in the autocomplete array. If you want to
  303. // search anywhere in the string, additionally set
  304. // the option fullSearch to true (default: off).
  305. //
  306. // - fullSsearch - Search anywhere in autocomplete array strings.
  307. //
  308. // - partialChars - How many characters to enter before triggering
  309. // a partial match (unlike minChars, which defines
  310. // how many characters are required to do any match
  311. // at all). Defaults to 2.
  312. //
  313. // - ignoreCase - Whether to ignore case when autocompleting.
  314. // Defaults to true.
  315. //
  316. // It's possible to pass in a custom function as the 'selector'
  317. // option, if you prefer to write your own autocompletion logic.
  318. // In that case, the other options above will not apply unless
  319. // you support them.
  320. Autocompleter.Local = Class.create();
  321. Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
  322. initialize: function(element, update, array, options) {
  323. this.baseInitialize(element, update, options);
  324. this.options.array = array;
  325. },
  326. getUpdatedChoices: function() {
  327. this.updateChoices(this.options.selector(this));
  328. },
  329. setOptions: function(options) {
  330. this.options = Object.extend({
  331. choices: 10,
  332. partialSearch: true,
  333. partialChars: 2,
  334. ignoreCase: true,
  335. fullSearch: false,
  336. selector: function(instance) {
  337. var ret = []; // Beginning matches
  338. var partial = []; // Inside matches
  339. var entry = instance.getToken();
  340. var count = 0;
  341. for (var i = 0; i < instance.options.array.length &&
  342. ret.length < instance.options.choices ; i++) {
  343. var elem = instance.options.array[i];
  344. var foundPos = instance.options.ignoreCase ?
  345. elem.toLowerCase().indexOf(entry.toLowerCase()) :
  346. elem.indexOf(entry);
  347. while (foundPos != -1) {
  348. if (foundPos == 0 && elem.length != entry.length) {
  349. ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
  350. elem.substr(entry.length) + "</li>");
  351. break;
  352. } else if (entry.length >= instance.options.partialChars &&
  353. instance.options.partialSearch && foundPos != -1) {
  354. if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
  355. partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
  356. elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
  357. foundPos + entry.length) + "</li>");
  358. break;
  359. }
  360. }
  361. foundPos = instance.options.ignoreCase ?
  362. elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
  363. elem.indexOf(entry, foundPos + 1);
  364. }
  365. }
  366. if (partial.length)
  367. ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
  368. return "<ul>" + ret.join('') + "</ul>";
  369. }
  370. }, options || {});
  371. }
  372. });
  373. // AJAX in-place editor
  374. //
  375. // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
  376. // Use this if you notice weird scrolling problems on some browsers,
  377. // the DOM might be a bit confused when this gets called so do this
  378. // waits 1 ms (with setTimeout) until it does the activation
  379. Field.scrollFreeActivate = function(field) {
  380. setTimeout(function() {
  381. Field.activate(field);
  382. }, 1);
  383. }
  384. Ajax.InPlaceEditor = Class.create();
  385. Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
  386. Ajax.InPlaceEditor.prototype = {
  387. initialize: function(element, url, options) {
  388. this.url = url;
  389. this.element = $(element);
  390. this.options = Object.extend({
  391. okText: "ok",
  392. cancelText: "cancel",
  393. savingText: "Saving...",
  394. clickToEditText: "Click to edit",
  395. okText: "ok",
  396. rows: 1,
  397. onComplete: function(transport, element) {
  398. new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
  399. },
  400. onFailure: function(transport) {
  401. alert("Error communicating with the server: " + transport.responseText.stripTags());
  402. },
  403. callback: function(form) {
  404. return Form.serialize(form);
  405. },
  406. handleLineBreaks: true,
  407. loadingText: 'Loading...',
  408. savingClassName: 'inplaceeditor-saving',
  409. loadingClassName: 'inplaceeditor-loading',
  410. formClassName: 'inplaceeditor-form',
  411. highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
  412. highlightendcolor: "#FFFFFF",
  413. externalControl: null,
  414. ajaxOptions: {}
  415. }, options || {});
  416. if(!this.options.formId && this.element.id) {
  417. this.options.formId = this.element.id + "-inplaceeditor";
  418. if ($(this.options.formId)) {
  419. // there's already a form with that name, don't specify an id
  420. this.options.formId = null;
  421. }
  422. }
  423. if (this.options.externalControl) {
  424. this.options.externalControl = $(this.options.externalControl);
  425. }
  426. this.originalBackground = Element.getStyle(this.element, 'background-color');
  427. if (!this.originalBackground) {
  428. this.originalBackground = "transparent";
  429. }
  430. this.element.title = this.options.clickToEditText;
  431. this.onclickListener = this.enterEditMode.bindAsEventListener(this);
  432. this.mouseoverListener = this.enterHover.bindAsEventListener(this);
  433. this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
  434. Event.observe(this.element, 'click', this.onclickListener);
  435. Event.observe(this.element, 'mouseover', this.mouseoverListener);
  436. Event.observe(this.element, 'mouseout', this.mouseoutListener);
  437. if (this.options.externalControl) {
  438. Event.observe(this.options.externalControl, 'click', this.onclickListener);
  439. Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
  440. Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
  441. }
  442. },
  443. enterEditMode: function(evt) {
  444. if (this.saving) return;
  445. if (this.editing) return;
  446. this.editing = true;
  447. this.onEnterEditMode();
  448. if (this.options.externalControl) {
  449. Element.hide(this.options.externalControl);
  450. }
  451. Element.hide(this.element);
  452. this.createForm();
  453. this.element.parentNode.insertBefore(this.form, this.element);
  454. Field.scrollFreeActivate(this.editField);
  455. // stop the event to avoid a page refresh in Safari
  456. if (evt) {
  457. Event.stop(evt);
  458. }
  459. return false;
  460. },
  461. createForm: function() {
  462. this.form = document.createElement("form");
  463. this.form.id = this.options.formId;
  464. Element.addClassName(this.form, this.options.formClassName)
  465. this.form.onsubmit = this.onSubmit.bind(this);
  466. this.createEditField();
  467. if (this.options.textarea) {
  468. var br = document.createElement("br");
  469. this.form.appendChild(br);
  470. }
  471. okButton = document.createElement("input");
  472. okButton.type = "submit";
  473. okButton.value = this.options.okText;
  474. this.form.appendChild(okButton);
  475. cancelLink = document.createElement("a");
  476. cancelLink.href = "#";
  477. cancelLink.appendChild(document.createTextNode(this.options.cancelText));
  478. cancelLink.onclick = this.onclickCancel.bind(this);
  479. this.form.appendChild(cancelLink);
  480. },
  481. hasHTMLLineBreaks: function(string) {
  482. if (!this.options.handleLineBreaks) return false;
  483. return string.match(/<br/i) || string.match(/<p>/i);
  484. },
  485. convertHTMLLineBreaks: function(string) {
  486. return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
  487. },
  488. createEditField: function() {
  489. var text;
  490. if(this.options.loadTextURL) {
  491. text = this.options.loadingText;
  492. } else {
  493. text = this.getText();
  494. }
  495. if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
  496. this.options.textarea = false;
  497. var textField = document.createElement("input");
  498. textField.type = "text";
  499. textField.name = "value";
  500. textField.value = text;
  501. textField.style.backgroundColor = this.options.highlightcolor;
  502. var size = this.options.size || this.options.cols || 0;
  503. if (size != 0) textField.size = size;
  504. this.editField = textField;
  505. } else {
  506. this.options.textarea = true;
  507. var textArea = document.createElement("textarea");
  508. textArea.name = "value";
  509. textArea.value = this.convertHTMLLineBreaks(text);
  510. textArea.rows = this.options.rows;
  511. textArea.cols = this.options.cols || 40;
  512. this.editField = textArea;
  513. }
  514. if(this.options.loadTextURL) {
  515. this.loadExternalText();
  516. }
  517. this.form.appendChild(this.editField);
  518. },
  519. getText: function() {
  520. return this.element.innerHTML;
  521. },
  522. loadExternalText: function() {
  523. Element.addClassName(this.form, this.options.loadingClassName);
  524. this.editField.disabled = true;
  525. new Ajax.Request(
  526. this.options.loadTextURL,
  527. Object.extend({
  528. asynchronous: true,
  529. onComplete: this.onLoadedExternalText.bind(this)
  530. }, this.options.ajaxOptions)
  531. );
  532. },
  533. onLoadedExternalText: function(transport) {
  534. Element.removeClassName(this.form, this.options.loadingClassName);
  535. this.editField.disabled = false;
  536. this.editField.value = transport.responseText.stripTags();
  537. },
  538. onclickCancel: function() {
  539. this.onComplete();
  540. this.leaveEditMode();
  541. return false;
  542. },
  543. onFailure: function(transport) {
  544. this.options.onFailure(transport);
  545. if (this.oldInnerHTML) {
  546. this.element.innerHTML = this.oldInnerHTML;
  547. this.oldInnerHTML = null;
  548. }
  549. return false;
  550. },
  551. onSubmit: function() {
  552. // onLoading resets these so we need to save them away for the Ajax call
  553. var form = this.form;
  554. var value = this.editField.value;
  555. // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
  556. // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
  557. // to be displayed indefinitely
  558. this.onLoading();
  559. new Ajax.Updater(
  560. {
  561. success: this.element,
  562. // don't update on failure (this could be an option)
  563. failure: null
  564. },
  565. this.url,
  566. Object.extend({
  567. parameters: this.options.callback(form, value),
  568. onComplete: this.onComplete.bind(this),
  569. onFailure: this.onFailure.bind(this)
  570. }, this.options.ajaxOptions)
  571. );
  572. // stop the event to avoid a page refresh in Safari
  573. if (arguments.length > 1) {
  574. Event.stop(arguments[0]);
  575. }
  576. return false;
  577. },
  578. onLoading: function() {
  579. this.saving = true;
  580. this.removeForm();
  581. this.leaveHover();
  582. this.showSaving();
  583. },
  584. showSaving: function() {
  585. this.oldInnerHTML = this.element.innerHTML;
  586. this.element.innerHTML = this.options.savingText;
  587. Element.addClassName(this.element, this.options.savingClassName);
  588. this.element.style.backgroundColor = this.originalBackground;
  589. Element.show(this.element);
  590. },
  591. removeForm: function() {
  592. if(this.form) {
  593. if (this.form.parentNode) Element.remove(this.form);
  594. this.form = null;
  595. }
  596. },
  597. enterHover: function() {
  598. if (this.saving) return;
  599. this.element.style.backgroundColor = this.options.highlightcolor;
  600. if (this.effect) {
  601. this.effect.cancel();
  602. }
  603. Element.addClassName(this.element, this.options.hoverClassName)
  604. },
  605. leaveHover: function() {
  606. if (this.options.backgroundColor) {
  607. this.element.style.backgroundColor = this.oldBackground;
  608. }
  609. Element.removeClassName(this.element, this.options.hoverClassName)
  610. if (this.saving) return;
  611. this.effect = new Effect.Highlight(this.element, {
  612. startcolor: this.options.highlightcolor,
  613. endcolor: this.options.highlightendcolor,
  614. restorecolor: this.originalBackground
  615. });
  616. },
  617. leaveEditMode: function() {
  618. Element.removeClassName(this.element, this.options.savingClassName);
  619. this.removeForm();
  620. this.leaveHover();
  621. this.element.style.backgroundColor = this.originalBackground;
  622. Element.show(this.element);
  623. if (this.options.externalControl) {
  624. Element.show(this.options.externalControl);
  625. }
  626. this.editing = false;
  627. this.saving = false;
  628. this.oldInnerHTML = null;
  629. this.onLeaveEditMode();
  630. },
  631. onComplete: function(transport) {
  632. this.leaveEditMode();
  633. this.options.onComplete.bind(this)(transport, this.element);
  634. },
  635. onEnterEditMode: function() {},
  636. onLeaveEditMode: function() {},
  637. dispose: function() {
  638. if (this.oldInnerHTML) {
  639. this.element.innerHTML = this.oldInnerHTML;
  640. }
  641. this.leaveEditMode();
  642. Event.stopObserving(this.element, 'click', this.onclickListener);
  643. Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
  644. Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
  645. if (this.options.externalControl) {
  646. Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
  647. Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
  648. Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
  649. }
  650. }
  651. };
  652. // Delayed observer, like Form.Element.Observer,
  653. // but waits for delay after last key input
  654. // Ideal for live-search fields
  655. Form.Element.DelayedObserver = Class.create();
  656. Form.Element.DelayedObserver.prototype = {
  657. initialize: function(element, delay, callback) {
  658. this.delay = delay || 0.5;
  659. this.element = $(element);
  660. this.callback = callback;
  661. this.timer = null;
  662. this.lastValue = $F(this.element);
  663. Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  664. },
  665. delayedListener: function(event) {
  666. if(this.lastValue == $F(this.element)) return;
  667. if(this.timer) clearTimeout(this.timer);
  668. this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
  669. this.lastValue = $F(this.element);
  670. },
  671. onTimerEvent: function() {
  672. this.timer = null;
  673. this.callback(this.element, $F(this.element));
  674. }
  675. };