Как и обещал в начале данного урока я вам расскажу, как можно создавать (не через админку) и другие опции товара и легко их добавлять в корзину. Обычно данная операция требуется на сайтах с нестандартным функционалом.
Смотрите, в корзине опции у товара содержатся в массиве $product[‘options’], и те, кто воспользовался моей подсказкой, прекрасно поняли, что, если мы хотим передать какую-либо опцию в корзину, нам необходимо, чтобы она попала в этот массив $product[‘options’]. А как сделать так, чтобы она попала? Вот:
<input type="hidden" name="option[custom_option]" value="Значание опции"/>
Где custom_option – это имя опции.
Перейдем с вами к нашему новому функционалу. В данном уроке мы будем с вами использовать компонент Comparison из modstore.pro. Данный компонент я ужет расматривал в “Сравнение товаров в miniShop2”, однако в данном уроке мы с вами будем модернизировать код сниппета CompareList, для того, чтобы опции товара у нас выводились сгрупированными, как в карточке товара.
Я думаю, что вы уже создали ресурс и шаблон “Сравнения товара”, поэтому не будем на это отвлекаться. Модифицированный сниппет CompareListGroup будет выглядеть следующим образом:
<?php /** @var array $scriptProperties */ /** @var Comparison $Comparison */ $Comparison = $modx->getService('comparison','Comparison',$modx->getOption('comparison_core_path',null,$modx->getOption('core_path').'components/comparison/').'model/comparison/',$scriptProperties); if (!($Comparison instanceof Comparison)) return ''; $Comparison->initialize($modx->context->key); /** @var pdoFetch $pdoFetch */ $pdoFetch = $modx->getService('pdoFetch'); $pdoFetch->setConfig($scriptProperties); $list = !empty($_REQUEST['list']) ? (string) $_REQUEST['list'] : 'default'; if (isset($_SESSION['Comparison'][$modx->context->key][$list]['ids'])) { $ids = array_keys($_SESSION['Comparison'][$modx->context->key][$list]['ids']); } elseif (!empty($_REQUEST['cmp_ids'])) { $ids = explode(',', preg_replace('/[^0-9\,]/', '', $_REQUEST['cmp_ids'])); } else { return $modx->lexicon('comparison_err_no_list'); } if (empty($fields)) {$fields = '{"default":["price","article","vendor.name","color","size"]}';} if (empty($tplRow)) {$tplRow = 'tpl.Comparison.row';} if (empty($tplParam)) {$tplParam = 'tpl.Comparison.param';} if (empty($tplCell)) {$tplCell = 'tpl.Comparison.cell';} if (empty($tplHead)) {$tplHead = 'tpl.Comparison.head';} if (empty($tplCorner)) {$tplCorner = 'tpl.Comparison.corner';} if (empty($tplOuter)) {$tplOuter = 'tpl.Comparison.outer';} if (empty($tplGroup)) {$tplGroup ='compareGroupTpl';} if (empty($minItems)) {$minItems = 1;} if (empty($maxItems)) {$maxItems = 10;} if (!isset($scriptProperties['showUnpublished'])) {$scriptProperties['showUnpublished'] = false;} if (!isset($scriptProperties['showDeleted'])) {$scriptProperties['showDeleted'] = false;} $fields = $modx->fromJSON($fields); if (empty($fields) || !is_array($fields)) { return $modx->lexicon('comparison_err_wrong_fields'); } elseif (!isset($fields[$list])) { if ($modx->user->isAuthenticated('mgr')) { return $modx->lexicon('comparison_err_wrong_list', array('list' => $list)); } else { return $modx->lexicon('comparison_err_no_list'); } } $fields = $fields[$list]; $format = null; if (!empty($formatSnippet)) { /** @var modSnippet $format */ $format = $modx->getObject('modSnippet', array('name' => $formatSnippet)); } // Joining MS2 tables if (in_array('msProduct', $modx->classMap['modResource'])) { $class = 'msProduct'; $leftJoin = array( 'Data' => array('class' => 'msProductData'), 'Vendor' => array('class' => 'msVendor', 'on' => 'Vendor.id = Data.vendor'), ); $select = array( $class => !empty($includeContent) ? $modx->getSelectColumns($class, $class) : $modx->getSelectColumns($class, $class, '', array('content'), true), 'Data' => $modx->getSelectColumns('msProductData', 'Data', '', array('id'), true), 'Vendor' => $modx->getSelectColumns('msVendor', 'Vendor', 'vendor.', array('id'), true), ); $thumbsSelect = array(); if (!empty($includeThumbs)) { $thumbs = array_map('trim',explode(',',$includeThumbs)); if(!empty($thumbs[0])){ foreach ($thumbs as $thumb) { $leftJoin[$thumb] = array( 'class' => 'msProductFile', 'on' => "`$thumb`.`product_id` = `{$class}`.`id` AND `$thumb`.`parent` != 0 AND `$thumb`.`path` LIKE '%/$thumb/'" ); $select[$thumb] = "`$thumb`.`url` as `$thumb`"; } } } } else { $class = 'modResource'; $leftJoin = $select = array(); } // Add custom parameters foreach (array('leftJoin','select') as $v) { if (!empty($scriptProperties[$v])) { $tmp = $modx->fromJSON($scriptProperties[$v]); if (is_array($tmp)) { $$v = array_merge($$v, $tmp); } } unset($scriptProperties[$v]); } $properties = array( 'class' => $class, 'parents' => 0, 'resources' => implode(',', $ids), 'includeTVs' => implode(',', $fields), 'leftJoin' => $leftJoin, 'select' => $select, 'groupby' => $class . '.id', 'limit' => $maxItems, 'return' => 'data', 'nestedChunkPrefix' => 'comparison_' ); $pdoFetch->setConfig(array_merge($scriptProperties, $properties), false); $resources = $pdoFetch->run(); $output = $rows = ''; if (count($ids) < $minItems) { $output = $modx->lexicon('comparison_err_min_count'); } elseif (count($ids) > $maxItems) { $output = $modx->lexicon('comparison_err_max_resource'); } else { $row_idx = 1; $grouper = array(); foreach ($fields as $field) { //$cells = $pdoFetch->getChunk($tplParam, array('field' => $field, 'option' => $data["option"])); $cells = ''; $cell_idx = 1; $previous_value = null; $same = true; foreach ($resources as $resource) { $value = ''; if (array_key_exists($field, $resource)) { $value = $resource[$field]; } elseif (stripos($field, 'option.') === 0) { $tmp_field = substr($field, 7); $values = $pdoFetch->getCollection( 'msProductOption', array('key' => $tmp_field, 'product_id' => $resource['id']), array('select' => 'value', 'sortby' => 'value') ); if (!empty($values)) { if (count($values) > 1) { $value = array(); foreach ($values as $tmp) { $value[] = $tmp['value']; } } else { $value = $values[0]['value']; } } $option_data = array(); $option = $modx->getObject('msOption', array('key' => $tmp_field)); if($option){ $cat = $modx->getObject("modCategory", $option->get("category")); $optid = $option->get("id"); $option_data["option_key"] = $option->get("key"); $option_data["option_measure_unit"] = $option->get("measure_unit"); $option_data["option_category"] = $option->get("category"); $option_data["option_category_name"] = $cat->get("category"); $option_data["option_type"] = $option->get("type"); $option_data["option_caption"] = $option->get("caption"); $option_data["option_description"] = $option->get("description"); } $pdoFetch->addTime('Get product option "' . $tmp_field . '" for product "' . $resource['id'] . '"'); } // Send value to special snippet if ($format) { $format->_cacheable = false; $format->_processed = false; $format->_content = ''; $value = $format->process(array( 'name' => $field, 'field' => $field, 'input' => $value, 'value' => $value, 'resource' => $resource, 'pdoTools' => $pdoFetch, 'pdoFetch' => $pdoFetch, )); } else { if (is_array($value)) { natsort($value); $value = implode(',', $value); } if ($class == 'msProduct' && in_array($field, array('price', 'old_price', 'weight'))) { /** @var miniShop2 $miniShop2 */ if ($miniShop2 = $modx->getService('miniShop2')) { switch ($field) { case 'price': case 'old_price': $value = $miniShop2->formatPrice($value) . ' ' . $modx->lexicon('ms2_frontend_currency'); break; case 'weight': $value = $miniShop2->formatWeight($value) . ' ' . $modx->lexicon('ms2_frontend_weight_unit'); break; } } } } if ($same && $cell_idx > 1) { $same = $previous_value == $value; } $cells .= $pdoFetch->getChunk($tplCell, array( 'value' => $value, 'product_id' => $resource['id'], 'cell_idx' => $cell_idx ++, 'classes' => ' field-'.$field, 'option_measure_unit' => $option_data["option_measure_unit"], 'option_type' => $option_data["option_type"]) ); $previous_value = $value; } $data = array( 'cells' => $cells, 'row_idx' => $row_idx++, 'same' => $same ); $option = $modx->getObject('msOption', array('key' => $tmp_field)); if($option){ $cat = $modx->getObject("modCategory", $option->get("category")); $optid = $option->get("id"); $data["option_key"] = $option_data["option_key"]; $data["option_measure_unit"] = $option_data["option_measure_unit"]; $data["option_category"] = $option_data["option_category"]; $data["option_category_name"] = $option_data["option_category_name"]; $data["option_type"] = $option_data["option_type"]; $data["option_caption"] = $option_data["option_caption"]; $data["option_description"] = $option_data["option_description"]; $grouper[$data["option_category"]]["name"] = $data["option_category_name"]; $grouper[$data["option_category"]]["row"] .= $pdoFetch->getChunk($tplRow, $data); }else{ if ($miniShop2 = $modx->getService('miniShop2')) { switch ($field) { case 'price': case 'old_price': $data["option_caption"] = "Цена"; break; case 'weight': $data["option_caption"] = "Вес"; break; case 'vendor.name': $data["option_caption"] = "Производитель"; break; } } $grouper[0]["name"] = "Основные характеристики"; $grouper[0]["row"] .= $pdoFetch->getChunk($tplRow, $data); } //$rows .= $pdoFetch->getChunk($tplRow, $data); } foreach($grouper as $group){ $rows .= $pdoFetch->getChunk($tplGroup, $group); } $head = ''; foreach ($resources as $resource) { $resource['list'] = $list; $head .= $pdoFetch->getChunk($tplHead, $resource); } $output = $pdoFetch->getChunk($tplOuter, array('head' => $head, 'rows' => $rows)); } if ($modx->user->hasSessionContext('mgr') && !empty($showLog)) { $output .= '<pre class="CompareListLog">' . print_r($pdoFetch->getTime(),1) . '</pre>'; } $modx->regClientScript('<script type="text/javascript">Comparison.list.initialize(".comparison-table", {minItems:'.$minItems.'});</script>', true); return $output;
Над данным сниппетом также можно поработать и сделать количество чанков гораздо меньше. Но в данном случае мы с вами будем работать с тем, что есть. Создаем новый сниппет compare_list_group.php в нашей файловой директории. Вызов сниппета в шаблоне у нас будет следующий:
{'@FILE snippets/compare_list_group.php' | snippet : [ 'fields' => '{"default":["price","vendor.name"]}', 'tplGroup' => '@FILE chunks/compare_group.tpl', 'tplRow' => '@FILE chunks/compare_row.tpl', 'tplHead' => '@FILE chunks/compare_product.tpl', 'tplCell' => '@FILE chunks/compare_cell.tpl', 'tplOuter' => '@FILE chunks/compare_outer.tpl', ]}
Конечно, нам еще нужны и чанки. Чанк группы compare_group.tpl:
<div class="compare-table"> <div class="title"> <span>{$name} <i class="fa fa-angle-up"></i></span> </div> {$row} </div>
Чанк строки compare_row.tpl:
<div class="table-row {if $same != 1?}different{else}same{/if}"> <div class="table-title">{$option_caption} {if $option_description?} <span data-toggle="tooltip" data-placement="right" title="{$option_description}"> <i class="fa fa-question-circle"></i> </span> {/if} </div> {$cells} </div>
Чанк товара – compare_product.tpl:
<div class="product" data-product-id="{$id}"> <a href="#" class="comparison-remove comparison-link close"></a> <div class="item eqh"> <div class="image"> <div class="labels"> {if $favorite?} <div> <span class="labels-hits">Хит продаж</span> </div> {/if} {if $new?} <div> <span class="labels-new">Новинка</span> </div> {/if} {if $old_price > 0?} <div> <span class="labels-action">Акция</span> </div> {/if} </div> <div class="bigger"> <a href="{$id | url}" class="text-center" style="display:block"> {if $image?} <img src="{$image}" alt="{$pagetitle | htmlent}" title="{$pagetitle | htmlent}" class="img-fluid"> {else} <img src="{$_modx->getPlaceholder('+noimage')}" alt="{$pagetitle | htmlent}" title="{$pagetitle | htmlent}" class="img-fluid"> {/if} </a> {if $_pls['vendor.logo']?} <img src="{$_pls['vendor.logo']}" class="vendor" alt="{$_pls['vendor.name']}"> {/if} </div> </div> <div class="text"> <a href="{$id | url}" class="title">{$pagetitle}</a> <div class="characters"> {$_modx->runSnippet('msProductOptions', [ 'onlyOptions' => $parent | resource: 'options', 'product' => $id, 'tpl' => '@FILE chunks/main_opts.tpl' ])} </div> <div class="prices-block"> <div class="prices"> {if $old_price > 0?} <div class="old_price"> <span>{$old_price}</span> <i class="fa fa-ruble"></i> </div> {/if} {if $price > 0?} <div class="price"> <span>{$price}</span> <i class="fa fa-ruble"></i> </div> {/if} </div> <form method="post" class="ms2_form"> <input type="hidden" name="id" value="{$id}"> <input type="hidden" name="count" value="1"> <input type="hidden" name="options" value="[]"> <button type="submit" name="ms2_action" value="cart/add" class="btn btn-blue btn-rounded">В корзину</button> </form> </div> </div> </div> </div>
Чанк ячейки – compare_cell.tpl:
<div class="table-value {$classes}" data-product-id="{$product_id}" data-type="{$option_type}"> <p> {if $option_type == "combo-boolean"?} {if $value == 1?} <span class="green">Да</span> {else} <span class="red">Нет</span> {/if} {else} {if $option_type == "combo-options"?} {var $values = $value|split} {foreach $values as $val} <span class="label label-default">{$val}</span> {/foreach} {$option_measure_unit} {else} {$value} {$option_measure_unit} {/if} {/if} </p> </div>
Чанк обертки – compare_outer.tpl:
<div class="compare-table-header comparison-table" id="compareHeader"> <div class="slider-container"> <div class="compare-slider products owl-carousel"> {$head} </div> </div> </div> <div class="compare-tables"> {$rows} </div>
Если подойти грамотно и с помощью всех возможностей Fenom, то мы можем обойтись одним чанком вместо пяти. Есть кто-нибудь, кто хочет это сделать? Отлично, если кто-то нашелся.
Следующим шагом нам необходимо реализовать механизм добавления товаров в сравнение. Как это будет выглядеть в карточке товара:
{$_modx->runSnippet('!addComparison', [ 'list_id' => 10, 'id' => $_modx->resource.id ])}
В параметре list_id содержится идентификатор страницы сравнения товаров. Изменим стандартный чанк tpl.Comparison.add на следующий:
<div class="comparison comparison-[[+list]][[+added]][[+can_compare]]" data-id="[[+id]]" data-list="[[+list]]"> <a href="#" class="comparison-add comparison-link compare" data-text="[[%comparison_updating_list]]">[[%comparison_add_to_list]]</a> <a href="#" class="comparison-remove comparison-link compare" data-text="[[%comparison_updating_list]]">[[%comparison_remove_from_list]]</a> <a href="[[+link]]" class="comparison-go compare">[[%comparison_go_to_list]]</a> <span class="comparison-total">[[+total]]</span> </div> <!--comparison_can_compare can_compare--> <!--comparison_added added-->
В шапке сайта нам нужно вывести блок с ссылкой на страницу стравнения. Для этого в чанке header выводим следующую конструкцию:
{$_modx->runSnippet('!addComparison', [ 'list_id' => 10, 'tpl' => '@FILE chunks/compare_total.tpl' ])}
Чанк compare_total.tpl:
<div data-id="{$id}" data-list="{$list}" class="comparison-default"> <a href="{10 | url}" data-toggle="tooltip" data-placement="bottom" title="Сравнение товаров"> <i class="fa fa-sliders-h"> <span class="comparison-total">{$total}</span> </i> </a> </div>
Последний шаг – нужно модернизировать assets/components/comparison/js/default.js у Comparison:
Comparison = { add: { options: { add: '.comparison-add', remove: '.comparison-remove', go: '.comparison-go', total: '.comparison-total', added: 'added', can_compare: 'can_compare', loading: 'loading' }, initialize: function(selector, params) { if (!$(selector).length) {return;} var options = this.options; var minItems = !params.minItems ? 2 : params.minItems; $(document).on('click', selector + ' ' + options.add + ',' + selector + ' ' + options.remove, function() { var $this = $(this); var $parent = $this.parents(selector); var text = $this.data('text'); var list = $parent.data('list'); var id = $parent.data('id'); var action = $this.hasClass(options.add.substr(1)) ? 'add' : 'remove'; if ($this.hasClass(options.loading)) {return false;} else {$this.addClass(options.loading);} if (text.length) { $this.attr('data-text', Comparison.utils.encode($this.html())).html(text); } $.post(document.location.href, {cmp_action: action, list: list, resource: id}, function(response) { if (text.length) { text = Comparison.utils.decode($this.attr('data-text')); $this.attr('data-text', Comparison.utils.encode($this.html())).html(text); } $this.removeClass(options.loading); if (response.success) { $(options.total, selector).text(response.data.total); if (response.data.link) { $(options.go, selector).attr('href', response.data.link); } if (response.data.total >= minItems) { $(selector).addClass(options.can_compare); } else {$(selector).removeClass(options.can_compare);} if (action == 'add') {$parent.addClass(options.added);} else {$parent.removeClass(options.added);} } else { if (typeof miniShop2 != 'undefined') {miniShop2.Message.error(response.message);} else {alert(response.message);} } }, 'json'); return false; }); } }, list: { options: { all: '.comparison-params-all', unique: '.comparison-params-unique', remove: '.comparison-remove', same_class: 'same', active_class: 'active' }, initialize: function(selector, params) { if (!$(selector).length) {return;} var options = this.options; var minItems = !params.min ? 1 : params.min; // Switch parameters $(document).on('click', selector + ' ' + options.all + ',' + selector + ' ' + options.unique, function() { var $this = $(this); var $parent = $this.parents(selector); if ($this.hasClass(options.active_class)) { return false; } else if ($this.hasClass(options.all.substr(1))) { $(options.unique, $parent).removeClass(options.active_class); $this.addClass(options.active_class); $('.'+options.same_class, $parent).show(); } else if ($this.hasClass(options.unique.substr(1))) { $(options.all, $parent).removeClass(options.active_class); $this.addClass(options.active_class); $('.'+options.same_class, $parent).hide(); } return false; }); // Remove from list $(document).on('click', selector + ' ' + options.remove, function(e) { e.preventDefault(); var $this = $(this); var par = $(this).parent().parent(); var $parent = $this.parents(selector); var text = $this.data('text'); var list = $this.parent().data('list'); var id = $this.parent().data('product-id'); var index = $(options.remove, selector).index(this) + 1; $.post(document.location.href, {cmp_action: 'remove', list: list, resource: id}, function(response) { $this.removeClass(options.loading); if (response.success) { if (response.data.total < minItems) { document.location = document.location.pathname; } $parent.find('.compare-table .table-row').each(function() { $(this).find('.table-value[data-product-id='+id+']').remove(); }); var carousel = $('.compare-slider').data('owl.carousel'); var owlindex = carousel.relative(par.index()); $(".compare-slider").trigger('remove.owl.carousel', [owlindex]).trigger('refresh.owl.carousel'); } else { if (typeof miniShop2 != 'undefined') {miniShop2.Message.error(response.message);} else {alert(response.message);} } }, 'json'); return false; }); } }, utils: { encode: function(string) { return $('<pre/>').text(string).html(); }, decode: function(string) { return $("<pre/>").html(string).text(); } } };
И у нас получился отличный сервис для сравнения товаров. Да, тут нужно еще поработать над группами, но в целом – очень даже неплохо.