Commits

Bogdan Savluk committed eb5d26b

Begin using jquery.iframe.transport to make ajax file upload in older browsers. Finish work on GalleryBehavior. Remove form tags from widget to make it possible to add it into anouther form tag.

Comments (0)

Files changed (7)

GalleryBehavior.php

     public $name;
     /** @var boolean does images in gallery need descriptions */
     public $description;
+    private $_gallery;
 
     /** Will create new gallery after save if no associated gallery exists */
     public function afterSave($event)
     /** @return Gallery Returns gallery associated with model */
     public function getGallery()
     {
-        return Gallery::model()->findByPk($this->getOwner()->{$this->idAttribute});
+        if (empty($this->_gallery)) {
+            $this->_gallery = Gallery::model()->findByPk($this->getOwner()->{$this->idAttribute});
+        }
+        return $this->_gallery;
     }
 
     /** @return GalleryPhoto[] Photos from associated gallery */

GalleryController.php

     }
 
     /**
-     * Upload single file thought form with name and description.
-     * On success redirects to returnUrl passed via post.
-     * @param int $gallery_id Gallery Id to upload image
-     * @throws CHttpException
-     */
-    public function actionUpload($gallery_id = null)
-    {
-        if (Yii::app()->getRequest()->getIsPostRequest()) {
-
-            $model = new GalleryPhoto();
-            $model->gallery_id = $gallery_id;
-            if (isset($_POST['GalleryPhoto']))
-                $model->attributes = $_POST['GalleryPhoto'];
-
-            $imageFile = CUploadedFile::getInstance($model, 'image');
-            $model->file_name = $imageFile->getName();
-            $model->save();
-
-            $model->setImage($imageFile->getTempName());
-
-            $this->redirect($_POST['returnUrl']);
-        } else
-            throw new CHttpException(403);
-    }
-
-    /**
      * Method to handle file upload thought XHR2
      * On success returns JSON object with image info.
      * @param $gallery_id string Gallery Id to upload images
             $model->save();
 
             $model->setImage($imageFile->getTempName());
-
+            header("Content-Type: application/json");
             echo CJSON::encode(
                 array(
                     'id' => $model->id,

GalleryManager.php

     public $gallery;
     /** @var string Route to gallery controller */
     public $controllerRoute = false;
