Инструменты пользователя

Инструменты сайта


eaze:samples:добавление_фотогалереи_в_vt_к_объекту

Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

Both sides previous revision Предыдущая версия
Следущая версия
Предыдущая версия
Последняя версия Both sides next revision
eaze:samples:добавление_фотогалереи_в_vt_к_объекту [2011/09/20 20:13]
sergeyfast
eaze:samples:добавление_фотогалереи_в_vt_к_объекту [2011/09/21 11:26]
sergeyfast
Строка 92: Строка 92:
 </​script>​ </​script>​
 </​code>​ </​code>​
 +
 +Создадим шаблон ''​albumTemplate'',​ который будет инициализироваться строчкой (entity-photos.js)
 +<code javascript>​$("#​albumTemplate"​).tmpl(data,​ { counter: albumCounter,​ photoCounter:​ photoCounter } ).appendTo("​.albums"​);</​code>​
 +  * ''​data''​ - наш источник данных.
 +  * ''​albumCounter''​ - объект,​ с помощью которого мы будем держать текущий индекс альбома (''​photoCounter''​ - аналогично с фото). Данный объект передается в tmpl() в качестве параметра для рендеринга (доступен в $item в шаблоне).
 +<code javascript>​
 +    /**
 +     * Counters for album index
 +     */
 +    var Counter = function() {
 +        this.index = -1;
 +    }
 +
 +    Counter.prototype.nextIndex = function() {
 +        this.index ++;
 +        return this.index;
 +    }
 +
 +    var albumCounter = new Counter();
 +    var photoCounter = new Counter();
 +</​code>​
 +
 +Опишем шаблон для создания строчки с альбомом.
 +<code html>
 +<script id="​albumTemplate"​ type="​text/​x-jquery-tmpl">​
 +    <div class="​row album sort" id="​album-${ $item.counter.nextIndex() }">
 +        <input type="​hidden"​ name="​{$prefix}[albums][${ $item.counter.index }][entityAlbumId]"​ value="​${id}"​ />
 +        <br />
 +        <table class="​objects"​ style="​width:​auto;">​
 +            <​tbody>​
 +            <tr>
 +                <td class="​handle">​Альбом</​td>​
 +                <td class="​left"><​input type="​text"​ name="​{$prefix}[albums][${ $item.counter.index }][title]"​ value="​${title}"​ /></​td>​
 +                <td colspan="​2"​ class="​left"><​input type="​text"​ name="​{$prefix}[albums][${ $item.counter.index }][description]"​ value="​${description}"​ size="​60"​ style="​width:​ auto;" /></​td>​
 +                <td width="​10%">​
 +                    <ul class="​actions">​
 +                        <li class="​edit"><​a href="#"​ class="​add-photo"​ data-album-id="​${ $item.counter.index }">​Добавить</​a></​li>​
 +                        <li class="​delete"><​a href="#"​ class="​delete-album"​ title="​Удалить"​ data-album-id="​${ $item.counter.index }">​Удалить</​a></​li>​
 +                    </ul>
 +                </td>
 +            </tr>
 +            {{each(i, photo) photos}}
 +                {{tmpl( photo , { counter: $item.photoCounter,​ albumIndex: $item.counter.index }) "#​photoTemplate"​}}
 +            {{/each}}
 +            </​tbody>​
 +        </​table>​
 +    </​div>​
 +</​script><​script>''</​script>​
 +</​code>​
 +
 +  * ''​${ $item.counter.nextIndex() }''​ - пробелы между {} нужны для того, чтобы шаблонизатор в Eaze не подумал,​ что это переменная ''​{$item}''​.
 +  * ''</​script><​script><​nowiki>''</​nowiki></​script>''​ - [[http://​youtrack.jetbrains.net/​issue/​IDEA-73686|workaround]] для IDE.
 +  * ''​${id}''​ - получение свойства id из элемента массива data.
 +  * ''<​nowiki>​{{each(i,​ photo) photos}}</​nowiki>''​ - цикл в шаблоне для отображения фотографий. ''​photos''​ - свойство из элемента массива data.
 +  * ''<​nowiki>​{{tmpl( photo , { counter: $item.photoCounter,​ albumIndex: $item.counter.index }) "#​photoTemplate"​}}</​nowiki>''​ - вызов шаблона photoTemplate,​ передача дополнительных параметров для рендеринга.
 +  * ''​{$prefix}''​ - стандартная переменная для data.tmpl.php (префикс объекта).
 +
 +Шаблон для отображения конкретной фотографии
 +<code html>
 +<script id="​photoTemplate"​ type="​text/​x-jquery-tmpl">​
 +    <tr class="​sort">​
 +        <​td><​input type="​hidden"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.nextIndex() }][entityPhotoId]"​ value="​${id}"​ /></​td>​
 +        <​td><​input type="​text"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][title]"​ value="​${title}"​ class="​title-${ $item.albumIndex }-${num}"​ size="​60"​ style="​width:​ auto;" /></​td>​
 +        <td class="​left"><​input type="​hidden"​ class="​vfsFile"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][smallImageId]"​ rel="​smallImageId-${ $item.albumIndex }-${num}"​ id="​photo-small-${ $item.counter.index }" vfs:​previewType="​image" ​ {{if smallImage}}value="​${ smallImage.id}"​ vfs:​src="​{web:​vfs://​}${ smallImage.src}"​ vfs:​name="​${ smallImage.name}"​{{/​if}}/></​td>​
 +        <td class="​left"><​input type="​hidden"​ class="​vfsFile"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][bigImageId]"​ rel="​bigImageId-${ $item.albumIndex }-${num}"​ id="​photo-big-${ $item.counter.index }" vfs:​previewType="​image"​ {{if bigImage}}value="​${ bigImage.id}"​ vfs:​src="​{web:​vfs://​}${ bigImage.src}"​ vfs:​name="​${ bigImage.name}"​{{/​if}}/></​td>​
 +        <td width="​10%">​
 +            <ul class="​actions">​
 +                <li class="​delete"><​a class="​delete-photo"​ href="#"​ title="​Удалить">​Удалить</​a></​li>​
 +            </ul>
 +        </td>
 +    </tr>
 +</​script><​script>''</​script>​
 +</​code>​
 +  * ''​${num}''​ - это дополнительное свойство (индекс массива),​ который используется для поиска элемента при отображении ошибок (''​$item.counter.nextIndex()''​ имеет сквозную нумерацию,​ а ''​${num}''​ начинается с 0 для каждого альбома).
 +
 +На этом работа с photos.tmpl.php завершена.
 +
 +===== entity-photos.js =====
 +//​Данный файл не претендует на идеальность :)//
 +
 +Начинается данный файл с описания счетчиков для альбомов и фотографий (см. код выше).
 +
 +После этого опишем поведение кнопок добавления и удаления альбомов и фотографий
 +<code javascript>​
 +    /**
 +     * Album Controls
 +     */
 +    $(function() {
 +        $("#​add-album"​).live('​click',​ function(e) {
 +            $("#​albumTemplate"​).tmpl(null,​ { counter: albumCounter } ).appendTo("​.albums"​);​
 +            rebindAlbumControls();​
 +            e.preventDefault();​
 +        });
 +
 +        $("​.add-photo"​).live('​click',​ function(e) {
 +            var id = $(this).data("​albumId"​);​
 +            $("#​photoTemplate"​).tmpl( null, { counter: photoCounter,​ albumIndex: id } ).appendTo("#​album-"​ + id + " .objects"​);​
 +            vfsSelector.Init();​
 +            e.preventDefault();​
 +        });
 +
 +        $("​.delete-photo"​).live('​click',​ function(e) {
 +            $(this).parent().parent().parent().parent().remove();​
 +            e.preventDefault();​
 +        });
 +
 +        $("​.delete-album"​).live('​click',​ function(e) {
 +            var id = $(this).data("​albumId"​);​
 +            $("#​album-"​ + id).remove();​
 +            e.preventDefault();​
 +        });
 +    });
 +</​code>​
 +
 +''​rebindAlbumControls()''​ необходимо для переинициализации vfs и сортировок
 +<code javascript>​
 +    function rebindAlbumControls() {
 +        vfsSelector.Init();​
 +
 +        $('​.objects'​).sortable({
 +            '​items':​ '​tr.sort'​
 +            , '​forceHelperSize':​ true
 +            , '​forcePlaceholderSize':​ true
 +            , '​handle':​ '​td:​first-child'​
 +        });
 +
 +        $('​.albums'​).sortable({
 +            '​items':​ '​.album'​
 +            , '​forceHelperSize':​ true
 +            , '​forcePlaceholderSize':​ true
 +            , '​handle':​ '​.handle'​
 +        });
 +    }
 +</​code>​
 +
 +При загрузки страницы запустим рендеринг
 +<code javascript>​
 +    $(function() {
 +        $("#​albumTemplate"​).tmpl(data,​ { counter: albumCounter,​ photoCounter:​ photoCounter } ).appendTo("​.albums"​);​
 +        rebindAlbumControls();​
 +        displayAlbumErrors();​
 +    });
 +</​code>​
 +
 +И отобразим ошибки
 +<code javascript>​
 +    function displayAlbumErrors() {
 +        if ( typeof( albumErrors ) == '​undefined'​ ) {
 +            return;
 +        }
 +
 +        $.each( albumErrors,​ function( albumIndex, album ) {
 +            $.each( album, function( albumField, albumData ) {
 +                if ( albumField == '​photos'​ ) {
 +                    $.each( albumData, function(photoIndex,​ photoField ){
 +                        $.each( photoField, function( fieldName, fieldError ) {
 +                            $( '​[rel='​ + fieldName + '​-'​ + albumIndex + '​-'​ + photoIndex + '​]'​ ).parent().append( ​ ' <span style="​color:​red;">​*</​span>'​ );
 +                        });
 +                    });
 +                } else {
 +                    $("​input[name='​entity[albums]["​ + albumIndex + "​]["​+ albumField + "​]'​]"​).parent().append( ' <span style="​color:​red;">​*</​span>'​ );
 +                }
 +            });
 +        });
 +    }
 +</​code>​
 +
 +Со клиентской частью мы закончили.
 +Данного кода достаточно для управления альбомами без серверной части, только они не будут сохранятся и валидироваться,​ но в ''​$object''​ данные попадут,​ т.к. мы используем полные пути к свойствам объекта (например ''​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][smallImageId]''​ ) - GetFromRequest соберет объект так, как нужно.
 +
 +Попробуем модифицировать SaveEntityAction таким образом,​ чтобы он сохранял альбомы в базу.
 +
 +===== SaveEntityAction.php =====
 +Прежде всего мы должны помнить о том, что albums у объекта Entity - лист. ''​EntityFactory::​GetById( $id, array( BaseFactory::​WithLists => true ) )''​ выберет только альбомы,​ без фотографий. Фотографии будем выбрать вручную.
 +Создадим метод ''​refillAlbumPhotos()'',​ который добавляет к альбомам фотографии.
 +<code php>
 +protected function refillAlbumPhotos( $object ) {
 +    $photos = EntityPhotoFactory::​Get( array( '​entityId'​ => $object->​entityId ) );
 +    $photos = ArrayHelper::​Collapse( $photos, '​entityAlbumId'​ );
 +
 +    if ( !empty( $photos ) ){
 +        foreach( $object->​albums as $album ) {
 +            if ( isset( $photos[$album->​entityAlbumId] ) ) {
 +                $album->​photos = $photos[$album->​entityAlbumId];​
 +            }
 +        }
 +    }
 +}
 +</​code>​
 +  * дополнительно в EntityPhotoFactory мы добавили поиск по entityId.
 +
 +Далее сделаем так, чтобы фотографии грузились только тогда, когда мы открыли объект для редактирования.
 +<code php>
 +/**
 + * Set Json Albums Data to Template
 + * @return void
 + */
 +protected function beforeSave() {
 +    if ( $this->​action != self::​UpdateAction && !empty( $this->​currentObject->​entityId ) ) {
 +        $this->​refillAlbumPhotos( $this->​currentObject );
 +    }
 +
 +    Response::​setParameter( '​data',​ EntityAlbumUtility::​PrepareAlbumsData( $this->​currentObject ) );
 +}
 +</​code>​
 +  * EntityAlbumUtility::​PrepareAlbumsData() - метод, который подготавливает переменную ''​data''​ для шаблона photos.tmpl.php.
 +
 +Теперь,​ если у объекта есть альбомы и фотографии,​ то мы уже увидим их в шаблоне :).
 +
 +Добавим валидацию альбомов и фотографий.
 +<code php>
 +protected function validate( $object ) {
 +    $errors = parent::​$factory->​Validate( $object );
 +
 +    $albumErrors = EntityAlbumUtility::​ValidateAlbums( $object->​albums );
 +    if ( !empty( $albumErrors ) ) {
 +        $errors['​fields'​]['​photos'​]['​format'​] = '​format';​
 +        Response::​setParameter( '​albumErrors',​ $albumErrors );
 +    }
 +
 +    return $errors;
 +}
 +</​code>​
 +  * fields => photos поможет нам подсветить таб "​Фотографии",​ если на нем были ошибки (''<​div data-row="​**photos**"​...''​ в photos.tmpl.php) и не дать сохранить ошибочный объект в базу.
 +
 +Теперь можно перейти к сохранению. Для этого сначала нужно заполнить $originalObject фотографиями (в оригинальном объекте они нужны для того, чтобы удалить те фотографии,​ которые мы удалили с формы).
 +<code php>
 +protected function beforeAction() {
 +    if ( !empty( $this->​originalObject ) ) {
 +       ​$this->​refillAlbumPhotos( $this->​originalObject );
 +   }
 +}
 +</​code>​
 +
 +add() и update() будут обернуты в транзакцию.
 +<code php>
 +/**
 + * Add Object
 + *
 + * @param Entity $object
 + * @return bool
 + */
 +protected function add( $object ) {
 +    ConnectionFactory::​BeginTransaction();​
 +
 +    $result = parent::​$factory->​Add( $object, array( BaseFactory::​WithReturningKeys => true ) );
 +    $result = $result && EntityAlbumUtility::​SaveAlbums( $object, $this->​originalObject );
 +
 +    ConnectionFactory::​CommitTransaction( $result );
 +
 +    return $result;
 +}
 +
 +
 +/**
 + * Update Object
 + *
 + * @param Entity $object
 + * @return bool
 + */
 +protected function update( $object ) {
 +    ConnectionFactory::​BeginTransaction();​
 +
 +    $result = parent::​$factory->​Update( $object );
 +    $result = $result && EntityAlbumUtility::​SaveAlbums( $object, $this->​originalObject );
 +
 +    ConnectionFactory::​CommitTransaction( $result );
 +    ​
 +    return $result;
 +}
 +</​code>​
 +
 +Последний штрих. При сохранении фоток мы не получаем их идентификаторы,​ из-за этого неправильно работает кнопка "​Применить"​ в режиме редактирование. Исправляется это путем переполучения фотографий только при успешном сохранении.
 +<code php>
 +protected function afterAction( $success ) {
 +    if ( $this->​redirect == '​view'​ && $success ) {
 +        $this->​refillAlbumPhotos( $this->​currentObject );
 +        Response::​setParameter( '​data',​ EntityAlbumUtility::​PrepareAlbumsData( $this->​currentObject ) );
 +    }
 +}
 +</​code>​
 +
 +Не забудьте посмотреть код [[eaze:​samples:​добавление_фотогалереи_в_vt_к_объекту_EntityAlbumUtility.php|EntityAlbumUtility.php]].
 +
 +
 +====== Итог ======
 +Поставленной цели мы добились. Можно было бы конечно сделать массовый загрузчик на flash и потом и мышкой раскидать фотографии по альбомам,​ но для этого нужно написать ещё больше javascript'​а.
 +
 +Осталось рассмотреть плюсы и минусы подхода.
 +
 +===== Плюсы =====
 +  * Не нужно дополнительно обрабатывать получение связанных объектов из формы на PHP (albums, photos).
 +  * Не нужно дублировать шаблон отображения album и photo сначала в PHP, потом на JS. Всего используется один шаблон.
 +  * Минимальное количество кода в SaveEntityAction (в основном - только получение листов второго и последующих уровней).
 +
 +===== Минусы =====
 +  * GetFromRequest на втором уровне получает каждый объект через GetById (соответственно сколько файлов - столько запросов при сохранении). //​можно исправить,​ но сложновато//​
 +  * Если сохранить страницу при выключенном JS - то все фотографии удалятся (потому что не пришли с формы). //​можно исправить через дополнительную переменную,​ которая выставляется через JS//
 +  * При сохранении страницы для не измененных данных каждый раз выполняется UPDATE. //​можно исправить путем добавления проверки на эквивалентность объектов//​
 +
 +
 +
 +
 +
 +
 +
 +
 +
  
eaze/samples/добавление_фотогалереи_в_vt_к_объекту.txt · Последние изменения: 2011/09/21 11:30 — sergeyfast