Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

inplace editing of title and description

  • Loading branch information...
commit 245ecbc3126bddda004ae7f35887ea4aa7bac2aa 1 parent 775b5b1
JP Mens jpmens authored
392 _attachments/lib/jquery-ui.min.js
392 additions, 0 deletions not shown
644 _attachments/lib/jquery.editinplace.js
... ... @@ -0,0 +1,644 @@
  1 +/*
  2 +
  3 +A jQuery edit in place plugin
  4 +
  5 +Version 2.2.0
  6 +
  7 +Authors:
  8 + Dave Hauenstein
  9 + Martin Häcker <spamfaenger [at] gmx [dot] de>
  10 +
  11 +Project home:
  12 + http://code.google.com/p/jquery-in-place-editor/
  13 +
  14 +Patches with tests welcomed! For guidance see the tests at </spec/unit/spec.js>. To submit, attach them to the bug tracker.
  15 +
  16 +License:
  17 +This source file is subject to the BSD license bundled with this package.
  18 +Available online: {@link http://www.opensource.org/licenses/bsd-license.php}
  19 +If you did not receive a copy of the license, and are unable to obtain it,
  20 +learn to use a search engine.
  21 +
  22 +*/
  23 +
  24 +(function($){
  25 +
  26 +$.fn.editInPlace = function(options) {
  27 +
  28 + var settings = $.extend({}, $.fn.editInPlace.defaults, options);
  29 +
  30 + assertMandatorySettingsArePresent(settings);
  31 +
  32 + preloadImage(settings.saving_image);
  33 +
  34 + return this.each(function() {
  35 + var dom = $(this);
  36 + // This won't work with live queries as there is no specific element to attach this
  37 + // one way to deal with this could be to store a reference to self and then compare that in click?
  38 + if (dom.data('editInPlace'))
  39 + return; // already an editor here
  40 + dom.data('editInPlace', true);
  41 +
  42 + new InlineEditor(settings, dom).init();
  43 + });
  44 +};
  45 +
  46 +/// Switch these through the dictionary argument to $(aSelector).editInPlace(overideOptions)
  47 +/// Required Options: Either url or callback, so the editor knows what to do with the edited values.
  48 +$.fn.editInPlace.defaults = {
  49 + url: "", // string: POST URL to send edited content
  50 + bg_over: "#ffc", // string: background color of hover of unactivated editor
  51 + bg_out: "transparent", // string: background color on restore from hover
  52 + hover_class: "", // string: class added to root element during hover. Will override bg_over and bg_out
  53 + show_buttons: false, // boolean: will show the buttons: cancel or save; will automatically cancel out the onBlur functionality
  54 + save_button: '<button class="inplace_save">Save</button>', // string: image button tag to use as “Save” button
  55 + cancel_button: '<button class="inplace_cancel">Cancel</button>', // string: image button tag to use as “Cancel” button
  56 + params: "", // string: example: first_name=dave&last_name=hauenstein extra paramters sent via the post request to the server
  57 + field_type: "text", // string: "text", "textarea", or "select"; The type of form field that will appear on instantiation
  58 + default_text: "(Click here to add text)", // string: text to show up if the element that has this functionality is empty
  59 + use_html: false, // boolean, set to true if the editor should use jQuery.fn.html() to extract the value to show from the dom node
  60 + textarea_rows: 10, // integer: set rows attribute of textarea, if field_type is set to textarea. Use CSS if possible though
  61 + textarea_cols: 25, // integer: set cols attribute of textarea, if field_type is set to textarea. Use CSS if possible though
  62 + select_text: "Choose new value", // string: default text to show up in select box
  63 + select_options: "", // string or array: Used if field_type is set to 'select'. Can be comma delimited list of options 'textandValue,text:value', Array of options ['textAndValue', 'text:value'] or array of arrays ['textAndValue', ['text', 'value']]. The last form is especially usefull if your labels or values contain colons)
  64 + text_size: null, // integer: set cols attribute of text input, if field_type is set to text. Use CSS if possible though
  65 +
  66 + // Specifying callback_skip_dom_reset will disable all saving_* options
  67 + saving_text: undefined, // string: text to be used when server is saving information. Example "Saving..."
  68 + saving_image: "", // string: uses saving text specify an image location instead of text while server is saving
  69 + saving_animation_color: 'transparent', // hex color string, will be the color the pulsing animation during the save pulses to. Note: Only works if jquery-ui is loaded
  70 +
  71 + value_required: false, // boolean: if set to true, the element will not be saved unless a value is entered
  72 + element_id: "element_id", // string: name of parameter holding the id or the editable
  73 + update_value: "update_value", // string: name of parameter holding the updated/edited value
  74 + original_value: 'original_value', // string: name of parameter holding the updated/edited value
  75 + original_html: "original_html", // string: name of parameter holding original_html value of the editable /* DEPRECATED in 2.2.0 */ use original_value instead.
  76 + save_if_nothing_changed: false, // boolean: submit to function or server even if the user did not change anything
  77 + on_blur: "save", // string: "save" or null; what to do on blur; will be overridden if show_buttons is true
  78 + cancel: "", // string: if not empty, a jquery selector for elements that will not cause the editor to open even though they are clicked. E.g. if you have extra buttons inside editable fields
  79 +
  80 + // All callbacks will have this set to the DOM node of the editor that triggered the callback
  81 +
  82 + callback: null, // function: function to be called when editing is complete; cancels ajax submission to the url param. Prototype: function(idOfEditor, enteredText, orinalHTMLContent, settingsParams, callbacks). The function needs to return the value that should be shown in the dom. Returning undefined means cancel and will restore the dom and trigger an error. callbacks is a dictionary with two functions didStartSaving and didEndSaving() that you can use to tell the inline editor that it should start and stop any saving animations it has configured. /* DEPRECATED in 2.1.0 */ Parameter idOfEditor, use $(this).attr('id') instead
  83 + callback_skip_dom_reset: false, // boolean: set this to true if the callback should handle replacing the editor with the new value to show
  84 + success: null, // function: this function gets called if server responds with a success. Prototype: function(newEditorContentString)
  85 + error: null, // function: this function gets called if server responds with an error. Prototype: function(request)
  86 + error_sink: function(idOfEditor, errorString) { alert(errorString); }, // function: gets id of the editor and the error. Make sure the editor has an id, or it will just be undefined. If set to null, no error will be reported. /* DEPRECATED in 2.1.0 */ Parameter idOfEditor, use $(this).attr('id') instead
  87 + preinit: null, // function: this function gets called after a click on an editable element but before the editor opens. If you return false, the inline editor will not open. Prototype: function(currentDomNode). DEPRECATED in 2.2.0 use delegate shouldOpenEditInPlace call instead
  88 + postclose: null, // function: this function gets called after the inline editor has closed and all values are updated. Prototype: function(currentDomNode). DEPRECATED in 2.2.0 use delegate didCloseEditInPlace call instead
  89 + delegate: null // object: if it has methods with the name of the callbacks documented below in delegateExample these will be called. This means that you just need to impelment the callbacks you are interested in.
  90 +};
  91 +
  92 +// Lifecycle events that the delegate can implement
  93 +// this will always be fixed to the delegate
  94 +var delegateExample = {
  95 + // called while opening the editor.
  96 + // return false to prevent editor from opening
  97 + shouldOpenEditInPlace: function(aDOMNode, aSettingsDict, triggeringEvent) {},
  98 + // return content to show in inplace editor
  99 + willOpenEditInPlace: function(aDOMNode, aSettingsDict) {},
  100 + didOpenEditInPlace: function(aDOMNode, aSettingsDict) {},
  101 +
  102 + // called while closing the editor
  103 + // return false to prevent the editor from closing
  104 + shouldCloseEditInPlace: function(aDOMNode, aSettingsDict, triggeringEvent) {},
  105 + // return value will be shown during saving
  106 + willCloseEditInPlace: function(aDOMNode, aSettingsDict) {},
  107 + didCloseEditInPlace: function(aDOMNode, aSettingsDict) {},
  108 +
  109 + missingCommaErrorPreventer:''
  110 +};
  111 +
  112 +
  113 +function InlineEditor(settings, dom) {
  114 + this.settings = settings;
  115 + this.dom = dom;
  116 + this.originalValue = null;
  117 + this.didInsertDefaultText = false;
  118 + this.shouldDelayReinit = false;
  119 +};
  120 +
  121 +$.extend(InlineEditor.prototype, {
  122 +
  123 + init: function() {
  124 + this.setDefaultTextIfNeccessary();
  125 + this.connectOpeningEvents();
  126 + },
  127 +
  128 + reinit: function() {
  129 + if (this.shouldDelayReinit)
  130 + return;
  131 +
  132 + this.triggerCallback(this.settings.postclose, /* DEPRECATED in 2.1.0 */ this.dom);
  133 + this.triggerDelegateCall('didCloseEditInPlace');
  134 +
  135 + this.markEditorAsInactive();
  136 + this.connectOpeningEvents();
  137 + },
  138 +
  139 + setDefaultTextIfNeccessary: function() {
  140 + if('' !== this.dom.html())
  141 + return;
  142 +
  143 + this.dom.html(this.settings.default_text);
  144 + this.didInsertDefaultText = true;
  145 + },
  146 +
  147 + connectOpeningEvents: function() {
  148 + var that = this;
  149 + this.dom
  150 + .bind('mouseenter.editInPlace', function(){ that.addHoverEffect(); })
  151 + .bind('mouseleave.editInPlace', function(){ that.removeHoverEffect(); })
  152 + .bind('click.editInPlace', function(anEvent){ that.openEditor(anEvent); });
  153 + },
  154 +
  155 + disconnectOpeningEvents: function() {
  156 + // prevent re-opening the editor when it is already open
  157 + this.dom.unbind('.editInPlace');
  158 + },
  159 +
  160 + addHoverEffect: function() {
  161 + if (this.settings.hover_class)
  162 + this.dom.addClass(this.settings.hover_class);
  163 + else
  164 + this.dom.css("background-color", this.settings.bg_over);
  165 + },
  166 +
  167 + removeHoverEffect: function() {
  168 + if (this.settings.hover_class)
  169 + this.dom.removeClass(this.settings.hover_class);
  170 + else
  171 + this.dom.css("background-color", this.settings.bg_out);
  172 + },
  173 +
  174 + openEditor: function(anEvent) {
  175 + if ( ! this.shouldOpenEditor(anEvent))
  176 + return;
  177 +
  178 + this.workAroundFirefoxBlurBug();
  179 + this.disconnectOpeningEvents();
  180 + this.removeHoverEffect();
  181 + this.removeInsertedDefaultTextIfNeccessary();
  182 + this.saveOriginalValue();
  183 + this.markEditorAsActive();
  184 + this.replaceContentWithEditor();
  185 + this.connectOpeningEventsToEditor();
  186 + this.triggerDelegateCall('didOpenEditInPlace');
  187 + },
  188 +
  189 + shouldOpenEditor: function(anEvent) {
  190 + if (this.isClickedObjectCancelled(anEvent.target))
  191 + return false;
  192 +
  193 + if (false === this.triggerCallback(this.settings.preinit, /* DEPRECATED in 2.1.0 */ this.dom))
  194 + return false;
  195 +
  196 + if (false === this.triggerDelegateCall('shouldOpenEditInPlace', true, anEvent))
  197 + return false;
  198 +
  199 + return true;
  200 + },
  201 +
  202 + removeInsertedDefaultTextIfNeccessary: function() {
  203 + if ( ! this.didInsertDefaultText
  204 + || this.dom.html() !== this.settings.default_text)
  205 + return;
  206 +
  207 + this.dom.html('');
  208 + this.didInsertDefaultText = false;
  209 + },
  210 +
  211 + isClickedObjectCancelled: function(eventTarget) {
  212 + if ( ! this.settings.cancel)
  213 + return false;
  214 +
  215 + var eventTargetAndParents = $(eventTarget).parents().andSelf();
  216 + var elementsMatchingCancelSelector = eventTargetAndParents.filter(this.settings.cancel);
  217 + return 0 !== elementsMatchingCancelSelector.length;
  218 + },
  219 +
  220 + saveOriginalValue: function() {
  221 + if (this.settings.use_html)
  222 + this.originalValue = this.dom.html();
  223 + else
  224 + this.originalValue = trim(this.dom.text());
  225 + },
  226 +
  227 + restoreOriginalValue: function() {
  228 + this.setClosedEditorContent(this.originalValue);
  229 + },
  230 +
  231 + setClosedEditorContent: function(aValue) {
  232 + if (this.settings.use_html)
  233 + this.dom.html(aValue);
  234 + else
  235 + this.dom.text(aValue);
  236 + },
  237 +
  238 + workAroundFirefoxBlurBug: function() {
  239 + if ( ! $.browser.mozilla)
  240 + return;
  241 +
  242 + // TODO: Opera seems to also have this bug....
  243 +
  244 + // Firefox will forget to send a blur event to an input element when another one is
  245 + // created and selected programmatically. This means that if another inline editor is
  246 + // opened, existing inline editors will _not_ close if they are configured to submit when blurred.
  247 + // This is actually the first time I've written browser specific code for a browser different than IE! Wohoo!
  248 +
  249 + // Using parents() instead document as base to workaround the fact that in the unittests
  250 + // the editor is not a child of window.document but of a document fragment
  251 + this.dom.parents(':last').find('.editInPlace-active :input').blur();
  252 + },
  253 +
  254 + replaceContentWithEditor: function() {
  255 + var buttons_html = (this.settings.show_buttons) ? this.settings.save_button + ' ' + this.settings.cancel_button : '';
  256 + var editorElement = this.createEditorElement(); // needs to happen before anything is replaced
  257 + /* insert the new in place form after the element they click, then empty out the original element */
  258 + this.dom.html('<form class="inplace_form" style="display: inline; margin: 0; padding: 0;"></form>')
  259 + .find('form')
  260 + .append(editorElement)
  261 + .append(buttons_html);
  262 + },
  263 +
  264 + createEditorElement: function() {
  265 + if (-1 === $.inArray(this.settings.field_type, ['text', 'textarea', 'select']))
  266 + throw "Unknown field_type <fnord>, supported are 'text', 'textarea' and 'select'";
  267 +
  268 + var editor = null;
  269 + if ("select" === this.settings.field_type)
  270 + editor = this.createSelectEditor();
  271 + else if ("text" === this.settings.field_type)
  272 + editor = $('<input type="text" ' + this.inputNameAndClass()
  273 + + ' size="' + this.settings.text_size + '" />');
  274 + else if ("textarea" === this.settings.field_type)
  275 + editor = $('<textarea ' + this.inputNameAndClass()
  276 + + ' rows="' + this.settings.textarea_rows + '" '
  277 + + ' cols="' + this.settings.textarea_cols + '" />');
  278 +
  279 + editor.val(this.triggerDelegateCall('willOpenEditInPlace', this.originalValue));
  280 + return editor;
  281 + },
  282 +
  283 + inputNameAndClass: function() {
  284 + return ' name="inplace_value" class="inplace_field" ';
  285 + },
  286 +
  287 + createSelectEditor: function() {
  288 + var editor = $('<select' + this.inputNameAndClass() + '>'
  289 + + '<option disabled="true" value="">' + this.settings.select_text + '</option>'
  290 + + '</select>');
  291 +
  292 + var optionsArray = this.settings.select_options;
  293 + if ( ! $.isArray(optionsArray))
  294 + optionsArray = optionsArray.split(',');
  295 +
  296 + for (var i=0; i<optionsArray.length; i++) {
  297 +
  298 + var currentTextAndValue = optionsArray[i];
  299 + if ( ! $.isArray(currentTextAndValue))
  300 + currentTextAndValue = currentTextAndValue.split(':');
  301 +
  302 + var value = trim(currentTextAndValue[1] || currentTextAndValue[0]);
  303 + var text = trim(currentTextAndValue[0]);
  304 +
  305 + var selected = (value == this.originalValue) ? 'selected="selected" ' : '';
  306 + var option = $('<option ' + selected + ' ></option>').val(value).text(text);
  307 + editor.append(option);
  308 + }
  309 + return editor;
  310 +
  311 + },
  312 +
  313 + // REFACT: rename opening is not what it's about. Its about closing events really
  314 + connectOpeningEventsToEditor: function() {
  315 + var that = this;
  316 + function cancelEditorAction(anEvent) {
  317 + that.handleCancelEditor(anEvent);
  318 + return false; // stop event bubbling
  319 + }
  320 + function saveEditorAction(anEvent) {
  321 + that.handleSaveEditor(anEvent);
  322 + return false; // stop event bubbling
  323 + }
  324 +
  325 + var form = this.dom.find("form");
  326 +
  327 + form.find(".inplace_field").focus().select();
  328 + form.find(".inplace_cancel").click(cancelEditorAction);
  329 + form.find(".inplace_save").click(saveEditorAction);
  330 +
  331 + if ( ! this.settings.show_buttons) {
  332 + // TODO: Firefox has a bug where blur is not reliably called when focus is lost
  333 + // (for example by another editor appearing)
  334 + if ("save" === this.settings.on_blur)
  335 + form.find(".inplace_field").blur(saveEditorAction);
  336 + else
  337 + form.find(".inplace_field").blur(cancelEditorAction);
  338 +
  339 + // workaround for firefox bug where it won't submit on enter if no button is shown
  340 + if ($.browser.mozilla)
  341 + this.bindSubmitOnEnterInInput();
  342 + }
  343 +
  344 + form.keyup(function(anEvent) {
  345 + // allow canceling with escape
  346 + var escape = 27;
  347 + if (escape === anEvent.which)
  348 + return cancelEditorAction();
  349 + });
  350 +
  351 + // workaround for webkit nightlies where they won't submit at all on enter
  352 + // REFACT: find a way to just target the nightlies
  353 + if ($.browser.safari)
  354 + this.bindSubmitOnEnterInInput();
  355 +
  356 +
  357 + form.submit(saveEditorAction);
  358 + },
  359 +
  360 + bindSubmitOnEnterInInput: function() {
  361 + if ('textarea' === this.settings.field_type)
  362 + return; // can't enter newlines otherwise
  363 +
  364 + var that = this;
  365 + this.dom.find(':input').keyup(function(event) {
  366 + var enter = 13;
  367 + if (enter === event.which)
  368 + return that.dom.find('form').submit();
  369 + });
  370 +
  371 + },
  372 +
  373 + handleCancelEditor: function(anEvent) {
  374 + // REFACT: remove duplication between save and cancel
  375 + if (false === this.triggerDelegateCall('shouldCloseEditInPlace', true, anEvent))
  376 + return;
  377 +
  378 + var enteredText = this.dom.find(':input').val();
  379 + enteredText = this.triggerDelegateCall('willCloseEditInPlace', enteredText);
  380 +
  381 + this.restoreOriginalValue();
  382 + if (hasContent(enteredText)
  383 + && ! this.isDisabledDefaultSelectChoice())
  384 + this.setClosedEditorContent(enteredText);
  385 + this.reinit();
  386 + },
  387 +
  388 + handleSaveEditor: function(anEvent) {
  389 + if (false === this.triggerDelegateCall('shouldCloseEditInPlace', true, anEvent))
  390 + return;
  391 +
  392 + var enteredText = this.dom.find(':input').val();
  393 + enteredText = this.triggerDelegateCall('willCloseEditInPlace', enteredText);
  394 +
  395 + if (this.isDisabledDefaultSelectChoice()
  396 + || this.isUnchangedInput(enteredText)) {
  397 + this.handleCancelEditor(anEvent);
  398 + return;
  399 + }
  400 +
  401 + if (this.didForgetRequiredText(enteredText)) {
  402 + this.handleCancelEditor(anEvent);
  403 + this.reportError("Error: You must enter a value to save this field");
  404 + return;
  405 + }
  406 +
  407 + this.showSaving(enteredText);
  408 +
  409 + if (this.settings.callback)
  410 + this.handleSubmitToCallback(enteredText);
  411 + else
  412 + this.handleSubmitToServer(enteredText);
  413 + },
  414 +
  415 + didForgetRequiredText: function(enteredText) {
  416 + return this.settings.value_required
  417 + && ("" === enteredText
  418 + || undefined === enteredText
  419 + || null === enteredText);
  420 + },
  421 +
  422 + isDisabledDefaultSelectChoice: function() {
  423 + return this.dom.find('option').eq(0).is(':selected:disabled');
  424 + },
  425 +
  426 + isUnchangedInput: function(enteredText) {
  427 + return ! this.settings.save_if_nothing_changed
  428 + && this.originalValue === enteredText;
  429 + },
  430 +
  431 + showSaving: function(enteredText) {
  432 + if (this.settings.callback && this.settings.callback_skip_dom_reset)
  433 + return;
  434 +
  435 + var savingMessage = enteredText;
  436 + if (hasContent(this.settings.saving_text))
  437 + savingMessage = this.settings.saving_text;
  438 + if(hasContent(this.settings.saving_image))
  439 + // REFACT: alt should be the configured saving message
  440 + savingMessage = $('<img />').attr('src', this.settings.saving_image).attr('alt', savingMessage);
  441 + this.dom.html(savingMessage);
  442 + },
  443 +
  444 + handleSubmitToCallback: function(enteredText) {
  445 + // REFACT: consider to encode enteredText and originalHTML before giving it to the callback
  446 + this.enableOrDisableAnimationCallbacks(true, false);
  447 + var newHTML = this.triggerCallback(this.settings.callback, /* DEPRECATED in 2.1.0 */ this.id(), enteredText, this.originalValue,
  448 + this.settings.params, this.savingAnimationCallbacks());
  449 +
  450 + if (this.settings.callback_skip_dom_reset)
  451 + ; // do nothing
  452 + else if (undefined === newHTML) {
  453 + // failure; put original back
  454 + this.reportError("Error: Failed to save value: " + enteredText);
  455 + this.restoreOriginalValue();
  456 + }
  457 + else
  458 + // REFACT: use setClosedEditorContent
  459 + this.dom.html(newHTML);
  460 +
  461 + if (this.didCallNoCallbacks()) {
  462 + this.enableOrDisableAnimationCallbacks(false, false);
  463 + this.reinit();
  464 + }
  465 + },
  466 +
  467 + handleSubmitToServer: function(enteredText) {
  468 + var data = this.settings.update_value + '=' + encodeURIComponent(enteredText)
  469 + + '&' + this.settings.element_id + '=' + this.dom.attr("id")
  470 + + ((this.settings.params) ? '&' + this.settings.params : '')
  471 + + '&' + this.settings.original_html + '=' + encodeURIComponent(this.originalValue) /* DEPRECATED in 2.2.0 */
  472 + + '&' + this.settings.original_value + '=' + encodeURIComponent(this.originalValue);
  473 +
  474 + this.enableOrDisableAnimationCallbacks(true, false);
  475 + this.didStartSaving();
  476 + var that = this;
  477 + $.ajax({
  478 + url: that.settings.url,
  479 + type: "POST",
  480 + data: data,
  481 + dataType: "html",
  482 + complete: function(request){
  483 + that.didEndSaving();
  484 + },
  485 + success: function(html){
  486 + var new_text = html || that.settings.default_text;
  487 +
  488 + /* put the newly updated info into the original element */
  489 + // FIXME: should be affected by the preferences switch
  490 + that.dom.html(new_text);
  491 + // REFACT: remove dom parameter, already in this, not documented, should be easy to remove
  492 + // REFACT: callback should be able to override what gets put into the DOM
  493 + that.triggerCallback(that.settings.success, html);
  494 + },
  495 + error: function(request) {
  496 + that.dom.html(that.originalHTML); // REFACT: what about a restorePreEditingContent()
  497 + if (that.settings.error)
  498 + // REFACT: remove dom parameter, already in this, not documented, can remove without deprecation
  499 + // REFACT: callback should be able to override what gets entered into the DOM
  500 + that.triggerCallback(that.settings.error, request);
  501 + else
  502 + that.reportError("Failed to save value: " + request.responseText || 'Unspecified Error');
  503 + }
  504 + });
  505 + },
  506 +
  507 + // Utilities .........................................................
  508 +
  509 + triggerCallback: function(aCallback /*, arguments */) {
  510 + if ( ! aCallback)
  511 + return; // callback wasn't specified after all
  512 +
  513 + var callbackArguments = Array.prototype.splice.call(arguments, 1);
  514 + return aCallback.apply(this.dom[0], callbackArguments);
  515 + },
  516 +
  517 + /// defaultReturnValue is only used if the delegate returns undefined
  518 + triggerDelegateCall: function(aDelegateMethodName, defaultReturnValue, optionalEvent) {
  519 + // REFACT: consider to trigger equivalent callbacks automatically via a mapping table?
  520 + if ( ! this.settings.delegate
  521 + || ! $.isFunction(this.settings.delegate[aDelegateMethodName]))
  522 + return defaultReturnValue;
  523 +
  524 + var delegateReturnValue = this.settings.delegate[aDelegateMethodName](this.dom, this.settings, optionalEvent);
  525 + return (undefined === delegateReturnValue)
  526 + ? defaultReturnValue
  527 + : delegateReturnValue;
  528 + },
  529 +
  530 + reportError: function(anErrorString) {
  531 + this.triggerCallback(this.settings.error_sink, /* DEPRECATED in 2.1.0 */ this.id(), anErrorString);
  532 + },
  533 +
  534 + // REFACT: this method should go, callbacks should get the dom node itself as an argument
  535 + id: function() {
  536 + return this.dom.attr('id');
  537 + },
  538 +
  539 + markEditorAsActive: function() {
  540 + this.dom.addClass('editInPlace-active');
  541 + },
  542 +
  543 + markEditorAsInactive: function() {
  544 + this.dom.removeClass('editInPlace-active');
  545 + },
  546 +
  547 + // REFACT: consider rename, doesn't deal with animation directly
  548 + savingAnimationCallbacks: function() {
  549 + var that = this;
  550 + return {
  551 + didStartSaving: function() { that.didStartSaving(); },
  552 + didEndSaving: function() { that.didEndSaving(); }
  553 + };
  554 + },
  555 +
  556 + enableOrDisableAnimationCallbacks: function(shouldEnableStart, shouldEnableEnd) {
  557 + this.didStartSaving.enabled = shouldEnableStart;
  558 + this.didEndSaving.enabled = shouldEnableEnd;
  559 + },
  560 +
  561 + didCallNoCallbacks: function() {
  562 + return this.didStartSaving.enabled && ! this.didEndSaving.enabled;
  563 + },
  564 +
  565 + assertCanCall: function(methodName) {
  566 + if ( ! this[methodName].enabled)
  567 + throw new Error('Cannot call ' + methodName + ' now. See documentation for details.');
  568 + },
  569 +
  570 + didStartSaving: function() {
  571 + this.assertCanCall('didStartSaving');
  572 + this.shouldDelayReinit = true;
  573 + this.enableOrDisableAnimationCallbacks(false, true);
  574 +
  575 + this.startSavingAnimation();
  576 + },
  577 +
  578 + didEndSaving: function() {
  579 + this.assertCanCall('didEndSaving');
  580 + this.shouldDelayReinit = false;
  581 + this.enableOrDisableAnimationCallbacks(false, false);
  582 + this.reinit();
  583 +
  584 + this.stopSavingAnimation();
  585 + },
  586 +
  587 + startSavingAnimation: function() {
  588 + var that = this;
  589 + this.dom
  590 + .animate({ backgroundColor: this.settings.saving_animation_color }, 400)
  591 + .animate({ backgroundColor: 'transparent'}, 400, 'swing', function(){
  592 + // In the tests animations are turned off - i.e they happen instantaneously.
  593 + // Hence we need to prevent this from becomming an unbounded recursion.
  594 + setTimeout(function(){ that.startSavingAnimation(); }, 10);
  595 + });
  596 + },
  597 +
  598 + stopSavingAnimation: function() {
  599 + this.dom
  600 + .stop(true)
  601 + .css({backgroundColor: ''});
  602 + },
  603 +
  604 + missingCommaErrorPreventer:''
  605 +});
  606 +
  607 +
  608 +
  609 +// Private helpers .......................................................
  610 +
  611 +function assertMandatorySettingsArePresent(options) {
  612 + // one of these needs to be non falsy
  613 + if (options.url || options.callback)
  614 + return;
  615 +
  616 + throw new Error("Need to set either url: or callback: option for the inline editor to work.");
  617 +}
  618 +
  619 +/* preload the loading icon if it is configured */
  620 +function preloadImage(anImageURL) {
  621 + if ('' === anImageURL)
  622 + return;
  623 +
  624 + var loading_image = new Image();
  625 + loading_image.src = anImageURL;
  626 +}
  627 +
  628 +function trim(aString) {
  629 + return aString
  630 + .replace(/^\s+/, '')
  631 + .replace(/\s+$/, '');
  632 +}
  633 +
  634 +function hasContent(something) {
  635 + if (undefined === something || null === something)
  636 + return false;
  637 +
  638 + if (0 === something.length)
  639 + return false;
  640 +
  641 + return true;
  642 +}
  643 +
  644 +})(jQuery);