+    public $assets;
+
+    public function init()
+    {
+        $this->assets = Yii::app()->getAssetManager()->publish(dirname(__FILE__) . '/assets');
+    }
 
     /** Render widget */
     public function run()
     {
+
+        $cs = Yii::app()->clientScript;
+        $cs->registerCoreScript('jquery');
+        $cs->registerScriptFile($this->assets . '/jquery.iframe-transport.min.js');
+
         if ($this->controllerRoute === null)
             throw new CException('$controllerRoute must be set.', 500);
         $model = new GalleryPhoto();

assets/jquery.iframe-transport.js

+// This [jQuery](http://jquery.com/) plugin implements an `<iframe>`
+// [transport](http://api.jquery.com/extending-ajax/#Transports) so that
+// `$.ajax()` calls support the uploading of files using standard HTML file
+// input fields. This is done by switching the exchange from `XMLHttpRequest`
+// to a hidden `iframe` element containing a form that is submitted.
+
+// The [source for the plugin](http://github.com/cmlenz/jquery-iframe-transport)
+// is available on [Github](http://github.com/) and dual licensed under the MIT
+// or GPL Version 2 licenses.
+
+// ## Usage
+
+// To use this plugin, you simply add an `iframe` option with the value `true`
+// to the Ajax settings an `$.ajax()` call, and specify the file fields to
+// include in the submssion using the `files` option, which can be a selector,
+// jQuery object, or a list of DOM elements containing one or more
+// `<input type="file">` elements:
+
+//     $("#myform").submit(function() {
+//         $.ajax(this.action, {
+//             files: $(":file", this),
+//             iframe: true
+//         }).complete(function(data) {
+//             console.log(data);
+//         });
+//     });
+
+// The plugin will construct hidden `<iframe>` and `<form>` elements, add the
+// file field(s) to that form, submit the form, and process the response.
+
+// If you want to include other form fields in the form submission, include
+// them in the `data` option, and set the `processData` option to `false`:
+
+//     $("#myform").submit(function() {
+//         $.ajax(this.action, {
+//             data: $(":text", this).serializeArray(),
+//             files: $(":file", this),
+//             iframe: true,
+//             processData: false
+//         }).complete(function(data) {
+//             console.log(data);
+//         });
+//     });
+
+// ### Response Data Types
+
+// As the transport does not have access to the HTTP headers of the server
+// response, it is not as simple to make use of the automatic content type
+// detection provided by jQuery as with regular XHR. If you can't set the
+// expected response data type (for example because it may vary depending on
+// the outcome of processing by the server), you will need to employ a
+// workaround on the server side: Send back an HTML document containing just a
+// `<textarea>` element with a `data-type` attribute that specifies the MIME
+// type, and put the actual payload in the textarea:
+
+//     <textarea data-type="application/json">
+//       {"ok": true, "message": "Thanks so much"}
+//     </textarea>
+
+// The iframe transport plugin will detect this and pass the value of the
+// `data-type` attribute on to jQuery as if it was the "Content-Type" response
+// header, thereby enabling the same kind of conversions that jQuery applies
+// to regular responses. For the example above you should get a Javascript
+// object as the `data` parameter of the `complete` callback, with the
+// properties `ok: true` and `message: "Thanks so much"`.
+
+// ### Handling Server Errors
+
+// Another problem with using an `iframe` for file uploads is that it is
+// impossible for the javascript code to determine the HTTP status code of the
+// servers response. Effectively, all of the calls you make will look like they
+// are getting successful responses, and thus invoke the `done()` or
+// `complete()` callbacks. You can only determine communicate problems using
+// the content of the response payload. For example, consider using a JSON
+// response such as the following to indicate a problem with an uploaded file:
+
+//     <textarea data-type="application/json">
+//       {"ok": false, "message": "Please only upload reasonably sized files."}
+//     </textarea>
+
+// ### Compatibility
+
+// This plugin has primarily been tested on Safari 5 (or later), Firefox 4 (or
+// later), and Internet Explorer (all the way back to version 6). While I
+// haven't found any issues with it so far, I'm fairly sure it still doesn't
+// work around all the quirks in all different browsers. But the code is still
+// pretty simple overall, so you should be able to fix it and contribute a
+// patch :)
+
+// ## Annotated Source
+
+(function($, undefined) {
+    "use strict";
+
+    // Register a prefilter that checks whether the `iframe` option is set, and
+    // switches to the "iframe" data type if it is `true`.
+    $.ajaxPrefilter(function(options, origOptions, jqXHR) {
+        if (options.iframe) {
+            return "iframe";
+        }
+    });
+
+    // Register a transport for the "iframe" data type. It will only activate
+    // when the "files" option has been set to a non-empty list of enabled file
+    // inputs.
+    $.ajaxTransport("iframe", function(options, origOptions, jqXHR) {
+        var form = null,
+            iframe = null,
+            name = "iframe-" + $.now(),
+            files = $(options.files).filter(":file:enabled"),
+            markers = null;
+
+        // This function gets called after a successful submission or an abortion
+        // and should revert all changes made to the page to enable the
+        // submission via this transport.
+        function cleanUp() {
+            markers.replaceWith(function(idx) {
+                return files.get(idx);
+            });
+            form.remove();
+            iframe.attr("src", "javascript:false;").remove();
+        }
+
+        // Remove "iframe" from the data types list so that further processing is
+        // based on the content type returned by the server, without attempting an
+        // (unsupported) conversion from "iframe" to the actual type.
+        options.dataTypes.shift();
+
+        if (files.length) {
+            form = $("<form enctype='multipart/form-data' method='post'></form>").
+                hide().attr({action: options.url, target: name});
+
+            // If there is any additional data specified via the `data` option,
+            // we add it as hidden fields to the form. This (currently) requires
+            // the `processData` option to be set to false so that the data doesn't
+            // get serialized to a string.
+            if (typeof(options.data) === "string" && options.data.length > 0) {
+                $.error("data must not be serialized");
+            }
+            $.each(options.data || {}, function(name, value) {
+                if ($.isPlainObject(value)) {
+                    name = value.name;
+                    value = value.value;
+                }
+                $("<input type='hidden' />").attr({name:  name, value: value}).
+                    appendTo(form);
+            });
+
+            // Add a hidden `X-Requested-With` field with the value `IFrame` to the
+            // field, to help server-side code to determine that the upload happened
+            // through this transport.
+            $("<input type='hidden' value='IFrame' name='X-Requested-With' />").
+                appendTo(form);
+
+            // Move the file fields into the hidden form, but first remember their
+            // original locations in the document by replacing them with disabled
+            // clones. This should also avoid introducing unwanted changes to the
+            // page layout during submission.
+            markers = files.after(function(idx) {
+                return $(this).clone().prop("disabled", true);
+            }).next();
+            files.appendTo(form);
+
+            return {
+
+                // The `send` function is called by jQuery when the request should be
+                // sent.
+                send: function(headers, completeCallback) {
+                    iframe = $("<iframe src='javascript:false;' name='" + name +
+                        "' id='" + name + "' style='display:none'></iframe>");
+
+                    // The first load event gets fired after the iframe has been injected
+                    // into the DOM, and is used to prepare the actual submission.
+                    iframe.bind("load", function() {
+
+                        // The second load event gets fired when the response to the form
+                        // submission is received. The implementation detects whether the
+                        // actual payload is embedded in a `<textarea>` element, and
+                        // prepares the required conversions to be made in that case.
+                        iframe.unbind("load").bind("load", function() {
+                            var doc = this.contentWindow ? this.contentWindow.document :
+                                    (this.contentDocument ? this.contentDocument : this.document),
+                                root = doc.documentElement ? doc.documentElement : doc.body,
+                                textarea = root.getElementsByTagName("textarea")[0],
+                                type = textarea ? textarea.getAttribute("data-type") : null,
+                                status = textarea ? textarea.getAttribute("data-status") : 200,
+                                statusText = textarea ? textarea.getAttribute("data-statusText") : "OK",
+                                content = {
+                                    html: root.innerHTML,
+                                    text: type ?
+                                        textarea.value :
+                                        root ? (root.textContent || root.innerText) : null
+                                };
+                            cleanUp();
+                            completeCallback(status, statusText, content, type ?
+                                ("Content-Type: " + type) :
+                                null);
+                        });
+
+                        // Now that the load handler has been set up, submit the form.
+                        form[0].submit();
+                    });
+
+                    // After everything has been set up correctly, the form and iframe
+                    // get injected into the DOM so that the submission can be
+                    // initiated.
+                    $("body").append(form, iframe);
+                },
+
+                // The `abort` function is called by jQuery when the request should be
+                // aborted.
+                abort: function() {
+                    if (iframe !== null) {
+                        iframe.unbind("load").attr("src", "javascript:false;");
+                        cleanUp();
+                    }
+                }
+
+            };
+        }
+    });
+
+})(jQuery);

assets/jquery.iframe-transport.min.js

+(function(a,b){"use strict",a.ajaxPrefilter(function(a,b,c){if(a.iframe)return"iframe"}),a.ajaxTransport("iframe",function(b,c,d){function j(){i.replaceWith(function(a){return h.get(a)}),e.remove(),f.attr("src","javascript:false;").remove()}var e=null,f=null,g="iframe-"+a.now(),h=a(b.files).filter(":file:enabled"),i=null;b.dataTypes.shift();if(h.length)return e=a("<form enctype='multipart/form-data' method='post'></form>").hide().attr({action:b.url,target:g}),typeof b.data=="string"&&b.data.length>0&&a.error("data must not be serialized"),a.each(b.data||{},function(b,c){a.isPlainObject(c)&&(b=c.name,c=c.value),a("<input type='hidden' />").attr({name:b,value:c}).appendTo(e)}),a("<input type='hidden' value='IFrame' name='X-Requested-With' />").appendTo(e),i=h.after(function(b){return a(this).clone().prop("disabled",!0)}).next(),h.appendTo(e),{send:function(b,c){f=a("<iframe src='javascript:false;' name='"+g+"' id='"+g+"' style='display:none'></iframe>"),f.bind("load",function(){f.unbind("load").bind("load",function(){var a=this.contentWindow?this.contentWindow.document:this.contentDocument?this.contentDocument:this.document,b=a.documentElement?a.documentElement:a.body,d=b.getElementsByTagName("textarea")[0],e=d?d.getAttribute("data-type"):null,f=d?d.getAttribute("data-status"):200,g=d?d.getAttribute("data-statusText"):"OK",h={html:b.innerHTML,text:e?d.value:b?b.textContent||b.innerText:null};j(),c(f,g,h,e?"Content-Type: "+e:null)}),e[0].submit()}),a("body").append(e,f)},abort:function(){f!==null&&(f.unbind("load").attr("src","javascript:false;"),j())}}})})(jQuery);
 ----------------------
 Using gallery behavior is possible to add gallery to any model in application.
 
-GalleryBehavior is under development and will be finished soon, so usage examples also will be later.
+To use GalleryBehavior:
+
+1. Add it to your model:
+
+        Example:
+        public function behaviors()
+        {
+            return array(
+                'galleryBehavior' => array(
+                    'class' => 'GalleryBehavior',
+                    'idAttribute' => 'gallery_id',
+                    'versions' => array(
+                        'small' => array(
+                            'centeredpreview' => array(98, 98),
+                        ),
+                        'medium' => array(
+                            'resize' => array(800, null),
+                        )
+                    ),
+                    'name' => true,
+                    'description' => true,
+                )
+            );
+        }
+
+2. Add gallery widget to your view:
+
+        Example:
+        <h2>Product galley</h2>
+        <?php
+        if ($model->galleryBehavior->getGallery() === null) {
+            echo '<p>Before add photos to product gallery, you need to save product</p>';
+        } else {
+            $this->widget('GalleryManager', array(
+                'gallery' => $model->galleryBehavior->getGallery(),
+            ));
+        }
+        ?>

views/galleryManager.php

 ?>
 <div class="GalleryEditor<?php echo $cls?>" id="<?php echo $this->id?>">
     <div class="gform">
-        <?php
-        $form = $this->getController()->beginWidget('CActiveForm',
-            array(
-                'action' => Yii::app()->createUrl($this->controllerRoute . '/upload', array('gallery_id' => $this->gallery->id)),
-                'method' => 'post',
-                'htmlOptions' => array('enctype' => 'multipart/form-data'),
-            ));
-        /** @var CActiveForm $form */
-        ?>
         <span class="btn btn-success fileinput-button">
-                <i class="icon-plus icon-white"></i><span><?php echo Yii::t('galleryManager.main', 'Add images…');?></span>
-            <?php echo $form->fileField($model, 'image', array('class' => 'afile', 'accept' => "image/*", 'multiple' => 'true'));?>
-            </span>
+            <i class="icon-plus icon-white"></i>
+            <?php echo Yii::t('galleryManager.main', 'Add images…');?>
+            <?php echo CHtml::activeFileField($model, 'image', array('class' => 'afile', 'accept' => "image/*", 'multiple' => 'true'));?>
+        </span>
 
         <span class="btn disabled edit_selected"><?php echo Yii::t('galleryManager.main', 'Edit selected');?></span>
         <span class="btn disabled remove_selected"><?php echo Yii::t('galleryManager.main', 'Remove selected');?></span>
 
         <label for="select_all_<?php echo $this->id?>" class="btn">
-            <input type="checkbox"
+            <input type="checkbox" style="margin-bottom: 4px;"
                    id="select_all_<?php echo $this->id?>"
-                   class="select_all"/> <?php echo Yii::t('galleryManager.main', 'Select all');?>
+                   class="select_all"/>
+            <?php echo Yii::t('galleryManager.main', 'Select all');?>
         </label>
 
         <!--  progress bar-->
         </div>-->
         <?php
         echo CHtml::hiddenField('returnUrl', Yii::app()->getRequest()->getUrl() . '#' . $this->id);
-        $this->getController()->endWidget();
         ?>
     </div>
     <hr/>
-    <form
-        method="post"
-        action="<?php echo Yii::app()->createUrl($this->controllerRoute . '/order')?>"
-        class="sorter"
-        >
+    <div class="sorter">
         <div class="images">
             <?php foreach ($this->gallery->galleryPhotos as $photo): ?>
             <div id="<?php echo $this->id . '-' . $photo->id ?>" class="photo">
                     echo ' <span data-photo-id="' . $photo->id . '" class="deletePhoto btn btn-danger"><i class="icon-remove icon-white"></i></span>';
                     ?>
                 </div>
-                <input type="checkbox" class="photo-select"/>
+                <label>
+                    <input type="checkbox" class="photo-select"/>
+                </label>
             </div>
             <?php endforeach;?>
         </div>
-        <?php echo CHtml::hiddenField('returnUrl', Yii::app()->getRequest()->getUrl() . '#' . $this->id); ?>
-
         <br style="clear: both;"/>
-    </form>
+    </div>
 
     <div class="modal hide editor-modal"> <!-- fade removed because of opera -->
         <div class="modal-header">
             <h3><?php echo Yii::t('galleryManager.main', 'Edit information')?></h3>
         </div>
         <div class="modal-body">
-            <form action="<?php echo Yii::app()->createUrl($this->controllerRoute . '/changeData')?>"></form>
+            <div class="form"></div>
         </div>
         <div class="modal-footer">
-            <a href="#"
-               class="btn btn-primary save-changes"><?php echo Yii::t('galleryManager.main', 'Save changes')?></a>
+            <a href="#" class="btn btn-primary save-changes">
+                <?php echo Yii::t('galleryManager.main', 'Save changes')?>
+            </a>
             <a href="#" class="btn" data-dismiss="modal"><?php echo Yii::t('galleryManager.main', 'Close')?></a>
         </div>
     </div>
 
 $cs->registerCss($this->id . 'css', $css);
 ?>
+
 <script type="text/javascript">
 $(function () {
     // variables from php
     var hasDesc = <?php echo $this->gallery->description ? 'true' : 'false' ?>;
 
     var wId = '<?php echo $this->id?>';
-    var ajaxUploadUrl = '<?php echo  Yii::app()->createUrl($this->controllerRoute . '/ajaxUpload', array('gallery_id' => $this->gallery->id))?>';
-    var deleteUrl = '<?php echo  Yii::app()->createUrl($this->controllerRoute . '/delete')?>';
+    var ajaxUploadUrl = <?php echo CJavaScript::encode(Yii::app()->createUrl($this->controllerRoute . '/ajaxUpload', array('gallery_id' => $this->gallery->id)))?>;
+    var deleteUrl = <?php echo CJavaScript::encode(Yii::app()->createUrl($this->controllerRoute . '/delete'))?>;
+    var editorActionUrl = <?php echo CJavaScript::encode(Yii::app()->createUrl($this->controllerRoute . '/changeData'))?>;
+    var sorterActionUrl = <?php echo CJavaScript::encode(Yii::app()->createUrl($this->controllerRoute . '/order'));?>;
+
 
     var $gallery = $('#' + wId);
     var $sorter = $('.sorter', $gallery);
     var $images = $('.images', $sorter);
     var $editorModal = $('.editor-modal');
-    var $editorForm = $('form', $editorModal);
+    var $editorForm = $('.form', $editorModal);
+
 
     function photoEditorTemplate(id, src, name, description) {
         return '<div class="photo-editor">' +
     bindPhotoEvents($('.photo'));
 
     $('.images', $sorter).sortable().disableSelection().bind("sortstop", function (event, ui) {
-        $.post($sorter.attr('action'), $sorter.serialize() + '&ajax=true', function (data, textStatus, jqXHR) {
+        $.post(sorterActionUrl, $('input', $sorter).serialize() + '&ajax=true', function (data, textStatus, jqXHR) {
             // order saved!
         }, 'json');
     });
         });
     } else {
         $('.afile', $gallery).on('change', function (e) {
-            this.form.submit();
+            //this.form.submit(); // todo: iframe xhr
+            e.preventDefault();
+            $editorForm.html('');
+
+            $.ajax(
+                ajaxUploadUrl, {
+                    files:$(this),
+                    iframe:true,
+                    dataType:"json"
+                }).done(function (resp) {
+                    var newOne = $(photoTemplate(resp.id, resp.preview, resp.name, resp.description, resp.rank));
+                    bindPhotoEvents(newOne);
+                    $images.append(newOne);
+                    if (hasName || hasDesc)
+                        $editorForm.append($(photoEditorTemplate(resp.id, resp.preview, resp.name, resp.description)));
+
+                    if (hasName || hasDesc) $editorModal.modal('show');
+                });
+
+
         });
     }
 
-    $('.save-changes', $editorModal).click(function () {
-        $.post($editorForm.attr('action'), $editorForm.serialize() + '&ajax=true', function (data, textStatus, jqXHR) {
+    $('.save-changes', $editorModal).click(function (e) {
+        e.preventDefault();
+        $.post(editorActionUrl, $('input,textarea', $editorForm).serialize() + '&ajax=true', function (data, textStatus, jqXHR) {
 
             var count = data.length;
             for (var key = 0; key < count; key++) {
         return false;
     });
 
-    $('.remove_selected', $gallery).click(function () {
+    $('.remove_selected', $gallery).click(function (e) {
+        e.preventDefault();
         $('.photo.selected', $sorter).each(function () {
             var id = $(this).attr('id').substr((wId + '-').length);
             $.ajax({
             }).removeClass('selected');
         }
         updateButtons();
-    }).parent().click(function (e) { //label event
-            if ($(e.target).attr('id').substr(10) !== 'select_all' &&
-                $(e.target).attr('for').substr(10) !== 'select_all')
-                $('.select_all', $gallery).prop('checked', !$('.select_all', $gallery).prop('checked')).change();
-        });
+    });
 
 })
 </script>