Как и обещал в начале данного урока я вам расскажу, как можно создавать (не через админку) и другие опции товара и легко их добавлять в корзину. Обычно данная операция требуется на сайтах с нестандартным функционалом.
Смотрите, в корзине опции у товара содержатся в массиве $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();
}
}
};
И у нас получился отличный сервис для сравнения товаров. Да, тут нужно еще поработать над группами, но в целом – очень даже неплохо.
$(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; } $(options.total).text(response.data.total); $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; });