Add a custom version of scriptaculous' inplaceeditor.
authorMichael J. Rubinsky <mrubinsk@horde.org>
Fri, 23 Jul 2010 18:59:08 +0000 (14:59 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Fri, 23 Jul 2010 19:01:19 +0000 (15:01 -0400)
More effecient usage, adds more options, use prototype elements, events.
Better CSS.

ansel/js/editcaption.js [deleted file]
ansel/lib/Ajax/Imple/EditCaption.php
ansel/lib/Tile/Image.php
ansel/lib/View/Image.php
ansel/themes/screen.css
horde/js/inplaceeditor.js [new file with mode: 0644]
horde/themes/screen.css

diff --git a/ansel/js/editcaption.js b/ansel/js/editcaption.js
deleted file mode 100644 (file)
index 3b49ac2..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-// InPlaceEditor extension based somewhat on an example given in the
-// scriptaculous wiki
-Ajax.InPlaceEditor.prototype.__initialize = Ajax.InPlaceEditor.prototype.initialize;
-Ajax.InPlaceEditor.prototype.__getText = Ajax.InPlaceEditor.prototype.getText;
-Object.extend(Ajax.InPlaceEditor.prototype, {
-    initialize: function(element, url, options) {
-        this.__initialize(element, url, options);
-        this.setOptions(options);
-        // Remove this line to stop from auto-showing the
-        // empty caption text on page load.
-        this.checkEmpty();
-    },
-
-    setOptions: function(options) {
-        this.options = Object.extend(Object.extend(this.options, {
-            emptyClassName: 'inplaceeditor-empty'
-        }),options||{});
-    },
-
-    checkEmpty: function() {
-        if (this.element.innerHTML.length == 0) {
-            emptyNode = new Element('span', {className: this.options.emptyClassName}).update(this.options.emptyText);
-            this.element.appendChild(emptyNode);
-        }
-    },
-
-    getText: function() {
-        $(this.element).select('.' + this.options.emptyClassName).each(function(child) {
-            this.element.removeChild(child);
-        }.bind(this));
-        return this.__getText();
-    }
-});
-
-function tileExit(ipe, e)
-{
-    ipe.checkEmpty();
-}
index 799f1de..0333160 100644 (file)
@@ -25,8 +25,7 @@ class Ansel_Ajax_Imple_EditCaption extends Horde_Core_Ajax_Imple
     public function attach()
     {
         Horde::addScriptFile('effects.js', 'horde');
-        Horde::addScriptFile('controls.js', 'horde');
-        Horde::addScriptFile('editcaption.js', 'ansel');
+        Horde::addScriptFile('inplaceeditor.js', 'horde');
 
         $params = array('input' => 'value',
                         'id' => $this->_params['id']);
@@ -35,14 +34,17 @@ class Ansel_Ajax_Imple_EditCaption extends Horde_Core_Ajax_Imple
         $loadTextUrl = $this->_getUrl('EditCaption', 'ansel', array_merge($params, array('action' => 'load')));
         $js = array();
 
-        $js[] = "new Ajax.InPlaceEditor('" . $this->_params['domid'] . "', '" . $url . "', {"
-                . "    callback: function(form, value) {"
-                . "      return 'value=' + encodeURIComponent(value);},"
+        $js[] = "new InPlaceEditor('" . $this->_params['domid'] . "', '" . $url . "', {"
+                . "   callback: function(form, value) {"
+                . "       return 'value=' + encodeURIComponent(value);},"
                 . "   loadTextURL: '". $loadTextUrl . "',"
                 . "   rows:" . $this->_params['rows'] . ","
-                . "   cols:" . $this->_params['cols'] . ","
+                . "   width:" . $this->_params['width'] . ","
                 . "   emptyText: '" . _("Click to add caption...") . "',"
-                . "   onComplete: function(transport, element) {tileExit(this);}"
+                . "   onComplete: function(ipe, opts) { ipe.checkEmpty() },"
+                . "   cancelText: '" . _("Cancel") . "',"
+                . "   okText: '" . _("Ok") . "',"
+                . "   cancelClassName: ''"
                 . "  });";
 
         Horde::addInlineScript($js, 'dom');
index 8eacff0..f1cd74c 100644 (file)
@@ -115,9 +115,11 @@ class Ansel_Tile_Image
         Horde::startBuffer();
         // In-line caption editing if we have Horde_Perms::EDIT
         if ($option_edit) {
-            $GLOBALS['injector']->getInstance('Horde_Ajax_Imple')->getImple(array('ansel', 'EditCaption'), array(
+            $geometry = $image->getDimensions($thumbstyle);
+            $GLOBALS['injector']->createInstance('Horde_Ajax_Imple')->getImple(array('ansel', 'EditCaption'), array(
                 'domid' => $image->id . 'caption',
-                'id' => $image->id
+                'id' => $image->id,
+                'width' => $geometry['width']
             ));
         }
         include ANSEL_BASE . '/templates/tile/image.inc';
index d961f77..3d63608 100644 (file)
@@ -363,8 +363,9 @@ class Ansel_View_Image extends Ansel_View_Base
 
             /* In line caption editing */
             if ($this->gallery->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::EDIT)) {
+                $geometry = $this->resource->getDimensions();
                 $GLOBALS['injector']->getInstance('Horde_Ajax_Imple')->getImple(array('ansel', 'EditCaption'), array(
-                    'cols' => 120,
+                    'width' => $geometry['width'],
                     'domid' => "Caption",
                     'id' => $this->resource->id
                 ));
index 9d94b7a..d1b5a5c 100644 (file)
@@ -287,29 +287,6 @@ a.latest:link, a.latest:visited, a.latest:hover, a.latest:active {
     padding-left: 15px;
 }
 
-/* For in place editing */
-form.inplaceeditor-form {
-    background: none;
-}
-form.inplaceeditor-form input[type="submit"] {
-    background:#AAFFAA none repeat scroll 0%;
-    border:1px solid #000000;
-    cursor:pointer;
-    font-weight:bold;
-    padding:2px;
-}
-form.inplaceeditor-form a {
-    background:#FFAAAA none repeat scroll 0%;
-    border:1px solid #000000;
-    cursor:pointer;
-    font-weight:bold;
-    padding:2px;
-}
-.inplaceeditor-empty {
-    font-style: italic;
-    color: #999;
-}
-
 /* Image resizer/slider */
 #slider-track {
     background: url('graphics/scaler_slider_track.gif') no-repeat;
diff --git a/horde/js/inplaceeditor.js b/horde/js/inplaceeditor.js
new file mode 100644 (file)
index 0000000..ca9fdc9
--- /dev/null
@@ -0,0 +1,460 @@
+/**
+ * inplaceeditor.js - A javascript library which implements ajax inplace editing
+ * Requires prototype.js v1.6.0.2+ and scriptaculous v1.8.0+ (effects.js) if
+ * using the default callback functions.
+ *
+ * Adapted from script.aculo.us controls.js v1.8.3
+ * Copyright (c) 2005-2009 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+ *          (c) 2005-2009 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+ *          (c) 2005-2009 Jon Tirsen (http://www.tirsen.com)
+ *   Contributors:
+ *   Richard Livsey
+ *   Rahul Bhargava
+ *   Rob Wills
+ *
+ * The original script was freely distributable under the terms of an
+ * MIT-style license.
+ *
+ * Usage:
+ * ------
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (GPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/gpl.html.
+ */
+
+InPlaceEditor = Class.create(
+{
+    /**
+     * Constructor
+     */
+    initialize: function(element, url, options)
+    {
+        // Default Options/Callbacks
+        var defaults =
+        {
+            ajaxOptions: { },
+            autoRows: 3,                                // Use when multi-line w/ rows == 1
+            cancelControl: 'link',                      // 'link'|'button'|false
+            cancelText: 'cancel',
+            cancelClassName: 'button',
+            clickToEditText: 'Click to edit',
+            emptyClassName: 'inplaceeditor-empty',
+            externalControl: null,                      // id|elt
+            externalControlOnly: false,
+            fieldPostCreation: 'activate',              // 'activate'|'focus'|false
+            formClassName: 'inplaceeditor-form',
+            formId: null,                               // id|elt
+            highlightColor: '#ffff99',
+            highlightEndColor: '#ffffff',
+            hoverClassName: '',
+            htmlResponse: true,
+            loadingClassName: 'inplaceeditor-loading',
+            loadingText: 'Loading...',
+            okControl: 'button',                        // 'link'|'button'|false
+            okText: 'ok',
+            okClassName: 'button',
+            paramName: 'value',
+            rows: 1,                                    // If 1 and multi-line, uses autoRows
+            savingClassName: 'inplaceeditor-saving',
+            savingText: 'Saving...',
+            size: 0,
+            stripLoadedTextTags: false,
+            submitOnBlur: false,
+            width: null,
+
+            /** Default Callbacks **/
+            callback: function(form)
+            {
+              return Form.serialize(form);
+            },
+
+            onComplete: function(ipe)
+            {
+                new Effect.Highlight(element, { startcolor: this.options.highlightColor, keepBackgroundImage: true });
+            },
+
+            onEnterEditMode: Prototype.emptyFunction,
+
+            onEnterHover: function(ipe)
+            {
+                ipe.element.style.backgroundColor = ipe.options.highlightColor;
+                if (ipe._effect) {
+                    ipe._effect.cancel();
+                }
+            },
+
+            onFailure: function(transport, ipe) {
+                alert('Error communication with the server: ' + transport.responseText.stripTags());
+            },
+
+            /**
+             * Takes the IPE and its generated form, after editor, before controls.
+             */
+            onFormCustomization: Prototype.emptyFunction,
+
+            onLeaveEditMode: Prototype.emptyFunction,
+
+            onLeaveHover: function(ipe)
+            {
+                ipe._effect = new Effect.Highlight(ipe.element, {
+                    startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
+                    restorecolor: ipe._originalBackground, keepBackgroundImage: true
+                });
+            }
+        };
+
+        this.url = url;
+        this.element = element = $(element);
+        this._controls = { };
+        Object.extend(defaults, options || { });
+        this.options = defaults;
+        if (!this.options.formId && this.element.id) {
+            this.options.formId = this.element.id + '-inplaceeditor';
+            if ($(this.options.formId)) {
+                this.options.formId = '';
+            }
+        }
+        if (this.options.externalControl) {
+            this.options.externalControl = $(this.options.externalControl);
+        }
+        if (!this.options.externalControl) {
+            this.options.externalControlOnly = false;
+        }
+        this._originalBackground = this.element.getStyle('background-color') || 'transparent';
+        this.element.title = this.options.clickToEditText;
+        this._boundCancelHandler = this.handleFormCancellation.bind(this);
+        this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
+        this._boundFailureHandler = this.handleAJAXFailure.bind(this);
+        this._boundSubmitHandler = this.handleFormSubmission.bind(this);
+        this._boundWrapperHandler = this.wrapUp.bind(this);
+        this.registerListeners();
+        this.checkEmpty();
+    },
+
+    checkEmpty: function() {
+        if (this.element.innerHTML.length == 0) {
+            emptyNode = new Element('span', {className: this.options.emptyClassName}).update(this.options.emptyText);
+            this.element.appendChild(emptyNode);
+        }
+    },
+
+    keyHandler: function(e)
+    {
+        if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
+        if (e.keyCode == Event.KEY_ESC) {
+            this.handleFormCancellation(e);
+        } else if (e.keyCode == Event.KEY_RETURN) {
+            this.handleFormSubmission(e);
+        }
+    },
+
+    createControl: function(mode, handler, extraClasses)
+    {
+        var control = this.options[mode + 'Control'];
+        var text = this.options[mode + 'Text'];
+        if (control == 'button') {
+            var btn = new Element('input', { type: 'submit', value: text, className: this.options[mode + 'ClassName'] });
+            if (mode == 'cancel') {
+                btn.observe('click', this._boundCancelHandler);
+            }
+            this._form.appendChild(btn);
+            this._controls[mode] = btn;
+        } else if (control == 'link') {
+            var link = new Element('a', { href: '#', className:  this.options[mode + 'ClassName'] });
+            link.observe('click', 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler);
+            link.appendChild(document.createTextNode(text));
+            if (extraClasses) {
+                link.addClassName(extraClasses);
+            }
+            this._form.appendChild(link);
+            this._controls[mode] = link;
+        }
+    },
+
+    createEditField: function()
+    {
+        var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
+        var fld;
+        if (this.options.rows <= 1 && !/\r|\n/.test(this.getText())) {
+            fld = new Element('input', { type: 'text' });
+            var size = this.options.size || this.options.cols || 0;
+            if (size > 0) {
+                fld.size = size;
+            }
+        } else {
+            fld = new Element('textarea', { rows: (this.options.rows <= 1 ? this.options.autoRows : this.options.rows),
+                                            cols: this.options.cols || 40 });
+        }
+        fld.name = this.options.paramName;
+        fld.value = text; // No HTML breaks conversion anymore
+        fld.className = 'editor_field';
+        if (this.options.width) {
+            fld.setStyle({ width: this.options.width + 'px' });
+        }
+        if (this.options.submitOnBlur) {
+            fld.observe('blur', this._boundSubmitHandler);
+        }
+        this._controls.editor = fld;
+        if (this.options.loadTextURL) {
+            this.loadExternalText();
+        }
+        this._form.appendChild(this._controls.editor);
+    },
+
+    createForm: function()
+    {
+        var ipe = this;
+        function addText(mode, condition)
+        {
+            var text = ipe.options['text' + mode + 'Controls'];
+            if (!text || condition === false) return;
+            ipe._form.appendChild(text);
+        };
+
+        this._form = new Element('form', { id: this.options.formId, className: this.options.formClassName });
+        this._form.observe('submit', this._boundSubmitHandler);
+        this.createEditField();
+        if (this._controls.editor.tagName.toLowerCase() == 'textarea') {
+            this._form.appendChild(new Element('br'));
+        }
+        if (this.options.onFormCustomization) {
+            this.options.onFormCustomization(this, this._form);
+        }
+        addText('Before', this.options.okControl || this.options.cancelControl);
+        this.createControl('ok', this._boundSubmitHandler);
+        addText('Between', this.options.okControl && this.options.cancelControl);
+        this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
+        addText('After', this.options.okControl || this.options.cancelControl);
+    },
+
+    destroy: function()
+    {
+        if (this._oldInnerHTML) {
+            this.element.innerHTML = this._oldInnerHTML;
+        }
+        this.leaveEditMode();
+        this.unregisterListeners();
+    },
+
+    clickHandler: function(e)
+    {
+        if (this._saving || this._editing) {
+            return;
+        }
+        this._editing = true;
+        this.triggerCallback('onEnterEditMode');
+        if (this.options.externalControl) {
+            this.options.externalControl.hide();
+        }
+        this.element.hide();
+        this.createForm();
+        this.element.parentNode.insertBefore(this._form, this.element);
+        if (!this.options.loadTextURL) {
+            this.postProcessEditField();
+        }
+        if (e) {
+            Event.stop(e);
+        }
+    },
+
+    mouseoverHandler: function(e)
+    {
+        if (this.options.hoverClassName) {
+            this.element.addClassName(this.options.hoverClassName);
+        }
+        if (this._saving) {
+            return;
+        }
+        this.triggerCallback('onEnterHover');
+    },
+
+    getText: function()
+    {   
+        $(this.element).select('.' + this.options.emptyClassName).each(function(child) {
+            this.element.removeChild(child);
+        }.bind(this));
+
+        return this.element.innerHTML.unescapeHTML();
+    },
+
+    handleAJAXFailure: function(transport)
+    {
+        this.triggerCallback('onFailure', transport);
+        if (this._oldInnerHTML) {
+            this.element.innerHTML = this._oldInnerHTML;
+            this._oldInnerHTML = null;
+        }
+    },
+
+    handleFormCancellation: function(e)
+    {
+        this.wrapUp();
+        if (e) {
+            Event.stop(e);
+        }
+    },
+
+    handleFormSubmission: function(e)
+    {
+        var form = this._form;
+        var value = $F(this._controls.editor);
+        this.prepareSubmission();
+        var params = this.options.callback(form, value) || '';
+        if (Object.isString(params)) {
+            params = params.toQueryParams();
+        }
+        params.editorId = this.element.id;
+        if (this.options.htmlResponse) {
+            var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
+            Object.extend(options, {
+                parameters: params,
+                onComplete: this._boundWrapperHandler,
+                onFailure: this._boundFailureHandler
+            });
+            new Ajax.Updater({ success: this.element }, this.url, options);
+        } else {
+            var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+            Object.extend(options, {
+                parameters: params,
+                onComplete: this._boundWrapperHandler,
+                onFailure: this._boundFailureHandler
+            });
+            new Ajax.Request(this.url, options);
+        }
+        if (e) {
+            Event.stop(e);
+        }
+    },
+
+    leaveEditMode: function()
+    {
+        this.element.removeClassName(this.options.savingClassName);
+        this.removeForm();
+        this.mouseoutHandler();
+        this.element.style.backgroundColor = this._originalBackground;
+        this.element.show();
+        if (this.options.externalControl) {
+            this.options.externalControl.show();
+        }
+        this._saving = false;
+        this._editing = false;
+        this._oldInnerHTML = null;
+        this.triggerCallback('onLeaveEditMode');
+    },
+
+    mouseoutHandler: function(e)
+    {
+        if (this.options.hoverClassName) {
+            this.element.removeClassName(this.options.hoverClassName);
+        }
+        if (this._saving) {
+            return;
+        }
+        this.triggerCallback('onLeaveHover');
+    },
+
+    loadExternalText: function()
+    {
+        this._form.addClassName(this.options.loadingClassName);
+        this._controls.editor.disabled = true;
+        var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
+        Object.extend(options, {
+            parameters: 'editorId=' + encodeURIComponent(this.element.id),
+            onComplete: Prototype.emptyFunction,
+            onSuccess: function(transport) {
+                this._form.removeClassName(this.options.loadingClassName);
+                var text = transport.responseText;
+                if (this.options.stripLoadedTextTags) {
+                    text = text.stripTags();
+                }
+                this._controls.editor.value = text;
+                this._controls.editor.disabled = false;
+                this.postProcessEditField();
+            }.bind(this),
+            onFailure: this._boundFailureHandler
+        });
+        new Ajax.Request(this.options.loadTextURL, options);
+    },
+
+    postProcessEditField: function()
+    {
+        var fpc = this.options.fieldPostCreation;
+        if (fpc) {
+            $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
+        }
+    },
+
+    prepareSubmission: function()
+    {
+        this._saving = true;
+        this.removeForm();
+        this.mouseoutHandler();
+        this.showSaving();
+    },
+
+    registerListeners: function()
+    {
+        var listeners = {
+            click: 'clickHandler',
+            keydown: 'keyHandler',
+            mouseover: 'mouseoverHandler',
+            mouseout: 'mouseoutHandler'
+        };
+
+        this._listeners = { };
+        var listener;
+        $H(listeners).each(function(pair) {
+            listener = this[pair.value].bind(this);
+            this._listeners[pair.key] = listener;
+            if (!this.options.externalControlOnly) {
+                this.element.observe(pair.key, listener);
+            }
+            if (this.options.externalControl) {
+                this.options.externalControl.observe(pair.key, listener);
+            }
+        }.bind(this));
+    },
+
+    removeForm: function()
+    {
+        if (!this._form) return;
+        this._form.remove();
+        this._form = null;
+        this._controls = { };
+    },
+
+    showSaving: function()
+    {
+        this._oldInnerHTML = this.element.innerHTML;
+        this.element.innerHTML = this.options.savingText;
+        this.element.addClassName(this.options.savingClassName);
+        this.element.style.backgroundColor = this._originalBackground;
+        this.element.show();
+    },
+
+    triggerCallback: function(cbName, arg)
+    {
+        if ('function' == typeof this.options[cbName]) {
+            this.options[cbName](this, arg);
+        }
+    },
+
+    unregisterListeners: function()
+    {
+        $H(this._listeners).each(function(pair) {
+            if (!this.options.externalControlOnly) {
+                this.element.stopObserving(pair.key, pair.value);
+            }
+            if (this.options.externalControl) {
+                this.options.externalControl.stopObserving(pair.key, pair.value);
+            }
+        }.bind(this));
+    },
+
+    wrapUp: function(transport) {
+        this.leaveEditMode();
+        this.triggerCallback('onComplete', null);
+    }
+});
\ No newline at end of file
index 55fd2a3..5c603b0 100644 (file)
@@ -1206,6 +1206,15 @@ div.GrowlerNoticeExit:hover {
     color: gray;
 }
 
+/* For in place editing */
+form.inplaceeditor-form {
+    background: none;
+}
+.inplaceeditor-empty {
+    font-style: italic;
+    color: #999;
+}
+
 /* Print CSS. */
 @media print {
     body, .header, .smallheader {