1  shows/bookmark.js
@@ -8,6 +8,7 @@ function(doc, req)
8 8 doc.header = { // for partials
9 9 title: 'Scrumptious Bookmark ' + doc._id,
10 10 cssdir: path.asset('style'),
  11 + libdir: path.asset('lib'),
11 12 };
12 13 doc.footer = {};
13 14 //doc.listall = path.list('ls', 'all') + '?limit=20';
39 templates/bookmark.html
@@ -2,7 +2,7 @@
2 2
3 3 <h2>
4 4 <span class='docid'><a href="{{listall}}">&#x2672;</a></span>
5   - <a href="{{url}}">{{title}}</a>
  5 + <a href="{{url}}">&gt;</a> <span id='edittitle'>{{title}}</span>
6 6 </h2>
7 7
8 8
@@ -19,16 +19,18 @@
19 19 <div class="date">{{date}}</div>
20 20 </div>
21 21
22   - <div class="desc">{{description}}
23   - <div id="deletedoc"><a href="#">(delete bookmark)</a></div>
  22 + <div class="desc"><p id='editdesc'>{{description}}</p>
  23 + <div id="deletedoc"><a href="#">(delete bookmark)</a></div>
24 24 </div>
25 25
26 26 </div>
27 27
28 28 <script type="text/javascript">
29 29 $(document).ready(function() {
  30 +
30 31 var id = '{{_id}}';
31 32 $db = $.couch.db('{{dbname}}');
  33 +
32 34 $("#deletedoc").click(function() {
33 35 $db.openDoc(id, {
34 36 success: function(doc) {
@@ -39,6 +41,37 @@
39 41 });
40 42 });
41 43
  44 + function updatedoc(id, field, value)
  45 + {
  46 + $db.openDoc(id, {
  47 + success: function(doc) {
  48 + doc[field] = value;
  49 + $db.saveDoc(doc);
  50 + }
  51 + });
  52 + }
  53 +
  54 + // EDITS
  55 + $("#edittitle").editInPlace({
  56 + callback: function(unused, intext) {
  57 + updatedoc(id, 'title', intext);
  58 + return intext;
  59 + },
  60 + show_buttons: false,
  61 + bg_over: "#79aad2",
  62 + });
  63 +
  64 + $("#editdesc").editInPlace({
  65 + callback: function(unused, intext) {
  66 + updatedoc(id, 'description', intext);
  67 + return intext;
  68 + },
  69 + bg_over: "#79aad2",
  70 + field_type: "textarea",
  71 + textarea_rows: "15",
  72 + textarea_cols: "35",
  73 + show_buttons: false,
  74 + });
42 75 });
43 76 </script>
44 77
53 templates/index/head.html
@@ -4,14 +4,61 @@
4 4 <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5 5 <link rel="stylesheet" href="../../style/screen.css" type="text/css"/>
6 6 <link rel="shortcut icon" href="../../style/favicon.ico">
  7 + <link rel="stylesheet" href="../../style/jquery-ui.css" type="text/css"/>
7 8 <title>{{title}}</title>
8 9 </head>
9 10 <body>
10 11
11   -<!--
12   - <h2><span class='docid'> <a href="bydate?descending=true">&#x2672;</a></span> Scrumptious Bookmarks in CouchDB</h2>
13   --->
14 12 <h2><span class='docid'> <a href="bydate?limit=20&descending=true">&#x2672;</a></span> Scrumptious Bookmarks in CouchDB</h2>
15 13
  14 +<!-- begin search -->
  15 + <script src="/_utils/script/json2.js"></script> <!-- from CouchDB /_utils -->
  16 + <script src="/_utils/script/jquery.js?1.3.1"></script>
  17 + <script src="/_utils/script/jquery.couch.js?0.9.0"></script>
  18 + <script src="../../lib/jquery-ui.min.js"></script>
  19 +
  20 + <form id='searchform'>
  21 + <input id='search' name='keyword' type='text' />
  22 + </form>
  23 +
  24 +<script type="text/javascript">
  25 +$(document).ready(function() {
  26 +
  27 + // http://docs.jquery.com/UI/API/1.8/Autocomplete
  28 + $('input#search').autocomplete({
  29 + source: function(request, response) {
  30 + // response([ 'one', 'dos', 'trois' ]);
  31 + $db = $.couch.db('scrumptious');
  32 + var kw = request.term;
  33 + $db.view('app/words', {
  34 + keys: [ kw ],
  35 + success: function(doc) {
  36 +
  37 + // alert('hi ' + JSON.stringify(doc));
  38 + var r = [];
  39 + for (var i = 0; i < doc.rows.length; i++) {
  40 +
  41 + var rv = {
  42 + label: doc.rows[i].value.title,
  43 + value: doc.rows[i].value.url,
  44 + };
  45 + r.push(rv);
  46 + }
  47 + response(r);
  48 + }
  49 + });
  50 +
  51 +
  52 + },
  53 + select: function(event, ui) {
  54 + alert("selected: " + JSON.stringify(ui.item));
  55 + return false;
  56 + }
  57 + });
  58 +
  59 +});
  60 +</script>
  61 +
  62 + <!-- end search -->
16 63
17 64 <ul id='listall'>
7 templates/partials/header.html
@@ -6,8 +6,13 @@
6 6 <link rel="shortcut icon" href="{{cssdir}}/favicon.ico">
7 7
8 8 <script src="/_utils/script/json2.js"></script> <!-- from CouchDB /_utils -->
9   - <script src="/_utils/script/jquery.js?1.3.1"></script>
  9 + <script src="/_utils/script/jquery.js?1.3.1"></script>
10 10 <script src="/_utils/script/jquery.couch.js?0.9.0"></script>
  11 +
  12 + <script src="{{libdir}}/jquery-ui.min.js" type="text/javascript"></script>
  13 + <script src="{{libdir}}/jquery.editinplace.js" type="text/javascript"></script>
  14 + <link rel="stylesheet" href="../../style/jquery-ui.css" type="text/css"/>
  15 +
11 16 <title>{{title}}</title>
12 17 </head>
13 18 <body>

0 comments on commit 245ecbc

Please sign in to comment.
Something went wrong with that request. Please try again.