first commit

This commit is contained in:
2024-11-05 12:22:50 +01:00
commit e5682a3912
19641 changed files with 2948548 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"presets": [
"@babel/preset-env"
]
}

View File

@@ -0,0 +1,43 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.bootstrap .filter_list .filter_list_item {
display: table;
width: 100%;
padding: 5px 0;
margin-bottom: 4px;
background-color: white;
box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.1) 0 -2px 0 inset;
border-radius: 3px;
cursor: pointer;
}
.bootstrap .filter_panel {
min-height: 20px;
padding: 7px 7px 0px 7px;
margin-bottom: 20px;
background-color: #ebebeb;
border: 1px solid #d9d9d9;
border-radius: 3px;
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
}
.bootstrap .filter_panel header {
margin-bottom: 7px;
}
.bootstrap .prestashop-switch span {
display: none;
}/*# sourceMappingURL=blocklayered.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["blocklayered.scss","blocklayered.css"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;EAAA;AAmBE;EACC,cAAA;EACA,WAAA;EACA,cAAA;EACA,kBAAA;EACA,uBAAA;EAEA,yEAAA;EAKA,kBAAA;EACC,eAAA;ACAJ;ADGE;EACC,gBAAA;EACA,wBAAA;EACA,mBAAA;EACA,yBAAA;EACA,yBAAA;EACA,kBAAA;EAEA,+CAAA;ACDH;ADGI;EACC,kBAAA;ACDL;ADMI;EACE,aAAA;ACJN","file":"blocklayered.css"}

View File

@@ -0,0 +1,56 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.bootstrap {
.filter_list .filter_list_item {
display: table;
width: 100%;
padding: 5px 0;
margin-bottom: 4px;
background-color: white;
-webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.1) 0 -2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.3) 0 0 3px, rgba(0, 0, 0, 0.1) 0 -2px 0 inset;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
-ms-border-radius: 3px;
-o-border-radius: 3px;
border-radius: 3px;
cursor: pointer;
}
.filter_panel {
min-height: 20px;
padding: 7px 7px 0px 7px;
margin-bottom: 20px;
background-color: #ebebeb;
border: 1px solid #d9d9d9;
border-radius: 3px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
header {
margin-bottom: 7px;
}
}
.prestashop-switch {
span {
display: none;
}
}
}

View File

@@ -0,0 +1,247 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import './blocklayered.scss';
/* eslint-disable no-unused-vars, no-alert */
function checkForm() {
let isCategorySelected = false;
let isFilterSelected = false;
$('#categories-treeview input[type=checkbox]').each(function checkCategoriesCheckboxes() {
if ($(this).prop('checked')) {
isCategorySelected = true;
return false;
}
return true;
});
$('.filter_list_item input[type=checkbox]').each(function checkFilterListCheckboxes() {
if ($(this).prop('checked')) {
isFilterSelected = true;
return false;
}
return true;
});
if (!isCategorySelected) {
alert(translations.no_selected_categories);
$('#categories-treeview input[type=checkbox]').first().focus();
return false;
}
if (!isFilterSelected) {
alert(translations.no_selected_filters);
$('#filter_list_item input[type=checkbox]').first().focus();
return false;
}
return true;
}
$(document).ready(() => {
$('.ajaxcall').click(function onAjaxCall() {
if (this.legend === undefined) {
this.legend = $(this).html();
}
if (this.running === undefined) {
this.running = false;
}
if (this.running === true) {
return false;
}
$('.ajax-message').hide();
this.running = true;
if (typeof (this.restartAllowed) === 'undefined' || this.restartAllowed) {
$(this).html(this.legend + translations.in_progress);
$('#indexing-warning').show();
}
this.restartAllowed = false;
const type = $(this).attr('rel');
$.ajax({
url: `${this.href}&ajax=1`,
context: this,
dataType: 'json',
cache: 'false',
success() {
this.running = false;
this.restartAllowed = true;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(
type === 'price' ? translations.url_indexation_finished : translations.attribute_indexation_finished,
);
$('#ajax-message-ok').show();
},
error() {
this.restartAllowed = true;
$('#indexing-warning').hide();
$('#ajax-message-ko span').html(
type === 'price' ? translations.url_indexation_failed : translations.attribute_indexation_failed,
);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.running = false;
},
});
return false;
});
let totalCount = 0;
$('.ajaxcall-recurcive').each((it, elm) => {
$(elm).click(function onAjaxRecursiveCall(e) {
e.preventDefault();
if (this.cursor === undefined) {
this.cursor = 0;
}
if (this.legend === undefined) {
this.legend = $(this).html();
}
if (this.running === undefined) {
this.running = false;
}
if (this.running === true) {
return false;
}
$('.ajax-message').hide();
this.running = true;
if (typeof (this.restartAllowed) === 'undefined' || this.restartAllowed) {
$(this).html(this.legend + translations.in_progress);
$('#indexing-warning').show();
}
this.restartAllowed = false;
$.ajax({
url: `${this.href}&ajax=1&cursor=${this.cursor}`,
context: this,
dataType: 'json',
cache: 'false',
success(res) {
this.running = false;
if (res.result) {
this.cursor = 0;
totalCount = 0;
$('#indexing-warning').hide();
$(this).html(this.legend);
$('#ajax-message-ok span').html(translations.price_indexation_finished);
$('#ajax-message-ok').show();
return;
}
totalCount += parseInt(res.count, 10);
this.cursor = parseInt(res.cursor, 10);
$(this).html(
this.legend + translations.price_indexation_in_progress.replace(
'%s',
`${totalCount}/${res.total}`,
),
);
$(this).click();
},
error(res) {
this.restartAllowed = true;
$('#indexing-warning').hide();
$('#ajax-message-ko span').html(translations.price_indexation_failed);
$('#ajax-message-ko').show();
$(this).html(this.legend);
this.cursor = 0;
this.running = false;
},
});
return false;
});
});
if (typeof PS_LAYERED_INDEXED !== 'undefined' && PS_LAYERED_INDEXED) {
$('#url-indexe').click();
$('#full-index').click();
}
$('.sortable').sortable({
forcePlaceholderSize: true,
});
$('.filter_list_item input[type=checkbox]').click(function onFilterLickItemCheckboxesClicked() {
const currentSelectedFiltersCount = parseInt($('#selected_filters').html(), 10);
$('#selected_filters').html(
$(this).prop('checked') ? currentSelectedFiltersCount + 1 : currentSelectedFiltersCount - 1,
);
});
if (typeof window.filters !== 'undefined') {
const filters = JSON.parse(window.filters);
let container = null;
let $el;
Object.keys(filters).forEach((filter) => {
$el = $(`#${filter}`);
$el.prop('checked', true);
$('#selected_filters').html(parseInt($('#selected_filters').html(), 10) + 1);
$(`select[name="${filter}_filter_type"]`).val(filters[filter].filter_type);
$(`select[name="${filter}_filter_show_limit"]`).val(filters[filter].filter_show_limit);
if (container === null) {
container = $(`#${filter}`).closest('ul');
$el.closest('li').detach().prependTo(container);
} else {
$el.closest('li').detach().insertAfter(container);
}
container = $el.closest('li');
});
}
});
$(document).on('ready', () => {
const layeredDefaultCategory = $('input[name="ps_layered_filter_by_default_category"]');
layeredDefaultCategory.on('change', function initializeOptions(event) {
const elm = $(this);
if (!elm.prop('checked')) {
return;
}
if (elm.val() === '1') {
$('input[name="ps_layered_full_tree"][value="0"]').prop('checked', true);
$('input[name="ps_layered_full_tree"]').prop('disabled', true);
} else {
$('input[name="ps_layered_full_tree"]').prop('disabled', false);
}
});
layeredDefaultCategory.filter('[value="1"]').trigger('change');
});

View File

@@ -0,0 +1,26 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
class LocalizationException {
constructor(message) {
this.message = message;
this.name = 'LocalizationException';
}
}
export default LocalizationException;

View File

@@ -0,0 +1,29 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import NumberFormatter from './number-formatter';
import NumberSymbol from './number-symbol';
import PriceSpecification from './specifications/price';
import NumberSpecification from './specifications/number';
export {
PriceSpecification,
NumberSpecification,
NumberFormatter,
NumberSymbol,
};

View File

@@ -0,0 +1,317 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
/**
* These placeholders are used in CLDR number formatting templates.
* They are meant to be replaced by the correct localized symbols in the number formatting process.
*/
import NumberSymbol from './number-symbol';
import PriceSpecification from './specifications/price';
import NumberSpecification from './specifications/number';
const escapeRE = require('lodash.escaperegexp');
const CURRENCY_SYMBOL_PLACEHOLDER = '¤';
const DECIMAL_SEPARATOR_PLACEHOLDER = '.';
const GROUP_SEPARATOR_PLACEHOLDER = ',';
const MINUS_SIGN_PLACEHOLDER = '-';
const PERCENT_SYMBOL_PLACEHOLDER = '%';
const PLUS_SIGN_PLACEHOLDER = '+';
class NumberFormatter {
/**
* @param NumberSpecification specification Number specification to be used
* (can be a number spec, a price spec, a percentage spec)
*/
constructor(specification) {
this.numberSpecification = specification;
}
/**
* Formats the passed number according to specifications.
*
* @param int|float|string number The number to format
* @param NumberSpecification specification Number specification to be used
* (can be a number spec, a price spec, a percentage spec)
*
* @return string The formatted number
* You should use this this value for display, without modifying it
*/
format(number, specification) {
if (specification !== undefined) {
this.numberSpecification = specification;
}
/*
* We need to work on the absolute value first.
* Then the CLDR pattern will add the sign if relevant (at the end).
*/
const num = Math.abs(number).toFixed(this.numberSpecification.getMaxFractionDigits());
let [majorDigits, minorDigits] = this.extractMajorMinorDigits(num);
majorDigits = this.splitMajorGroups(majorDigits);
minorDigits = this.adjustMinorDigitsZeroes(minorDigits);
// Assemble the final number
let formattedNumber = majorDigits;
if (minorDigits) {
formattedNumber += DECIMAL_SEPARATOR_PLACEHOLDER + minorDigits;
}
// Get the good CLDR formatting pattern. Sign is important here !
const pattern = this.getCldrPattern(number < 0);
formattedNumber = this.addPlaceholders(formattedNumber, pattern);
formattedNumber = this.replaceSymbols(formattedNumber);
formattedNumber = this.performSpecificReplacements(formattedNumber);
return formattedNumber;
}
/**
* Get number's major and minor digits.
*
* Major digits are the "integer" part (before decimal separator),
* minor digits are the fractional part
* Result will be an array of exactly 2 items: [majorDigits, minorDigits]
*
* Usage example:
* list(majorDigits, minorDigits) = this.getMajorMinorDigits(decimalNumber);
*
* @param DecimalNumber number
*
* @return string[]
*/
extractMajorMinorDigits(number) {
// Get the number's major and minor digits.
const result = number.toString().split('.');
const majorDigits = result[0];
const minorDigits = (result[1] === undefined) ? '' : result[1];
return [majorDigits, minorDigits];
}
/**
* Splits major digits into groups.
*
* e.g.: Given the major digits "1234567", and major group size
* configured to 3 digits, the result would be "1 234 567"
*
* @param string majorDigits The major digits to be grouped
*
* @return string The grouped major digits
*/
splitMajorGroups(digit) {
if (!this.numberSpecification.isGroupingUsed()) {
return digit;
}
// Reverse the major digits, since they are grouped from the right.
const majorDigits = digit.split('').reverse();
// Group the major digits.
let groups = [];
groups.push(majorDigits.splice(0, this.numberSpecification.getPrimaryGroupSize()));
while (majorDigits.length) {
groups.push(majorDigits.splice(0, this.numberSpecification.getSecondaryGroupSize()));
}
// Reverse back the digits and the groups
groups = groups.reverse();
const newGroups = [];
groups.forEach((group) => {
newGroups.push(group.reverse().join(''));
});
// Reconstruct the major digits.
return newGroups.join(GROUP_SEPARATOR_PLACEHOLDER);
}
/**
* Adds or remove trailing zeroes, depending on specified min and max fraction digits numbers.
*
* @param string minorDigits Digits to be adjusted with (trimmed or padded) zeroes
*
* @return string The adjusted minor digits
*/
adjustMinorDigitsZeroes(minorDigits) {
let digit = minorDigits;
if (digit.length > this.numberSpecification.getMaxFractionDigits()) {
// Strip any trailing zeroes.
digit = digit.replace(/0+$/, '');
}
if (digit.length < this.numberSpecification.getMinFractionDigits()) {
// Re-add needed zeroes
digit = digit.padEnd(
this.numberSpecification.getMinFractionDigits(),
'0',
);
}
return digit;
}
/**
* Get the CLDR formatting pattern.
*
* @see http://cldr.unicode.org/translation/number-patterns
*
* @param bool isNegative If true, the negative pattern
* will be returned instead of the positive one
*
* @return string The CLDR formatting pattern
*/
getCldrPattern(isNegative) {
if (isNegative) {
return this.numberSpecification.getNegativePattern();
}
return this.numberSpecification.getPositivePattern();
}
/**
* Replace placeholder number symbols with relevant numbering system's symbols.
*
* @param string number
* The number to process
*
* @return string
* The number with replaced symbols
*/
replaceSymbols(number) {
const symbols = this.numberSpecification.getSymbol();
const map = {};
map[DECIMAL_SEPARATOR_PLACEHOLDER] = symbols.getDecimal();
map[GROUP_SEPARATOR_PLACEHOLDER] = symbols.getGroup();
map[MINUS_SIGN_PLACEHOLDER] = symbols.getMinusSign();
map[PERCENT_SYMBOL_PLACEHOLDER] = symbols.getPercentSign();
map[PLUS_SIGN_PLACEHOLDER] = symbols.getPlusSign();
return this.strtr(number, map);
}
/**
* strtr() for JavaScript
* Translate characters or replace substrings
*/
strtr(str, pairs) {
const substrs = Object.keys(pairs).map(escapeRE);
return str.split(RegExp(`(${substrs.join('|')})`))
.map((part) => pairs[part] || part)
.join('');
}
/**
* Add missing placeholders to the number using the passed CLDR pattern.
*
* Missing placeholders can be the percent sign, currency symbol, etc.
*
* e.g. with a currency CLDR pattern:
* - Passed number (partially formatted): 1,234.567
* - Returned number: 1,234.567 ¤
* ("¤" symbol is the currency symbol placeholder)
*
* @see http://cldr.unicode.org/translation/number-patterns
*
* @param formattedNumber
* Number to process
* @param pattern
* CLDR formatting pattern to use
*
* @return string
*/
addPlaceholders(formattedNumber, pattern) {
/*
* Regex groups explanation:
* # : literal "#" character. Once.
* (,#+)* : any other "#" characters group, separated by ",". Zero to infinity times.
* 0 : literal "0" character. Once.
* (\.[0#]+)* : any combination of "0" and "#" characters groups, separated by '.'.
* Zero to infinity times.
*/
return pattern.replace(/#?(,#+)*0(\.[0#]+)*/, formattedNumber);
}
/**
* Perform some more specific replacements.
*
* Specific replacements are needed when number specification is extended.
* For instance, prices have an extended number specification in order to
* add currency symbol to the formatted number.
*
* @param string formattedNumber
*
* @return mixed
*/
performSpecificReplacements(formattedNumber) {
if (this.numberSpecification instanceof PriceSpecification) {
return formattedNumber
.split(CURRENCY_SYMBOL_PLACEHOLDER)
.join(this.numberSpecification.getCurrencySymbol());
}
return formattedNumber;
}
static build(specifications) {
let symbol;
if (undefined !== specifications.numberSymbols) {
symbol = new NumberSymbol(...specifications.numberSymbols);
} else {
symbol = new NumberSymbol(...specifications.symbol);
}
let specification;
if (specifications.currencySymbol) {
specification = new PriceSpecification(
specifications.positivePattern,
specifications.negativePattern,
symbol,
parseInt(specifications.maxFractionDigits, 10),
parseInt(specifications.minFractionDigits, 10),
specifications.groupingUsed,
specifications.primaryGroupSize,
specifications.secondaryGroupSize,
specifications.currencySymbol,
specifications.currencyCode,
);
} else {
specification = new NumberSpecification(
specifications.positivePattern,
specifications.negativePattern,
symbol,
parseInt(specifications.maxFractionDigits, 10),
parseInt(specifications.minFractionDigits, 10),
specifications.groupingUsed,
specifications.primaryGroupSize,
specifications.secondaryGroupSize,
);
}
return new NumberFormatter(specification);
}
}
export default NumberFormatter;

View File

@@ -0,0 +1,222 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from './exception/localization';
class NumberSymbol {
/**
* NumberSymbolList constructor.
*
* @param string decimal Decimal separator character
* @param string group Digits group separator character
* @param string list List elements separator character
* @param string percentSign Percent sign character
* @param string minusSign Minus sign character
* @param string plusSign Plus sign character
* @param string exponential Exponential character
* @param string superscriptingExponent Superscripting exponent character
* @param string perMille Permille sign character
* @param string infinity The infinity sign. Corresponds to the IEEE infinity bit pattern.
* @param string nan The NaN (Not A Number) sign. Corresponds to the IEEE NaN bit pattern.
*
* @throws LocalizationException
*/
constructor(
decimal,
group,
list,
percentSign,
minusSign,
plusSign,
exponential,
superscriptingExponent,
perMille,
infinity,
nan,
) {
this.decimal = decimal;
this.group = group;
this.list = list;
this.percentSign = percentSign;
this.minusSign = minusSign;
this.plusSign = plusSign;
this.exponential = exponential;
this.superscriptingExponent = superscriptingExponent;
this.perMille = perMille;
this.infinity = infinity;
this.nan = nan;
this.validateData();
}
/**
* Get the decimal separator.
*
* @return string
*/
getDecimal() {
return this.decimal;
}
/**
* Get the digit groups separator.
*
* @return string
*/
getGroup() {
return this.group;
}
/**
* Get the list elements separator.
*
* @return string
*/
getList() {
return this.list;
}
/**
* Get the percent sign.
*
* @return string
*/
getPercentSign() {
return this.percentSign;
}
/**
* Get the minus sign.
*
* @return string
*/
getMinusSign() {
return this.minusSign;
}
/**
* Get the plus sign.
*
* @return string
*/
getPlusSign() {
return this.plusSign;
}
/**
* Get the exponential character.
*
* @return string
*/
getExponential() {
return this.exponential;
}
/**
* Get the exponent character.
*
* @return string
*/
getSuperscriptingExponent() {
return this.superscriptingExponent;
}
/**
* Gert the per mille symbol (often "‰").
*
* @see https://en.wikipedia.org/wiki/Per_mille
*
* @return string
*/
getPerMille() {
return this.perMille;
}
/**
* Get the infinity symbol (often "∞").
*
* @see https://en.wikipedia.org/wiki/Infinity_symbol
*
* @return string
*/
getInfinity() {
return this.infinity;
}
/**
* Get the NaN (not a number) sign.
*
* @return string
*/
getNan() {
return this.nan;
}
/**
* Symbols list validation.
*
* @throws LocalizationException
*/
validateData() {
if (!this.decimal || typeof this.decimal !== 'string') {
throw new LocalizationException('Invalid decimal');
}
if (!this.group || typeof this.group !== 'string') {
throw new LocalizationException('Invalid group');
}
if (!this.list || typeof this.list !== 'string') {
throw new LocalizationException('Invalid symbol list');
}
if (!this.percentSign || typeof this.percentSign !== 'string') {
throw new LocalizationException('Invalid percentSign');
}
if (!this.minusSign || typeof this.minusSign !== 'string') {
throw new LocalizationException('Invalid minusSign');
}
if (!this.plusSign || typeof this.plusSign !== 'string') {
throw new LocalizationException('Invalid plusSign');
}
if (!this.exponential || typeof this.exponential !== 'string') {
throw new LocalizationException('Invalid exponential');
}
if (!this.superscriptingExponent || typeof this.superscriptingExponent !== 'string') {
throw new LocalizationException('Invalid superscriptingExponent');
}
if (!this.perMille || typeof this.perMille !== 'string') {
throw new LocalizationException('Invalid perMille');
}
if (!this.infinity || typeof this.infinity !== 'string') {
throw new LocalizationException('Invalid infinity');
}
if (!this.nan || typeof this.nan !== 'string') {
throw new LocalizationException('Invalid nan');
}
}
}
export default NumberSymbol;

View File

@@ -0,0 +1,170 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from '../exception/localization';
import NumberSymbol from '../number-symbol';
class NumberSpecification {
/**
* Number specification constructor.
*
* @param string positivePattern CLDR formatting pattern for positive amounts
* @param string negativePattern CLDR formatting pattern for negative amounts
* @param NumberSymbol symbol Number symbol
* @param int maxFractionDigits Maximum number of digits after decimal separator
* @param int minFractionDigits Minimum number of digits after decimal separator
* @param bool groupingUsed Is digits grouping used ?
* @param int primaryGroupSize Size of primary digits group in the number
* @param int secondaryGroupSize Size of secondary digits group in the number
*
* @throws LocalizationException
*/
constructor(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
) {
this.positivePattern = positivePattern;
this.negativePattern = negativePattern;
this.symbol = symbol;
this.maxFractionDigits = maxFractionDigits;
// eslint-disable-next-line
this.minFractionDigits = maxFractionDigits < minFractionDigits ? maxFractionDigits : minFractionDigits;
this.groupingUsed = groupingUsed;
this.primaryGroupSize = primaryGroupSize;
this.secondaryGroupSize = secondaryGroupSize;
if (!this.positivePattern || typeof this.positivePattern !== 'string') {
throw new LocalizationException('Invalid positivePattern');
}
if (!this.negativePattern || typeof this.negativePattern !== 'string') {
throw new LocalizationException('Invalid negativePattern');
}
if (!this.symbol || !(this.symbol instanceof NumberSymbol)) {
throw new LocalizationException('Invalid symbol');
}
if (typeof this.maxFractionDigits !== 'number') {
throw new LocalizationException('Invalid maxFractionDigits');
}
if (typeof this.minFractionDigits !== 'number') {
throw new LocalizationException('Invalid minFractionDigits');
}
if (typeof this.groupingUsed !== 'boolean') {
throw new LocalizationException('Invalid groupingUsed');
}
if (typeof this.primaryGroupSize !== 'number') {
throw new LocalizationException('Invalid primaryGroupSize');
}
if (typeof this.secondaryGroupSize !== 'number') {
throw new LocalizationException('Invalid secondaryGroupSize');
}
}
/**
* Get symbol.
*
* @return NumberSymbol
*/
getSymbol() {
return this.symbol;
}
/**
* Get the formatting rules for this number (when positive).
*
* This pattern uses the Unicode CLDR number pattern syntax
*
* @return string
*/
getPositivePattern() {
return this.positivePattern;
}
/**
* Get the formatting rules for this number (when negative).
*
* This pattern uses the Unicode CLDR number pattern syntax
*
* @return string
*/
getNegativePattern() {
return this.negativePattern;
}
/**
* Get the maximum number of digits after decimal separator (rounding if needed).
*
* @return int
*/
getMaxFractionDigits() {
return this.maxFractionDigits;
}
/**
* Get the minimum number of digits after decimal separator (fill with "0" if needed).
*
* @return int
*/
getMinFractionDigits() {
return this.minFractionDigits;
}
/**
* Get the "grouping" flag. This flag defines if digits
* grouping should be used when formatting this number.
*
* @return bool
*/
isGroupingUsed() {
return this.groupingUsed;
}
/**
* Get the size of primary digits group in the number.
*
* @return int
*/
getPrimaryGroupSize() {
return this.primaryGroupSize;
}
/**
* Get the size of secondary digits groups in the number.
*
* @return int
*/
getSecondaryGroupSize() {
return this.secondaryGroupSize;
}
}
export default NumberSpecification;

View File

@@ -0,0 +1,108 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import LocalizationException from '../exception/localization';
import NumberSpecification from './number';
/**
* Currency display option: symbol notation.
*/
const CURRENCY_DISPLAY_SYMBOL = 'symbol';
class PriceSpecification extends NumberSpecification {
/**
* Price specification constructor.
*
* @param string positivePattern CLDR formatting pattern for positive amounts
* @param string negativePattern CLDR formatting pattern for negative amounts
* @param NumberSymbol symbol Number symbol
* @param int maxFractionDigits Maximum number of digits after decimal separator
* @param int minFractionDigits Minimum number of digits after decimal separator
* @param bool groupingUsed Is digits grouping used ?
* @param int primaryGroupSize Size of primary digits group in the number
* @param int secondaryGroupSize Size of secondary digits group in the number
* @param string currencySymbol Currency symbol of this price (eg. : €)
* @param currencyCode Currency code of this price (e.g.: EUR)
*
* @throws LocalizationException
*/
constructor(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
currencySymbol,
currencyCode,
) {
super(
positivePattern,
negativePattern,
symbol,
maxFractionDigits,
minFractionDigits,
groupingUsed,
primaryGroupSize,
secondaryGroupSize,
);
this.currencySymbol = currencySymbol;
this.currencyCode = currencyCode;
if (!this.currencySymbol || typeof this.currencySymbol !== 'string') {
throw new LocalizationException('Invalid currencySymbol');
}
if (!this.currencyCode || typeof this.currencyCode !== 'string') {
throw new LocalizationException('Invalid currencyCode');
}
}
/**
* Get type of display for currency symbol.
*
* @return string
*/
static getCurrencyDisplay() {
return CURRENCY_DISPLAY_SYMBOL;
}
/**
* Get the currency symbol
* e.g.: €.
*
* @return string
*/
getCurrencySymbol() {
return this.currencySymbol;
}
/**
* Get the currency ISO code
* e.g.: EUR.
*
* @return string
*/
getCurrencyCode() {
return this.currencyCode;
}
}
export default PriceSpecification;

View File

@@ -0,0 +1,33 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import refreshSliders from './slider';
import {showOverlay, hideOverlay} from './overlay';
$(document).ready(() => {
prestashop.on('updateProductList', () => {
hideOverlay();
refreshSliders();
});
refreshSliders();
prestashop.on('updateFacets', () => {
showOverlay();
});
});

View File

@@ -0,0 +1,48 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
#search_filters .facet .title {
display: flex;
}
#search_filters .facet .title .collapse-icons {
margin-left: auto;
}
#search_filters .facet .facet-title {
width: calc(100% - 30px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#search_filters .facet .facet-label {
width: 100%;
text-align: left;
}
#search_filters .facet .facet-label .custom-checkbox,
#search_filters .facet .facet-label .custom-radio {
top: -7px;
margin-right: 0;
}
#search_filters .facet .facet-label .color {
margin-left: 0;
}
#search_filters .facet .facet-label a {
width: calc(100% - 30px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}/*# sourceMappingURL=facet.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["facet.scss","facet.css"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;EAAA;AA2BI;EACE,aAAA;ACRN;ADSM;EACE,iBAAA;ACPR;ADWI;EAfF,wBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;ACOF;ADSI;EACE,WAAA;EACA,gBAAA;ACPN;ADQM;;EAEE,SAAA;EACA,eAAA;ACNR;ADQM;EACE,cAAA;ACNR;ADSM;EA/BJ,wBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;ACyBF","file":"facet.css"}

View File

@@ -0,0 +1,56 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
@mixin text-ellipsis() {
width: calc(100% - 30px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
#search_filters {
.facet {
.title {
display: flex;
.collapse-icons {
margin-left: auto;
}
}
.facet-title {
@include text-ellipsis();
}
.facet-label {
width: 100%;
text-align: left;
.custom-checkbox,
.custom-radio {
top: -7px;
margin-right: 0;
}
.color {
margin-left: 0;
}
a {
@include text-ellipsis();
}
}
}
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import 'jquery-ui-touch-punch';
import './events';
import './slider.scss';
import './facet.scss';

View File

@@ -0,0 +1,57 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.faceted-overlay {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: fixed;
background-color: rgba(25, 25, 25, 0.5);
z-index: 100;
}
.faceted-overlay .overlay__inner {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: absolute;
}
.faceted-overlay .overlay__content {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.faceted-overlay .spinner {
width: 75px;
height: 75px;
display: inline-block;
border-width: 2px;
border-color: rgba(255, 255, 255, 0.05);
border-top-color: #fff;
animation: spin 1s infinite linear;
border-radius: 100%;
border-style: solid;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}/*# sourceMappingURL=overlay.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["overlay.scss","overlay.css"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;EAAA;AAkBA;EACE,OAAA;EACA,MAAA;EACA,WAAA;EACA,YAAA;EACA,eAAA;EACA,uCAAA;EACA,YAAA;ACCF;ADCE;EACE,OAAA;EACA,MAAA;EACA,WAAA;EACA,YAAA;EACA,kBAAA;ACCJ;ADEE;EACE,SAAA;EACA,kBAAA;EACA,QAAA;EACA,gCAAA;ACAJ;ADGE;EACE,WAAA;EACA,YAAA;EACA,qBAAA;EACA,iBAAA;EACA,uCAAA;EACA,sBAAA;EACA,kCAAA;EACA,mBAAA;EACA,mBAAA;ACDJ;;ADKA;EACE;IACE,yBAAA;ECFF;AACF","file":"overlay.css"}

View File

@@ -0,0 +1,43 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import './overlay.scss';
const template = `<div class="faceted-overlay">
<div class="overlay__inner">
<div class="overlay__content"><span class="spinner"></span></div>
</div>
</div>`;
function show() {
if ($('.faceted-overlay').length === 1) {
return;
}
$('body').append(template);
}
function hide() {
$('.faceted-overlay').remove();
}
export {
show as showOverlay,
hide as hideOverlay,
};

View File

@@ -0,0 +1,60 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
.faceted-overlay {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: fixed;
background-color: rgba(25, 25, 25, 0.5);
z-index: 100;
.overlay__inner {
left: 0;
top: 0;
width: 100%;
height: 100%;
position: absolute;
}
.overlay__content {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
}
.spinner {
width: 75px;
height: 75px;
display: inline-block;
border-width: 2px;
border-color: rgba(255, 255, 255, 0.05);
border-top-color: #fff;
animation: spin 1s infinite linear;
border-radius: 100%;
border-style: solid;
}
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
#search_filters .ui-slider-horizontal .ui-slider-handle {
margin-left: -1px;
cursor: pointer;
}
#search_filters .ui-widget-header {
background: #555;
}
#search_filters .ui-slider .ui-slider-handle {
top: -0.45em;
width: 0.4em;
background: #fff;
border: 1px solid #555;
}
#search_filters .ui-slider-horizontal {
height: 0.4em;
}/*# sourceMappingURL=slider.css.map */

View File

@@ -0,0 +1 @@
{"version":3,"sources":["slider.scss","slider.css"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;EAAA;AAoBI;EACE,iBAAA;EACA,eAAA;ACDN;ADIE;EACE,gBAAA;ACFJ;ADKI;EACE,YAAA;EACA,YAAA;EACA,gBAAA;EACA,sBAAA;ACHN;ADME;EACE,aAAA;ACJJ","file":"slider.css"}

View File

@@ -0,0 +1,129 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
import getQueryParameters from './urlparser';
import NumberFormatter from '../cldr/number-formatter';
const formatters = {};
const displayLabelBlock = (formatterId, displayBlock, min, max) => {
if (formatters[formatterId] === undefined) {
displayBlock.text(
displayBlock.text().replace(
/([^\d]*)(?:[\d\s.,]+)([^\d]+)(?:[\d\s.,]+)(.*)/,
`$1${min}$2${max}$3`,
),
);
} else {
displayBlock.text(
`${formatters[formatterId].format(min)} - ${formatters[formatterId].format(max)}`,
);
}
};
/**
* Refresh facets sliders
*/
const refreshSliders = () => {
$('.faceted-slider').each(function initializeSliders() {
const $el = $(this);
const values = $el.data('slider-values');
const specifications = $el.data('slider-specifications');
if (specifications !== null && specifications !== undefined) {
formatters[$el.data('slider-id')] = NumberFormatter.build(specifications);
}
displayLabelBlock(
$el.data('slider-id'),
$(`#facet_label_${$el.data('slider-id')}`),
values === null ? $el.data('slider-min') : values[0],
values === null ? $el.data('slider-max') : values[1],
);
$(`#slider-range_${$el.data('slider-id')}`).slider({
range: true,
min: $el.data('slider-min'),
max: $el.data('slider-max'),
values: [
values === null ? $el.data('slider-min') : values[0],
values === null ? $el.data('slider-max') : values[1],
],
stop(event, ui) {
const nextEncodedFacetsURL = $el.data('slider-encoded-url');
const urlsSplitted = nextEncodedFacetsURL.split('?');
let queryParams = [];
// Retrieve parameters if exists
if (urlsSplitted.length > 1) {
queryParams = getQueryParameters(urlsSplitted[1]);
}
let found = false;
queryParams.forEach((query) => {
if (query.name === 'q') {
found = true;
}
});
if (!found) {
queryParams.push({name: 'q', value: ''});
}
// Update query parameter
queryParams.forEach((query) => {
if (query.name === 'q') {
// eslint-disable-next-line
query.value += [
query.value.length > 0 ? '/' : '',
$el.data('slider-label'),
'-',
$el.data('slider-unit'),
'-',
ui.values[0],
'-',
ui.values[1],
].join('');
}
});
const requestUrl = [
urlsSplitted[0],
'?',
$.param(queryParams),
].join('');
prestashop.emit(
'updateFacets',
requestUrl,
);
},
slide(event, ui) {
displayLabelBlock(
$el.data('slider-id'),
$(`#facet_label_${$el.data('slider-id')}`),
ui.values[0],
ui.values[1],
);
},
});
});
};
export default refreshSliders;

View File

@@ -0,0 +1,40 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
#search_filters {
.ui-slider-horizontal {
.ui-slider-handle {
margin-left: -1px;
cursor: pointer;
}
}
.ui-widget-header {
background: #555;
}
.ui-slider {
.ui-slider-handle {
top: -.45em;
width: 0.4em;
background: #fff;
border: 1px solid #555;
}
}
.ui-slider-horizontal {
height: .4em;
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
const getQueryParameters = (params) => params.split('&').map((str) => {
const [key, val] = str.split('=');
return {
name: key,
value: decodeURIComponent(val).replace(/\+/g, ' '),
};
});
export default getQueryParameters;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../');
exit;

3104
modules/ps_facetedsearch/composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ps_facetedsearch</name>
<displayName><![CDATA[Faceted search]]></displayName>
<version><![CDATA[3.8.0]]></version>
<description><![CDATA[Displays a block allowing multiple filters.]]></description>
<author><![CDATA[PrestaShop]]></author>
<tab><![CDATA[front_office_features]]></tab>
<is_configurable>1</is_configurable>
<need_instance>0</need_instance>
<limited_countries></limited_countries>
</module>

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,10 @@
services:
_defaults:
public: true
prestashop.module.ps_facetedsearch.constraint.url_segment_validator:
class: PrestaShop\Module\FacetedSearch\Constraint\UrlSegmentValidator
arguments:
- '@prestashop.adapter.tools'
tags:
- { name: validator.constraint_validator }

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8" ?>
<module>
<name>ps_facetedsearch</name>
<displayName><![CDATA[Nawigacja fasetowa]]></displayName>
<version><![CDATA[3.8.0]]></version>
<description><![CDATA[Filter your catalog to help visitors picture the category tree and browse your store easily.]]></description>
<author><![CDATA[PrestaShop]]></author>
<tab><![CDATA[front_office_features]]></tab>
<is_configurable>1</is_configurable>
<need_instance>0</need_instance>
<limited_countries></limited_countries>
</module>

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 B

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../');
exit;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

23925
modules/ps_facetedsearch/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::hash('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
exit('Bad token');
}
Shop::setContext(Shop::CONTEXT_ALL);
$psFacetedsearch = new Ps_Facetedsearch();
$psFacetedsearch->indexAttributes();
$psFacetedsearch->indexFeatures();
$psFacetedsearch->indexAttributeGroup();
echo 1;

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::hash('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
exit('Bad token');
}
$psFacetedsearch = new Ps_Facetedsearch();
echo $psFacetedsearch->invalidateLayeredFilterBlockCache();

View File

@@ -0,0 +1,34 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
require_once __DIR__ . '/../../config/config.inc.php';
require_once __DIR__ . '/ps_facetedsearch.php';
if (substr(Tools::hash('ps_facetedsearch/index'), 0, 10) != Tools::getValue('token') || !Module::isInstalled('ps_facetedsearch')) {
exit('Bad token');
}
Shop::setContext(Shop::CONTEXT_ALL);
$module = new Ps_Facetedsearch();
if (Tools::getValue('full')) {
echo $module->fullPricesIndexProcess((int) Tools::getValue('cursor'), (bool) Tools::getValue('ajax'), true);
} else {
echo $module->pricesIndexProcess((int) Tools::getValue('cursor'), (bool) Tools::getValue('ajax'));
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{if isset($listing.rendered_active_filters)}
{$listing.rendered_active_filters nofilter}
{/if}
{if isset($listing.rendered_facets)}
{$listing.rendered_facets nofilter}
{/if}

View File

@@ -0,0 +1,328 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Doctrine\Common\Collections\ArrayCollection;
abstract class AbstractAdapter implements InterfaceAdapter
{
/**
* @var ArrayCollection
*/
protected $filters;
/**
* @var ArrayCollection
*/
protected $operationsFilters;
/**
* @var ArrayCollection
*/
protected $selectFields;
/**
* @var ArrayCollection
*/
protected $groupFields;
protected $orderField = 'id_product';
protected $orderDirection = 'DESC';
protected $limit = 20;
protected $offset = 0;
/** @var InterfaceAdapter */
protected $initialPopulation = null;
public function __construct()
{
$this->groupFields = new ArrayCollection();
$this->selectFields = new ArrayCollection();
$this->filters = new ArrayCollection();
$this->operationsFilters = new ArrayCollection();
}
public function __clone()
{
$this->filters = clone $this->filters;
$this->operationsFilters = clone $this->operationsFilters;
$this->groupFields = clone $this->groupFields;
$this->selectFields = clone $this->selectFields;
}
/**
* {@inheritdoc}
*/
public function getInitialPopulation()
{
return $this->initialPopulation;
}
/**
* {@inheritdoc}
*/
public function resetFilter($filterName)
{
if ($this->filters->offsetExists($filterName)) {
$this->filters->offsetUnset($filterName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function resetOperationsFilter($filterName)
{
if ($this->operationsFilters->offsetExists($filterName)) {
$this->operationsFilters->offsetUnset($filterName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function resetOperationsFilters()
{
$this->operationsFilters = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function resetAll()
{
$this->selectFields = new ArrayCollection();
$this->groupFields = new ArrayCollection();
$this->filters = new ArrayCollection();
$this->operationsFilters = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function getFilter($filterName)
{
if (isset($this->filters[$filterName])) {
return $this->filters[$filterName];
}
return null;
}
/**
* {@inheritdoc}
*/
public function getOrderDirection()
{
return $this->orderDirection;
}
/**
* {@inheritdoc}
*/
public function getOrderField()
{
return $this->orderField;
}
/**
* {@inheritdoc}
*/
public function getGroupFields()
{
return $this->groupFields;
}
/**
* {@inheritdoc}
*/
public function getSelectFields()
{
return $this->selectFields;
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return $this->filters;
}
/**
* {@inheritdoc}
*/
public function getOperationsFilters()
{
return $this->operationsFilters;
}
/**
* {@inheritdoc}
*/
public function copyFilters(InterfaceAdapter $adapter)
{
$this->filters = clone $adapter->getFilters();
$this->operationsFilters = clone $adapter->getOperationsFilters();
}
/**
* {@inheritdoc}
*/
public function addFilter($filterName, $values, $operator = '=')
{
$filters = $this->filters->get($filterName);
if (!isset($filters[$operator])) {
$filters[$operator] = [];
}
$filters[$operator][] = $values;
$this->filters->set($filterName, $filters);
return $this;
}
/**
* {@inheritdoc}
*/
public function addOperationsFilter($filterName, array $operations = [])
{
$this->operationsFilters->set($filterName, $operations);
return $this;
}
/**
* {@inheritdoc}
*/
public function addSelectField($fieldName)
{
if (!$this->selectFields->contains($fieldName)) {
$this->selectFields->add($fieldName);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setSelectFields($selectFields)
{
$this->selectFields = new ArrayCollection($selectFields);
return $this;
}
/**
* {@inheritdoc}
*/
public function resetSelectField()
{
$this->selectFields = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function addGroupBy($groupField)
{
$this->groupFields->add($groupField);
return $this;
}
/**
* {@inheritdoc}
*/
public function setGroupFields($groupFields)
{
$this->groupFields = new ArrayCollection($groupFields);
return $this;
}
/**
* {@inheritdoc}
*/
public function resetGroupBy()
{
$this->groupFields = new ArrayCollection();
return $this;
}
/**
* {@inheritdoc}
*/
public function setFilter($filterName, $value)
{
if ($value !== null) {
$this->filters->set($filterName, $value);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setOrderField($fieldName)
{
$this->orderField = $fieldName;
return $this;
}
/**
* {@inheritdoc}
*/
public function setOrderDirection($direction)
{
$this->orderDirection = $direction === 'desc' ? 'desc' : 'asc';
return $this;
}
/**
* {@inheritdoc}
*/
public function setLimit($limit, $offset = 0)
{
$this->limit = $limit ? (int) $limit : null;
$this->offset = (int) $offset;
return $this;
}
}

View File

@@ -0,0 +1,294 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
interface InterfaceAdapter
{
/**
* Set order by field
*
* @param string $fieldName
*
* @return self
*/
public function setOrderField($fieldName);
/**
* Set the order by direction for the given field
*
* @param string $direction
*
* @return self
*/
public function setOrderDirection($direction);
/**
* Set the limit and offset associated with the current search
*
* @param int|null $limit
* @param int $offset
*
* @return self
*/
public function setLimit($limit, $offset = 0);
/**
* Execute the search
*
* @return mixed
*/
public function execute();
/**
* Get the current query
*
* @return string
*/
public function getQuery();
/**
* Get the min & max value of the field filedName associated with the current search
*
* @param string $fieldName
*
* @return mixed
*/
public function getMinMaxValue($fieldName);
/**
* Get the min & max value of the price associated with the current search
*
* @return array
*/
public function getMinMaxPriceValue();
/**
* Return order direction associated with the current search
*
* @return mixed
*/
public function getOrderDirection();
/**
* Return order field associated with the current search
*
* @return mixed
*/
public function getOrderField();
/**
* Return all group fields associated with the current search
*
* @return mixed
*/
public function getGroupFields();
/**
* Return all selected fields associated with the current search
*
* @return mixed
*/
public function getSelectFields();
/**
* Return all the filters associated with the current search
*
* @return mixed
*/
public function getFilters();
/**
* Return all the operations filters associated with the current search
*
* @return mixed
*/
public function getOperationsFilters();
/**
* Return the number of results associated for the current search
*
* @return int
*/
public function count();
/**
* Move the current search into the "initialPopulation"
* This initialPopulation will be used to generate the first derived table 'FROM (SELECT ...)' in the final query
* e.g. : SELECT ... FROM (initialPopulation) p JOIN ....
*/
public function useFiltersAsInitialPopulation();
/**
* Create a new SearchAdapter, keeping the initialPopulation of the current Search
*
* @param string $resetFilter reset this filter inside the initialPopulation
* @param bool $skipInitialPopulation if enable, do not copy the initialPopulation filter
*
* @return InterfaceAdapter
*/
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false);
/**
* Add a new filter with filterName, operator & values to the current search
* If several values are provided with the = operator, it's converted automatically to a IN () in the final query
*
* @param string $filterName
* @param array $values
* @param string $operator
*
* @return self
*/
public function addFilter($filterName, $values, $operator = '=');
/**
* Add a stack of operations with filterName. Operations must contains filterName, values and to the current search
*
* @param string $filterName
* @param array $operations
*
* @return self
*/
public function addOperationsFilter($filterName, array $operations);
/**
* Add fieldName in the current search result
*
* @param string $fieldName
*
* @return self
*/
public function addSelectField($fieldName);
/**
* Returns the number of distinct products, group by fieldName values
*
* @param string $fieldName
*
* @return mixed
*/
public function valueCount($fieldName = null);
/**
* Reset the operations filters
*
* @return self
*/
public function resetOperationsFilters();
/**
* Reset the operations filter for the given filterName
*
* @param string $filterName
*
* @return self
*/
public function resetOperationsFilter($filterName);
/**
* Reset the filter for the given filterName
*
* @param string $filterName
*
* @return self
*/
public function resetFilter($filterName);
/**
* Return the filter associated with filterName
*
* @param string $filterName
*
* @return mixed
*/
public function getFilter($filterName);
/**
* Set the filterName to the given array value
*
* @param string $filterName
* @param mixed $value
*
* @return mixed
*/
public function setFilter($filterName, $value);
/**
* Return the current initialPopulation
*
* @return self|null
*/
public function getInitialPopulation();
/**
* Return all the filters / groupFields / selectFields
*
* @return self
*/
public function resetAll();
/**
* Copy all the filters & operationsFilters from adapter to the current search
*
* @param InterfaceAdapter $adapter
*/
public function copyFilters(InterfaceAdapter $adapter);
/**
* Set all the select fields
*
* @param array $selectFields
*
* @return self
*/
public function setSelectFields($selectFields);
/**
* Reset all the select fields
*
* @return self
*/
public function resetSelectField();
/**
* Add a group by field
*
* @param string $groupField
*
* @return self
*/
public function addGroupBy($groupField);
/**
* Set the group by fields
*
* @param array $groupFields
*
* @return self
*/
public function setGroupFields($groupFields);
/**
* Reset the group by conditions
*
* @return self
*/
public function resetGroupBy();
}

View File

@@ -0,0 +1,815 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Configuration;
use Context;
use Db;
use Doctrine\Common\Collections\ArrayCollection;
use Product;
use StockAvailable;
class MySQL extends AbstractAdapter
{
/**
* @var string
*/
const TYPE = 'MySQL';
/**
* @var string
*/
const LEFT_JOIN = 'LEFT JOIN';
/**
* @var string
*/
const INNER_JOIN = 'INNER JOIN';
/**
* {@inheritdoc}
*/
public function getMinMaxPriceValue()
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$mysqlAdapter->setSelectFields(['price_min', 'MIN(price_min) as min, MAX(price_max) as max']);
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return [floor((float) $result[0]['min']), ceil((float) $result[0]['max'])];
}
/**
* {@inheritdoc}
*/
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false)
{
$mysqlAdapter = new self();
if ($this->getInitialPopulation() !== null && !$skipInitialPopulation) {
$mysqlAdapter->initialPopulation = clone $this->getInitialPopulation();
if ($resetFilter) {
// Try to reset filter & operations filter
$mysqlAdapter->initialPopulation->resetFilter($resetFilter);
$mysqlAdapter->initialPopulation->resetOperationsFilter($resetFilter);
}
}
return $mysqlAdapter;
}
/**
* {@inheritdoc}
*/
public function execute()
{
return $this->getDatabase()->executeS($this->getQuery());
}
/**
* Construct the final sql query
*
* @return string
*/
public function getQuery()
{
$filterToTableMapping = $this->getFieldMapping();
$orderField = $this->computeOrderByField($filterToTableMapping);
if ($this->getInitialPopulation() === null) {
$referenceTable = _DB_PREFIX_ . 'product';
} else {
$referenceTable = '(' . $this->getInitialPopulation()->getQuery() . ')';
}
$query = 'SELECT ';
$selectFields = $this->computeSelectFields($filterToTableMapping);
$whereConditions = $this->computeWhereConditions($filterToTableMapping);
$joinConditions = $this->computeJoinConditions($filterToTableMapping);
$groupFields = $this->computeGroupByFields($filterToTableMapping);
$query .= implode(', ', $selectFields) . ' FROM ' . $referenceTable . ' p';
foreach ($joinConditions as $joinAliasInfos) {
foreach ($joinAliasInfos as $tableAlias => $joinInfos) {
$query .= ' ' . $joinInfos['joinType'] . ' ' . _DB_PREFIX_ . $joinInfos['tableName'] . ' ' .
$tableAlias . ' ON ' . $joinInfos['joinCondition'];
}
}
if (!empty($whereConditions)) {
$query .= ' WHERE ' . implode(' AND ', $whereConditions);
}
if ($groupFields) {
$query .= ' GROUP BY ' . implode(', ', $groupFields);
}
if ($orderField) {
$query .= ' ORDER BY ' . $orderField . ' ' . strtoupper($this->getOrderDirection());
if ($orderField !== 'p.id_product') {
$query .= ', p.id_product DESC';
}
}
if ($this->limit !== null) {
$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
}
return $query;
}
/**
* Define the mapping between fields and tables
*
* @return array
*/
protected function getFieldMapping()
{
$stockCondition = StockAvailable::addSqlShopRestriction(
null,
null,
'sa'
);
$filterToTableMapping = [
'id_product_attribute' => [
'tableName' => 'product_attribute',
'tableAlias' => 'pa',
'joinCondition' => '(p.id_product = pa.id_product)',
'joinType' => self::LEFT_JOIN,
],
'id_attribute' => [
'tableName' => 'product_attribute_combination',
'tableAlias' => 'pac',
'joinCondition' => '(pa.id_product_attribute = pac.id_product_attribute)',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_product_attribute',
],
'id_attribute_group' => [
'tableName' => 'attribute',
'tableAlias' => 'a',
'joinCondition' => '(a.id_attribute = pac.id_attribute)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_attribute',
],
'id_feature' => [
'tableName' => 'feature_product',
'tableAlias' => 'fp',
'joinCondition' => '(p.id_product = fp.id_product)',
'joinType' => self::INNER_JOIN,
],
'id_shop' => [
'tableName' => 'product_shop',
'tableAlias' => 'ps',
'joinCondition' => '(p.id_product = ps.id_product AND ps.id_shop = ' .
$this->getContext()->shop->id . ' AND ps.active = TRUE)',
'joinType' => self::INNER_JOIN,
],
'id_feature_value' => [
'tableName' => 'feature_product',
'tableAlias' => 'fp',
'joinCondition' => '(p.id_product = fp.id_product)',
'joinType' => self::LEFT_JOIN,
],
'id_category' => [
'tableName' => 'category_product',
'tableAlias' => 'cp',
'joinCondition' => '(p.id_product = cp.id_product)',
'joinType' => self::INNER_JOIN,
],
'position' => [
'tableName' => 'category_product',
'tableAlias' => 'cp',
'joinCondition' => '(p.id_product = cp.id_product)',
'joinType' => self::INNER_JOIN,
],
'manufacturer_name' => [
'tableName' => 'manufacturer',
'tableAlias' => 'm',
'fieldName' => 'name',
'joinCondition' => '(p.id_manufacturer = m.id_manufacturer)',
'joinType' => self::INNER_JOIN,
],
'name' => [
'tableName' => 'product_lang',
'tableAlias' => 'pl',
'joinCondition' => '(p.id_product = pl.id_product AND pl.id_shop = ' .
$this->getContext()->shop->id . ' AND pl.id_lang = ' . $this->getContext()->language->id . ')',
'joinType' => self::INNER_JOIN,
],
'nleft' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'nright' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'level_depth' => [
'tableName' => 'category',
'tableAlias' => 'c',
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
'joinType' => self::INNER_JOIN,
'dependencyField' => 'id_category',
],
'out_of_stock' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_attribute',
],
'quantity' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'id_attribute',
'aggregateFunction' => 'SUM',
'aggregateFieldName' => 'quantity',
],
'price_min' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'price_max' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'range_start' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'range_end' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
'joinType' => self::INNER_JOIN,
],
'id_group' => [
'tableName' => 'category_group',
'tableAlias' => 'cg',
'joinCondition' => '(cg.id_category = c.id_category)',
'joinType' => self::LEFT_JOIN,
'dependencyField' => 'nleft',
],
'sales' => [
'tableName' => 'product_sale',
'tableAlias' => 'psales',
'fieldName' => 'quantity',
'fieldAlias' => 'sales',
'joinCondition' => '(psales.id_product = p.id_product)',
'joinType' => self::LEFT_JOIN,
],
];
return $filterToTableMapping;
}
/**
* Get the joined and escaped value from an multi-dimensional array
*
* @param string $separator
* @param array $values
*
* @return string Escaped string value
*/
protected function getJoinedEscapedValue($separator, array $values)
{
foreach ($values as $key => $value) {
if (is_array($value)) {
$values[$key] = $this->getJoinedEscapedValue($separator, $value);
} elseif (is_numeric($value)) {
$values[$key] = pSQL($value);
} else {
$values[$key] = "'" . pSQL($value) . "'";
}
}
return implode($separator, $values);
}
/**
* Compute the orderby fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return string
*/
protected function computeOrderByField(array $filterToTableMapping)
{
$orderField = $this->getOrderField();
if ($this->getInitialPopulation() !== null && !empty($orderField)) {
$this->getInitialPopulation()->addSelectField($orderField);
}
// do not try to process the orderField if it already has an alias, or if it's a group function
if (empty($orderField) || strpos($orderField, '.') !== false
|| strpos($orderField, '(') !== false) {
return $orderField;
}
if ($orderField === 'price') {
$orderField = $this->getOrderDirection() === 'asc' ? 'price_min' : 'price_max';
}
$orderField = $this->computeFieldName($orderField, $filterToTableMapping, true);
// put some products at the end of the list
$orderField = $this->computeShowLast($orderField, $filterToTableMapping);
return $orderField;
}
/**
* Sort product list: InStock, OOPS with qty 0, OutOfStock
*
* @param string $orderField
* @param array $filterToTableMapping
*
* @return string
*/
protected function computeShowLast($orderField, $filterToTableMapping)
{
// allow only if feature is enabled & it is main product list query
if ($this->getInitialPopulation() === null
|| empty($orderField)
|| !Configuration::get('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST')
) {
return $orderField;
}
$this->addSelectField('out_of_stock');
// order by out-of-stock last
$computedQuantityField = $this->computeFieldName('quantity', $filterToTableMapping);
$byOutOfStockLast = 'IFNULL(' . $computedQuantityField . ', 0) <= 0';
/**
* Default behaviour when out of stock
* 0 - when deny orders
* 1 - when allow orders
*
* @var int
*/
$isAvailableWhenOutOfStock = (int) Product::isAvailableWhenOutOfStock(2);
// computing values for order by 'allow to order last'
$computedField = $this->computeFieldName('out_of_stock', $filterToTableMapping);
$computedValue = $isAvailableWhenOutOfStock ? 0 : 1;
$computedDirection = $isAvailableWhenOutOfStock ? 'ASC' : 'DESC';
// query: products with zero or less quantity and not available to order go to the end
$byOOPS = str_replace(
[':byOutOfStockLast', ':field', ':value', ':direction'],
[$byOutOfStockLast, $computedField, $computedValue, $computedDirection],
':byOutOfStockLast AND FIELD(:field, :value) :direction'
);
$orderField = $byOutOfStockLast . ', '
. $byOOPS . ', '
. $orderField;
return $orderField;
}
/**
* Add alias to table field name
*
* @param string $fieldName
* @param array $filterToTableMapping
*
* @return string Table Field name with an alias
*/
protected function computeFieldName($fieldName, $filterToTableMapping, $sortByField = false)
{
if (array_key_exists($fieldName, $filterToTableMapping)
&& (
// If the requested order field is in the result, no need to change tableAlias
// unless a fieldName key exists
isset($filterToTableMapping[$fieldName]['fieldName'])
|| $this->getInitialPopulation() === null
|| !$this->getInitialPopulation()->getSelectFields()->contains($fieldName)
)
) {
$joinMapping = $filterToTableMapping[$fieldName];
$fieldName = $joinMapping['tableAlias'] . '.' . (isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $fieldName);
if ($sortByField === false) {
$fieldName .= isset($joinMapping['fieldAlias']) ? ' as ' . $joinMapping['fieldAlias'] : '';
}
if (isset($joinMapping['aggregateFunction'], $joinMapping['aggregateFieldName'])) {
$fieldName = $joinMapping['aggregateFunction'] . '(' . $fieldName . ') as ' . $joinMapping['aggregateFieldName'];
}
} else {
if (strpos($fieldName, '(') === false) {
$fieldName = 'p.' . $fieldName;
}
}
return $fieldName;
}
/**
* Compute the select fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
protected function computeSelectFields(array $filterToTableMapping)
{
$selectFields = [];
foreach ($this->getSelectFields() as $key => $selectField) {
$selectFields[] = $this->computeFieldName($selectField, $filterToTableMapping);
}
return $selectFields;
}
/**
* Computer the where conditions that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
protected function computeWhereConditions(array $filterToTableMapping)
{
$whereConditions = [];
$operationIdx = 0;
foreach ($this->getOperationsFilters() as $filterName => $filterOperations) {
$operationsConditions = [];
foreach ($filterOperations as $operations) {
$conditions = [];
foreach ($operations as $idx => $operation) {
$selectAlias = 'p';
$values = $operation[1];
if (array_key_exists($operation[0], $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$operation[0]];
// If index is not the first, append to the table alias for
// multi join
$selectAlias = $joinMapping['tableAlias'] .
($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx);
$operation[0] = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $operation[0];
}
if (count($values) === 1) {
$operator = !empty($operation[2]) ? $operation[2] : '=';
$conditions[] = $selectAlias . '.' . $operation[0] . $operator . current($values);
} else {
$conditions[] = $selectAlias . '.' . $operation[0] . ' IN (' . $this->getJoinedEscapedValue(', ', $values) . ')';
}
}
$operationsConditions[] = '(' . implode(' AND ', $conditions) . ')';
}
++$operationIdx;
if (!empty($operationsConditions)) {
$whereConditions[] = '(' . implode(' OR ', $operationsConditions) . ')';
}
}
foreach ($this->getFilters() as $filterName => $filterContent) {
$selectAlias = 'p';
if (array_key_exists($filterName, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$filterName];
$selectAlias = $joinMapping['tableAlias'];
$filterName = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $filterName;
}
foreach ($filterContent as $operator => $values) {
if (count($values) == 1) {
$values = current($values);
if ($operator === '=') {
if (count($values) == 1) {
$whereConditions[] =
$selectAlias . '.' . $filterName . $operator . "'" . current($values) . "'";
} else {
$whereConditions[] =
$selectAlias . '.' . $filterName . ' IN (' . $this->getJoinedEscapedValue(', ', $values) . ')';
}
} else {
$orConditions = [];
foreach ($values as $value) {
$orConditions[] = $selectAlias . '.' . $filterName . $operator . $value;
}
$whereConditions[] = implode(' OR ', $orConditions);
}
}
}
}
// if we have several "groups" of the same filter, we need to use the intersect of the matching products
// e.g. : mix of id_feature like Composition & Styles
$idFilteredProducts = null;
foreach ($this->getFilters() as $filterName => $filterContent) {
foreach ($filterContent as $operator => $filterValues) {
if (count($filterValues) <= 1) {
continue;
}
$idTmpFilteredProducts = [];
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->addSelectField('id_product');
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$mysqlAdapter->addFilter($filterName, $filterValues, $operator);
$idProducts = $mysqlAdapter->execute();
foreach ($idProducts as $idProduct) {
$idTmpFilteredProducts[] = $idProduct['id_product'];
}
if ($idFilteredProducts === null) {
$idFilteredProducts = $idTmpFilteredProducts;
} else {
$idFilteredProducts += array_intersect($idFilteredProducts, $idTmpFilteredProducts);
}
if (empty($idFilteredProducts)) {
// set it to 0 to make sure no result will be returned
$idFilteredProducts[] = 0;
break;
}
$whereConditions[] = 'p.id_product IN (' . implode(', ', $idFilteredProducts) . ')';
}
}
return $whereConditions;
}
/**
* Compute the joinConditions needed depending on the fields required in select, where, groupby & orderby fields
*
* @param array $filterToTableMapping
*
* @return ArrayCollection
*/
protected function computeJoinConditions(array $filterToTableMapping)
{
$joinList = new ArrayCollection();
$this->addJoinList($joinList, $this->getSelectFields(), $filterToTableMapping);
$this->addJoinList($joinList, $this->getFilters()->getKeys(), $filterToTableMapping);
$operationIdx = 0;
foreach ($this->getOperationsFilters() as $filterOperations) {
foreach ($filterOperations as $operations) {
foreach ($operations as $idx => $operation) {
if (array_key_exists($operation[0], $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$operation[0]];
if ($idx !== 0 || $operationIdx !== 0) {
// Index is not the first, append index to tableAlias on joinCondition
$joinMapping['joinCondition'] = preg_replace(
'~([\(\s=]' . $joinMapping['tableAlias'] . ')\.~',
'${1}' .
($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx) .
'.',
$joinMapping['joinCondition']
);
$joinMapping['tableAlias'] .= ($operationIdx === 0 ? '' : '_' . $operationIdx) .
($idx === 0 ? '' : '_' . $idx);
}
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
++$operationIdx;
}
$this->addJoinList($joinList, $this->getGroupFields()->getKeys(), $filterToTableMapping);
if (array_key_exists($this->getOrderField(), $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$this->getOrderField()];
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
return $joinList;
}
/**
* Helper to add tables infos to the join list.
*
* @param ArrayCollection $joinList
* @param array|ArrayCollection $list
* @param array $filterToTableMapping
*/
private function addJoinList(ArrayCollection $joinList, $list, array $filterToTableMapping)
{
foreach ($list as $field) {
if (array_key_exists($field, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$field];
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
/**
* Add the required table infos to the join list, taking care of the dependent tables
*
* @param ArrayCollection $joinList
* @param array $joinMapping
* @param array $filterToTableMapping
*/
private function addJoinConditions(ArrayCollection $joinList, array $joinMapping, array $filterToTableMapping)
{
if (array_key_exists('dependencyField', $joinMapping)) {
$dependencyJoinMapping = $filterToTableMapping[$joinMapping['dependencyField']];
$this->addJoinConditions($joinList, $dependencyJoinMapping, $filterToTableMapping);
}
$joinInfos[$joinMapping['tableAlias']] = [
'tableName' => $joinMapping['tableName'],
'joinCondition' => $joinMapping['joinCondition'],
'joinType' => $joinMapping['joinType'],
];
$joinList->set($joinMapping['tableAlias'] . '_' . $joinMapping['tableName'], $joinInfos);
}
/**
* Compute the groupby condition, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
private function computeGroupByFields(array $filterToTableMapping)
{
$groupFields = [];
if ($this->getGroupFields()->isEmpty()) {
return $groupFields;
}
foreach ($this->getGroupFields() as $key => $values) {
if (strpos($values, '.') !== false
|| strpos($values, '(') !== false) {
$groupFields[$key] = $values;
continue;
}
if (array_key_exists($values, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$values];
$groupFields[$key] = $joinMapping['tableAlias'] . '.' . $values;
} else {
$groupFields[$key] = 'p.' . $values;
}
}
return $groupFields;
}
/**
* {@inheritdoc}
*/
public function getMinMaxValue($fieldName)
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$mysqlAdapter->setSelectFields(['MIN(' . $fieldName . ') as min, MAX(' . $fieldName . ') as max']);
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return [(float) $result[0]['min'], (float) $result[0]['max']];
}
/**
* {@inheritdoc}
*/
public function count()
{
$mysqlAdapter = $this->getFilteredSearchAdapter();
$mysqlAdapter->copyFilters($this);
$result = $mysqlAdapter->valueCount();
return isset($result[0]['c']) ? (int) $result[0]['c'] : 0;
}
/**
* {@inheritdoc}
*/
public function valueCount($fieldName = null)
{
$this->resetGroupBy();
if ($fieldName !== null) {
$this->addGroupBy($fieldName);
$this->addSelectField($fieldName);
}
$this->addSelectField('COUNT(DISTINCT p.id_product) c');
$this->setLimit(null);
$this->setOrderField('');
$this->copyOperationsFilters();
return $this->execute();
}
/**
* {@inheritdoc}
*/
public function useFiltersAsInitialPopulation()
{
$this->setLimit(null);
$this->setOrderField('');
$this->setSelectFields(
[
'id_product',
'id_manufacturer',
'quantity',
'condition',
'weight',
'price',
'sales',
]
);
$this->initialPopulation = clone $this;
$this->resetAll();
$this->addSelectField('id_product');
}
/**
* @return Context
*/
protected function getContext()
{
return Context::getContext();
}
/**
* @return Db
*/
protected function getDatabase()
{
return Db::getInstance();
}
/**
* Copy stock management operation filters
* to make sure quantity is also used
*/
protected function copyOperationsFilters()
{
$initialPopulation = $this->getInitialPopulation();
if (null === $initialPopulation) {
return;
}
$operationsFilters = clone $initialPopulation->getOperationsFilters();
foreach ($operationsFilters as $operationName => $operations) {
$this->addOperationsFilter(
$operationName,
$operations
);
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Constraint;
use Symfony\Component\Validator\Constraint;
class UrlSegment extends Constraint
{
public $message = '%s is invalid.';
/**
* {@inheritdoc}
*/
public function validatedBy()
{
return UrlSegmentValidator::class;
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Constraint;
use PrestaShop\PrestaShop\Adapter\Tools;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* Class UrlSegmentValidator responsible for validating an URL segment.
*/
class UrlSegmentValidator extends ConstraintValidator
{
/**
* @var Tools
*/
private $tools;
/**
* @param Tools $tools
*/
public function __construct(Tools $tools)
{
$this->tools = $tools;
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint)
{
if (!$constraint instanceof UrlSegment) {
throw new UnexpectedTypeException($constraint, UrlSegment::class);
}
if (null === $value || '' === $value) {
return;
}
if (strtolower($value) !== $this->tools->linkRewrite($value)) {
$this->context->buildViolation($constraint->message)
->setTranslationDomain('Admin.Notifications.Error')
->setParameter('%s', $this->formatValue($value))
->addViolation()
;
}
}
}

View File

@@ -0,0 +1,982 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Category;
use Configuration;
use Context;
use Db;
use Feature;
use Group;
use Manufacturer;
use PrestaShop\Module\FacetedSearch\Adapter\InterfaceAdapter;
use PrestaShop\Module\FacetedSearch\Product\Search;
use PrestaShop\PrestaShop\Core\Localization\Locale;
use PrestaShop\PrestaShop\Core\Localization\Specification\NumberSymbolList;
use PrestaShopDatabaseException;
use Tools;
/**
* Display filters block on navigation
*/
class Block
{
/**
* @var InterfaceAdapter
*/
private $searchAdapter;
/**
* @var bool
*/
private $psStockManagement;
/**
* @var bool
*/
private $psOrderOutOfStock;
/**
* @var Context
*/
private $context;
/**
* @var Db
*/
private $database;
/**
* @var array
*/
private $attributesGroup;
/**
* @var DataAccessor
*/
private $dataAccessor;
public function __construct(InterfaceAdapter $searchAdapter, Context $context, Db $database, DataAccessor $dataAccessor)
{
$this->searchAdapter = $searchAdapter;
$this->context = $context;
$this->database = $database;
$this->dataAccessor = $dataAccessor;
}
/**
* @param int $nbProducts
* @param array $selectedFilters
*
* @return array
*/
public function getFilterBlock(
$nbProducts,
$selectedFilters
) {
$idLang = (int) $this->context->language->id;
$idShop = (int) $this->context->shop->id;
$idParent = (int) Tools::getValue(
'id_category',
Tools::getValue('id_category_layered', Configuration::get('PS_HOME_CATEGORY'))
);
/* Get the filters for the current category */
$filters = $this->database->executeS(
'SELECT type, id_value, filter_show_limit, filter_type ' .
'FROM ' . _DB_PREFIX_ . 'layered_category ' .
'WHERE id_category = ' . $idParent . ' ' .
'AND id_shop = ' . $idShop . ' ' .
'GROUP BY `type`, id_value ORDER BY position ASC'
);
$filterBlocks = [];
// iterate through each filter, and the get corresponding filter block
foreach ($filters as $filter) {
switch ($filter['type']) {
case 'price':
$filterBlocks[] = $this->getPriceRangeBlock($filter, $selectedFilters, $nbProducts);
break;
case 'weight':
$filterBlocks[] = $this->getWeightRangeBlock($filter, $selectedFilters, $nbProducts);
break;
case 'condition':
$filterBlocks[] = $this->getConditionsBlock($filter, $selectedFilters);
break;
case 'quantity':
$filterBlocks[] = $this->getQuantitiesBlock($filter, $selectedFilters);
break;
case 'manufacturer':
$filterBlocks[] = $this->getManufacturersBlock($filter, $selectedFilters, $idLang);
break;
case 'id_attribute_group':
$filterBlocks =
array_merge($filterBlocks, $this->getAttributesBlock($filter, $selectedFilters, $idLang));
break;
case 'id_feature':
$filterBlocks =
array_merge($filterBlocks, $this->getFeaturesBlock($filter, $selectedFilters, $idLang));
break;
case 'category':
$parent = new Category($idParent, $idLang);
$filterBlocks[] = $this->getCategoriesBlock($filter, $selectedFilters, $idLang, $parent);
}
}
return [
'filters' => $filterBlocks,
];
}
protected function showPriceFilter()
{
return Group::getCurrent()->show_prices;
}
/**
* Get the filter block from the cache table
*
* @param string $filterHash
*
* @return array|null
*/
public function getFromCache($filterHash)
{
if (!Configuration::get('PS_LAYERED_CACHE_ENABLED')) {
return null;
}
$row = $this->database->getRow(
'SELECT data FROM ' . _DB_PREFIX_ . 'layered_filter_block WHERE hash="' . pSQL($filterHash) . '"'
);
if (!empty($row)) {
return unserialize(current($row));
}
return null;
}
/**
* Insert the filter block into the cache table
*
* @param string $filterHash
* @param array $data
*/
public function insertIntoCache($filterHash, $data)
{
if (!Configuration::get('PS_LAYERED_CACHE_ENABLED')) {
return;
}
try {
$this->database->execute(
'REPLACE INTO ' . _DB_PREFIX_ . 'layered_filter_block (hash, data) ' .
'VALUES ("' . $filterHash . '", "' . pSQL(serialize($data)) . '")'
);
} catch (PrestaShopDatabaseException $e) {
// Don't worry if the cache have invalid or duplicate hash
}
}
/**
* @param array $filter
* @param array $selectedFilters
* @param int $nbProducts
*
* @return array
*/
private function getPriceRangeBlock($filter, $selectedFilters, $nbProducts)
{
if (!$this->showPriceFilter()) {
return [];
}
$priceSpecifications = $this->preparePriceSpecifications();
$priceBlock = [
'type_lite' => 'price',
'type' => 'price',
'id_key' => 0,
'name' => $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop'),
'max' => '0',
'min' => null,
'unit' => $this->context->currency->sign,
'specifications' => $priceSpecifications,
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => Converter::WIDGET_TYPE_SLIDER,
'nbr' => $nbProducts,
];
list($priceMinFilter, $priceMaxFilter, $weightFilter) = $this->ignorePriceAndWeightFilters(
$this->searchAdapter->getInitialPopulation()
);
list($priceBlock['min'], $priceBlock['max']) = $this->searchAdapter->getInitialPopulation()->getMinMaxPriceValue();
$priceBlock['value'] = !empty($selectedFilters['price']) ? $selectedFilters['price'] : null;
$this->restorePriceAndWeightFilters(
$this->searchAdapter->getInitialPopulation(),
$priceMinFilter,
$priceMaxFilter,
$weightFilter
);
return $priceBlock;
}
/**
* Price / weight filter block should not apply their own filters
* otherwise they will always disappear if we filter on price / weight
* because only one choice will remain
*
* @param InterfaceAdapter $filteredSearchAdapter
*
* @return array
*/
private function ignorePriceAndWeightFilters(InterfaceAdapter $filteredSearchAdapter)
{
// disable the current price and weight filters to compute ranges
$priceMinFilter = $filteredSearchAdapter->getFilter('price_min');
$priceMaxFilter = $filteredSearchAdapter->getFilter('price_max');
$weightFilter = $filteredSearchAdapter->getFilter('weight');
$filteredSearchAdapter->resetFilter('price_min');
$filteredSearchAdapter->resetFilter('price_max');
$filteredSearchAdapter->resetFilter('weight');
return [
$priceMinFilter,
$priceMaxFilter,
$weightFilter,
];
}
/**
* Restore price and weight filters
*
* @param InterfaceAdapter $filteredSearchAdapter
* @param int $priceMinFilter
* @param int $priceMaxFilter
* @param int $weightFilter
*/
private function restorePriceAndWeightFilters(
$filteredSearchAdapter,
$priceMinFilter,
$priceMaxFilter,
$weightFilter
) {
// put back the price and weight filters
$filteredSearchAdapter->setFilter('price_min', $priceMinFilter);
$filteredSearchAdapter->setFilter('price_max', $priceMaxFilter);
$filteredSearchAdapter->setFilter('weight', $weightFilter);
}
/**
* Get the weight filter block
*
* @param array $filter
* @param array $selectedFilters
* @param int $nbProducts
*
* @return array
*/
private function getWeightRangeBlock($filter, $selectedFilters, $nbProducts)
{
$weightBlock = [
'type_lite' => 'weight',
'type' => 'weight',
'id_key' => 0,
'name' => $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop'),
'max' => '0',
'min' => null,
'unit' => Configuration::get('PS_WEIGHT_UNIT'),
'specifications' => null,
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => Converter::WIDGET_TYPE_SLIDER,
'value' => null,
'nbr' => $nbProducts,
];
list($priceMinFilter, $priceMaxFilter, $weightFilter) = $this->ignorePriceAndWeightFilters(
$this->searchAdapter->getInitialPopulation()
);
list($weightBlock['min'], $weightBlock['max']) = $this->searchAdapter->getInitialPopulation()->getMinMaxValue('p.weight');
if (empty($weightBlock['min']) && empty($weightBlock['max'])) {
// We don't need to continue, no filter available
return [];
}
$weightBlock['value'] = !empty($selectedFilters['weight']) ? $selectedFilters['weight'] : null;
$this->restorePriceAndWeightFilters(
$this->searchAdapter->getInitialPopulation(),
$priceMinFilter,
$priceMaxFilter,
$weightFilter
);
return $weightBlock;
}
/**
* Get the condition filter block
*
* @param array $filter
* @param array $selectedFilters
*
* @return array
*/
private function getConditionsBlock($filter, $selectedFilters)
{
$conditionArray = [
'new' => [
'name' => $this->context->getTranslator()->trans(
'New',
[],
'Modules.Facetedsearch.Shop'
),
'nbr' => 0,
],
'used' => [
'name' => $this->context->getTranslator()->trans(
'Used',
[],
'Modules.Facetedsearch.Shop'
),
'nbr' => 0,
],
'refurbished' => [
'name' => $this->context->getTranslator()->trans(
'Refurbished',
[],
'Modules.Facetedsearch.Shop'
),
'nbr' => 0,
],
];
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('condition');
$results = $filteredSearchAdapter->valueCount('condition');
foreach ($results as $key => $values) {
$condition = $values['condition'];
$count = $values['c'];
$conditionArray[$condition]['nbr'] = $count;
if (isset($selectedFilters['condition'])
&& in_array($condition, $selectedFilters['condition'])
) {
$conditionArray[$condition]['checked'] = true;
}
}
$conditionBlock = [
'type_lite' => 'condition',
'type' => 'condition',
'id_key' => 0,
'name' => $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop'),
'values' => $conditionArray,
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => $filter['filter_type'],
];
return $conditionBlock;
}
/**
* Get the quantities filter block
*
* @param array $filter
* @param array $selectedFilters
*
* @return array
*/
private function getQuantitiesBlock($filter, $selectedFilters)
{
if ($this->psStockManagement === null) {
$this->psStockManagement = (bool) Configuration::get('PS_STOCK_MANAGEMENT');
}
if ($this->psOrderOutOfStock === null) {
$this->psOrderOutOfStock = (bool) Configuration::get('PS_ORDER_OUT_OF_STOCK');
}
// We only initialize the options if stock management is activated
$availabilityOptions = [];
if ($this->psStockManagement) {
$availabilityOptions = [
0 => [
'name' => $this->context->getTranslator()->trans(
'Not available',
[],
'Modules.Facetedsearch.Shop'
),
'nbr' => 0,
],
1 => [
'name' => $this->context->getTranslator()->trans(
'Available',
[],
'Modules.Facetedsearch.Shop'
),
'nbr' => 0,
],
2 => [
'name' => $this->context->getTranslator()->trans(
'In stock',
[],
'Modules.Facetedsearch.Shop'
),
'nbr' => 0,
],
];
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter(Search::STOCK_MANAGEMENT_FILTER);
// Products without quantity in stock, with out-of-stock ordering disabled
$filteredSearchAdapter->addOperationsFilter(
Search::STOCK_MANAGEMENT_FILTER,
[
[
['quantity', [0], '<='],
['out_of_stock', !$this->psOrderOutOfStock ? [0, 2] : [0], '='],
],
]
);
$availabilityOptions[0]['nbr'] = $filteredSearchAdapter->count();
// Products in stock, or with out-of-stock ordering enabled
$filteredSearchAdapter->addOperationsFilter(
Search::STOCK_MANAGEMENT_FILTER,
[
[
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
],
[
['quantity', [0], '>'],
],
]
);
$availabilityOptions[1]['nbr'] = $filteredSearchAdapter->count();
// Products in stock
$filteredSearchAdapter->addOperationsFilter(
Search::STOCK_MANAGEMENT_FILTER,
[
[
['quantity', [0], '>'],
],
]
);
$availabilityOptions[2]['nbr'] = $filteredSearchAdapter->count();
// If some filter was selected, we want to show only this single filter, it does not make sense to show others
if (isset($selectedFilters['quantity'])) {
// We loop through selected filters and assign it to our options and remove the rest
foreach ($availabilityOptions as $key => $values) {
if (in_array($key, $selectedFilters['quantity'], true)) {
$availabilityOptions[$key]['checked'] = true;
}
}
}
}
$quantityBlock = [
'type_lite' => 'quantity',
'type' => 'quantity',
'id_key' => 0,
'name' => $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop'),
'values' => $availabilityOptions,
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => $filter['filter_type'],
];
return $quantityBlock;
}
/**
* Get the manufacturers filter block
*
* @param array $filter
* @param array $selectedFilters
* @param int $idLang
*
* @return array
*/
private function getManufacturersBlock($filter, $selectedFilters, $idLang)
{
$manufacturersArray = $manufacturers = [];
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('id_manufacturer');
$tempManufacturers = Manufacturer::getManufacturers(false, $idLang);
if (empty($tempManufacturers)) {
return $manufacturersArray;
}
foreach ($tempManufacturers as $key => $manufacturer) {
$manufacturers[$manufacturer['id_manufacturer']] = $manufacturer;
}
$results = $filteredSearchAdapter->valueCount('id_manufacturer');
foreach ($results as $key => $values) {
if (!isset($values['id_manufacturer'])) {
continue;
}
$id_manufacturer = $values['id_manufacturer'];
if (empty($manufacturers[$id_manufacturer]['name'])) {
continue;
}
$count = $values['c'];
$manufacturersArray[$id_manufacturer] = [
'name' => $manufacturers[$id_manufacturer]['name'],
'nbr' => $count,
];
if (isset($selectedFilters['manufacturer'])
&& in_array($id_manufacturer, $selectedFilters['manufacturer'])
) {
$manufacturersArray[$id_manufacturer]['checked'] = true;
}
}
$manufacturerBlock = [
'type_lite' => 'manufacturer',
'type' => 'manufacturer',
'id_key' => 0,
'name' => $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop'),
'values' => $manufacturersArray,
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => $filter['filter_type'],
];
return $manufacturerBlock;
}
/**
* Get the attributes filter block
*
* @param array $filter
* @param array $selectedFilters
* @param int $idLang
*
* @return array
*/
private function getAttributesBlock($filter, $selectedFilters, $idLang)
{
$attributesBlock = [];
$filteredSearchAdapter = null;
$idAttributeGroup = $filter['id_value'];
if (!empty($selectedFilters['id_attribute_group'])) {
foreach ($selectedFilters['id_attribute_group'] as $key => $selectedFilter) {
if ($key == $idAttributeGroup) {
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('with_attributes_' . $idAttributeGroup);
break;
}
}
}
if (!$filteredSearchAdapter) {
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter();
}
$attributesGroup = $this->dataAccessor->getAttributesGroups($idLang);
if ($attributesGroup === []) {
return $attributesBlock;
}
$attributes = $this->dataAccessor->getAttributes($idLang, $idAttributeGroup);
$filteredSearchAdapter->addOperationsFilter(
'id_attribute_group_' . $idAttributeGroup,
[[['id_attribute_group', [(int) $idAttributeGroup]]]]
);
$results = $filteredSearchAdapter->valueCount('id_attribute');
foreach ($results as $key => $values) {
$idAttribute = $values['id_attribute'];
if (!isset($attributes[$idAttribute])) {
continue;
}
$count = $values['c'];
$attribute = $attributes[$idAttribute];
$idAttributeGroup = $attribute['id_attribute_group'];
if (!isset($attributesBlock[$idAttributeGroup])) {
$attributeGroup = $attributesGroup[$idAttributeGroup];
$attributesBlock[$idAttributeGroup] = [
'type_lite' => 'id_attribute_group',
'type' => 'id_attribute_group',
'id_key' => $idAttributeGroup,
'name' => $attributeGroup['attribute_group_name'],
'is_color_group' => (bool) $attributeGroup['is_color_group'],
'values' => [],
'url_name' => $attributeGroup['url_name'],
'meta_title' => $attributeGroup['meta_title'],
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => $filter['filter_type'],
];
}
$attributesBlock[$idAttributeGroup]['values'][$idAttribute] = [
'name' => $attribute['name'],
'nbr' => $count,
'url_name' => $attribute['url_name'],
'meta_title' => $attribute['meta_title'],
];
if ($attributesBlock[$idAttributeGroup]['is_color_group'] !== false) {
$attributesBlock[$idAttributeGroup]['values'][$idAttribute]['color'] = $attribute['color'];
}
if (array_key_exists('id_attribute_group', $selectedFilters)) {
foreach ($selectedFilters['id_attribute_group'] as $selectedAttribute) {
if (in_array($idAttribute, $selectedAttribute)) {
$attributesBlock[$idAttributeGroup]['values'][$idAttribute]['checked'] = true;
}
}
}
}
foreach ($attributesBlock as $idAttributeGroup => $value) {
$attributesBlock[$idAttributeGroup]['values'] = $this->sortByKey($attributes, $value['values']);
}
$attributesBlock = $this->sortByKey($attributesGroup, $attributesBlock);
return $attributesBlock;
}
/**
* Sort an array using the same key order than the sortedReferenceArray
*
* @param array $sortedReferenceArray
* @param array $array
*
* @return array
*/
private function sortByKey(array $sortedReferenceArray, $array)
{
$sortedArray = [];
// iterate in the original order
foreach ($sortedReferenceArray as $key => $value) {
if (array_key_exists($key, $array)) {
$sortedArray[$key] = $array[$key];
}
}
return $sortedArray;
}
/**
* Get the features filter block
*
* @param array $filter
* @param array $selectedFilters
* @param int $idLang
*
* @return array
*/
private function getFeaturesBlock($filter, $selectedFilters, $idLang)
{
$features = $featureBlock = [];
$idFeature = $filter['id_value'];
$filteredSearchAdapter = null;
if (!empty($selectedFilters['id_feature'])) {
foreach ($selectedFilters['id_feature'] as $key => $selectedFilter) {
if ($key == $idFeature) {
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('with_features_' . $idFeature);
break;
}
}
}
if (!$filteredSearchAdapter) {
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter();
}
$tempFeatures = $this->dataAccessor->getFeatures($idLang);
if (empty($tempFeatures)) {
return [];
}
foreach ($tempFeatures as $key => $feature) {
$features[$feature['id_feature']] = $feature;
}
$filteredSearchAdapter->addOperationsFilter(
'id_feature_' . $idFeature,
[[['id_feature', [(int) $idFeature]]]]
);
$filteredSearchAdapter->addSelectField('id_feature');
$results = $filteredSearchAdapter->valueCount('id_feature_value');
foreach ($results as $key => $values) {
$idFeatureValue = $values['id_feature_value'];
$idFeature = $values['id_feature'];
$count = $values['c'];
$feature = $features[$idFeature];
if (!isset($featureBlock[$idFeature])) {
$tempFeatureValues = $this->dataAccessor->getFeatureValues($idFeature, $idLang);
foreach ($tempFeatureValues as $featureValueKey => $featureValue) {
$features[$idFeature]['featureValues'][$featureValue['id_feature_value']] = $featureValue;
}
$featureBlock[$idFeature] = [
'type_lite' => 'id_feature',
'type' => 'id_feature',
'id_key' => $idFeature,
'values' => [],
'name' => $feature['name'],
'url_name' => $feature['url_name'],
'meta_title' => $feature['meta_title'],
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => $filter['filter_type'],
];
}
$featureValues = $features[$idFeature]['featureValues'];
if (!isset($featureValues[$idFeatureValue]['value'])) {
continue;
}
$featureBlock[$idFeature]['values'][$idFeatureValue] = [
'nbr' => $count,
'name' => $featureValues[$idFeatureValue]['value'],
'url_name' => $featureValues[$idFeatureValue]['url_name'],
'meta_title' => $featureValues[$idFeatureValue]['meta_title'],
];
if (array_key_exists('id_feature', $selectedFilters)) {
foreach ($selectedFilters['id_feature'] as $selectedFeature) {
if (in_array($idFeatureValue, $selectedFeature)) {
$featureBlock[$feature['id_feature']]['values'][$idFeatureValue]['checked'] = true;
}
}
}
}
$featureBlock = $this->sortFeatureBlock($featureBlock);
return $featureBlock;
}
/**
* Natural sort multi-dimensional feature array
*
* @param array $featureBlock
*
* @return array
*/
private function sortFeatureBlock($featureBlock)
{
//Natural sort
foreach ($featureBlock as $key => $value) {
$temp = [];
foreach ($featureBlock[$key]['values'] as $idFeatureValue => $featureValueInfos) {
$temp[$idFeatureValue] = $featureValueInfos['name'];
}
natcasesort($temp);
$temp2 = [];
foreach ($temp as $keytemp => $valuetemp) {
$temp2[$keytemp] = $featureBlock[$key]['values'][$keytemp];
}
$featureBlock[$key]['values'] = $temp2;
}
return $featureBlock;
}
/**
* Add the categories filter condition based on the parent and config variables
*
* @param InterfaceAdapter $filteredSearchAdapter
* @param Category $parent
*/
private function addCategoriesBlockFilters(InterfaceAdapter $filteredSearchAdapter, $parent)
{
if (Group::isFeatureActive()) {
$userGroups = ($this->context->customer->isLogged() ? $this->context->customer->getGroups() : [
Configuration::get(
'PS_UNIDENTIFIED_GROUP'
),
]);
$filteredSearchAdapter->addFilter('id_group', $userGroups);
}
$depth = (int) Configuration::get('PS_LAYERED_FILTER_CATEGORY_DEPTH', null, null, null, 1);
if ($depth) {
$levelDepth = $parent->level_depth;
$filteredSearchAdapter->addFilter('level_depth', [$depth + $levelDepth], '<=');
}
$filteredSearchAdapter->addFilter('nleft', [$parent->nleft], '>');
$filteredSearchAdapter->addFilter('nright', [$parent->nright], '<');
}
/**
* Get the categories filter block
*
* @param array $filter
* @param array $selectedFilters
* @param int $idLang
* @param Category $parent
*
* @return array
*/
private function getCategoriesBlock($filter, $selectedFilters, $idLang, $parent)
{
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('id_category');
$this->addCategoriesBlockFilters($filteredSearchAdapter, $parent);
$categoryArray = [];
$categories = Category::getAllCategoriesName(
null,
$idLang,
true,
null,
true,
'',
'ORDER BY c.nleft, c.position'
);
foreach ($categories as $key => $value) {
$categories[$value['id_category']] = $value;
}
$results = $filteredSearchAdapter->valueCount('id_category');
foreach ($results as $key => $values) {
$idCategory = $values['id_category'];
if (!isset($categories[$idCategory])) {
// Category can sometimes not be found in case of multistore
// plus waiting for indexation
continue;
}
$count = $values['c'];
$categoryArray[$idCategory] = [
'name' => $categories[$idCategory]['name'],
'nbr' => $count,
];
if (isset($selectedFilters['category']) && in_array($idCategory, $selectedFilters['category'])) {
$categoryArray[$idCategory]['checked'] = true;
}
}
$categoryBlock = [
'type_lite' => 'category',
'type' => 'category',
'id_key' => 0,
'name' => $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop'),
'values' => $categoryArray,
'filter_show_limit' => (int) $filter['filter_show_limit'],
'filter_type' => $filter['filter_type'],
];
return $categoryBlock;
}
/**
* Prepare price specifications to display cldr prices.
*
* @return array
*/
private function preparePriceSpecifications()
{
/* @var Currency */
$currency = $this->context->currency;
// New method since PS 1.7.6
if (isset($this->context->currentLocale) && method_exists($this->context->currentLocale, 'getPriceSpecification')) {
/* @var PriceSpecification */
$priceSpecification = $this->context->currentLocale->getPriceSpecification($currency->iso_code);
/* @var NumberSymbolList */
$symbolList = $priceSpecification->getSymbolsByNumberingSystem(Locale::NUMBERING_SYSTEM_LATIN);
$symbol = [
$symbolList->getDecimal(),
$symbolList->getGroup(),
$symbolList->getList(),
$symbolList->getPercentSign(),
$symbolList->getMinusSign(),
$symbolList->getPlusSign(),
$symbolList->getExponential(),
$symbolList->getSuperscriptingExponent(),
$symbolList->getPerMille(),
$symbolList->getInfinity(),
$symbolList->getNaN(),
];
return array_merge(
['symbol' => $symbol],
$priceSpecification->toArray()
);
}
// Default symbol configuration
$symbol = [
'.',
',',
';',
'%',
'-',
'+',
'E',
'×',
'‰',
'∞',
'NaN',
];
// The property `$precision` exists only from PS 1.7.6. On previous versions, all prices have 2 decimals
$precision = isset($currency->precision) ? $currency->precision : 2;
$formats = explode(';', $currency->format);
if (count($formats) > 1) {
$positivePattern = $formats[0];
$negativePattern = $formats[1];
} else {
$positivePattern = $currency->format;
$negativePattern = $currency->format;
}
return [
'positivePattern' => $positivePattern,
'negativePattern' => $negativePattern,
'symbol' => $symbol,
'maxFractionDigits' => $precision,
'minFractionDigits' => $precision,
'groupingUsed' => true,
'primaryGroupSize' => 3,
'secondaryGroupSize' => 3,
'currencyCode' => $currency->iso_code,
'currencySymbol' => $currency->sign,
];
}
}

View File

@@ -0,0 +1,517 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Category;
use Configuration;
use Context;
use Db;
use Manufacturer;
use PrestaShop\Module\FacetedSearch\Filters;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use Tools;
class Converter
{
const WIDGET_TYPE_CHECKBOX = 0;
const WIDGET_TYPE_RADIO = 1;
const WIDGET_TYPE_DROPDOWN = 2;
const WIDGET_TYPE_SLIDER = 3;
const TYPE_ATTRIBUTE_GROUP = 'id_attribute_group';
const TYPE_AVAILABILITY = 'availability';
const TYPE_CATEGORY = 'category';
const TYPE_CONDITION = 'condition';
const TYPE_FEATURE = 'id_feature';
const TYPE_QUANTITY = 'quantity';
const TYPE_MANUFACTURER = 'manufacturer';
const TYPE_PRICE = 'price';
const TYPE_WEIGHT = 'weight';
const PROPERTY_URL_NAME = 'url_name';
const PROPERTY_COLOR = 'color';
const PROPERTY_TEXTURE = 'texture';
/**
* @var array
*/
const RANGE_FILTERS = [self::TYPE_PRICE, self::TYPE_WEIGHT];
/**
* @var Context
*/
protected $context;
/**
* @var Db
*/
protected $database;
/**
* @var URLSerializer
*/
protected $urlSerializer;
/**
* @var Filters\DataAccessor
*/
private $dataAccessor;
public function __construct(
Context $context,
Db $database,
URLSerializer $urlSerializer,
Filters\DataAccessor $dataAccessor
) {
$this->context = $context;
$this->database = $database;
$this->urlSerializer = $urlSerializer;
$this->dataAccessor = $dataAccessor;
}
public function getFacetsFromFilterBlocks(array $filterBlocks)
{
$facets = [];
foreach ($filterBlocks as $filterBlock) {
if (empty($filterBlock)) {
// Empty filter, let's continue
continue;
}
$facet = new Facet();
$facet
->setLabel($filterBlock['name'])
->setProperty('filter_show_limit', $filterBlock['filter_show_limit'])
->setMultipleSelectionAllowed(true);
switch ($filterBlock['type']) {
case self::TYPE_CATEGORY:
case self::TYPE_CONDITION:
case self::TYPE_MANUFACTURER:
case self::TYPE_QUANTITY:
case self::TYPE_ATTRIBUTE_GROUP:
case self::TYPE_FEATURE:
$type = $filterBlock['type'];
if ($filterBlock['type'] === self::TYPE_QUANTITY) {
$type = 'availability';
} elseif ($filterBlock['type'] == self::TYPE_ATTRIBUTE_GROUP) {
$type = 'attribute_group';
$facet->setProperty(self::TYPE_ATTRIBUTE_GROUP, $filterBlock['id_key']);
if (isset($filterBlock['url_name'])) {
$facet->setProperty(self::PROPERTY_URL_NAME, $filterBlock['url_name']);
}
} elseif ($filterBlock['type'] == self::TYPE_FEATURE) {
$type = 'feature';
$facet->setProperty(self::TYPE_FEATURE, $filterBlock['id_key']);
if (isset($filterBlock['url_name'])) {
$facet->setProperty(self::PROPERTY_URL_NAME, $filterBlock['url_name']);
}
}
$facet->setType($type);
$filters = [];
foreach ($filterBlock['values'] as $id => $filterArray) {
$filter = new Filter();
$filter
->setType($type)
->setLabel($filterArray['name'])
->setMagnitude($filterArray['nbr'])
->setValue($id);
if (isset($filterArray['url_name'])) {
$filter->setProperty(self::PROPERTY_URL_NAME, $filterArray['url_name']);
}
if (array_key_exists('checked', $filterArray)) {
$filter->setActive($filterArray['checked']);
}
if (isset($filterArray['color'])) {
if ($filterArray['color'] != '') {
$filter->setProperty(self::PROPERTY_COLOR, $filterArray['color']);
} elseif (file_exists(_PS_COL_IMG_DIR_ . $id . '.jpg')) {
$filter->setProperty(self::PROPERTY_TEXTURE, _THEME_COL_DIR_ . $id . '.jpg');
}
}
$filters[] = $filter;
}
if ((int) $filterBlock['filter_show_limit'] !== 0) {
usort($filters, [$this, 'sortFiltersByMagnitude']);
}
$this->hideZeroValuesAndShowLimit($filters, (int) $filterBlock['filter_show_limit']);
if ((int) $filterBlock['filter_show_limit'] !== 0 || $filterBlock['type'] !== self::TYPE_ATTRIBUTE_GROUP) {
usort($filters, [$this, 'sortFiltersByLabel']);
}
// No method available to add all filters
foreach ($filters as $filter) {
$facet->addFilter($filter);
}
break;
case self::TYPE_WEIGHT:
case self::TYPE_PRICE:
$facet
->setType($filterBlock['type'])
->setProperty('min', $filterBlock['min'])
->setProperty('max', $filterBlock['max'])
->setProperty('unit', $filterBlock['unit'])
->setProperty('specifications', $filterBlock['specifications'])
->setMultipleSelectionAllowed(false)
->setProperty('range', true);
$filter = new Filter();
$filter
->setActive($filterBlock['value'] !== null)
->setType($filterBlock['type'])
->setMagnitude($filterBlock['nbr'])
->setProperty('symbol', $filterBlock['unit'])
->setValue($filterBlock['value']);
$facet->addFilter($filter);
break;
}
switch ((int) $filterBlock['filter_type']) {
case self::WIDGET_TYPE_CHECKBOX:
$facet->setMultipleSelectionAllowed(true);
$facet->setWidgetType('checkbox');
break;
case self::WIDGET_TYPE_RADIO:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('radio');
break;
case self::WIDGET_TYPE_DROPDOWN:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('dropdown');
break;
case self::WIDGET_TYPE_SLIDER:
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('slider');
break;
}
$facets[] = $facet;
}
return $facets;
}
/**
* @param ProductSearchQuery $query
*
* @return array
*/
public function createFacetedSearchFiltersFromQuery(ProductSearchQuery $query)
{
$idShop = (int) $this->context->shop->id;
$idLang = (int) $this->context->language->id;
$idParent = $query->getIdCategory();
if (empty($idParent)) {
$idParent = (int) Tools::getValue('id_category_layered', Configuration::get('PS_HOME_CATEGORY'));
}
$searchFilters = [];
/* Get the filters for the current category */
$filters = $this->database->executeS(
'SELECT type, id_value, filter_show_limit, filter_type FROM ' . _DB_PREFIX_ . 'layered_category
WHERE id_category = ' . (int) $idParent . '
AND id_shop = ' . (int) $idShop . '
GROUP BY `type`, id_value ORDER BY position ASC'
);
$facetAndFiltersLabels = $this->urlSerializer->unserialize($query->getEncodedFacets());
foreach ($filters as $filter) {
$filterLabel = $this->convertFilterTypeToLabel($filter['type']);
switch ($filter['type']) {
case self::TYPE_MANUFACTURER:
if (!isset($facetAndFiltersLabels[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$manufacturers = Manufacturer::getManufacturers(false, $idLang);
$searchFilters[$filter['type']] = [];
foreach ($manufacturers as $manufacturer) {
if (in_array($manufacturer['name'], $facetAndFiltersLabels[$filterLabel])) {
$searchFilters[$filter['type']][$manufacturer['name']] = $manufacturer['id_manufacturer'];
}
}
break;
case self::TYPE_QUANTITY:
if (!isset($facetAndFiltersLabels[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$quantityArray = [
$this->context->getTranslator()->trans(
'Not available',
[],
'Modules.Facetedsearch.Shop'
) => 0,
$this->context->getTranslator()->trans(
'Available',
[],
'Modules.Facetedsearch.Shop'
) => 1,
$this->context->getTranslator()->trans(
'In stock',
[],
'Modules.Facetedsearch.Shop'
) => 2,
];
$searchFilters[$filter['type']] = [];
foreach ($quantityArray as $quantityName => $quantityId) {
if (isset($facetAndFiltersLabels[$filterLabel]) && in_array($quantityName, $facetAndFiltersLabels[$filterLabel])) {
$searchFilters[$filter['type']][] = $quantityId;
}
}
break;
case self::TYPE_CONDITION:
if (!isset($facetAndFiltersLabels[$filterLabel])) {
// No need to filter if no information
continue 2;
}
$conditionArray = [
$this->context->getTranslator()->trans(
'New',
[],
'Modules.Facetedsearch.Shop'
) => 'new',
$this->context->getTranslator()->trans(
'Used',
[],
'Modules.Facetedsearch.Shop'
) => 'used',
$this->context->getTranslator()->trans(
'Refurbished',
[],
'Modules.Facetedsearch.Shop'
) => 'refurbished',
];
$searchFilters[$filter['type']] = [];
foreach ($conditionArray as $conditionName => $conditionId) {
if (isset($facetAndFiltersLabels[$filterLabel]) && in_array($conditionName, $facetAndFiltersLabels[$filterLabel])) {
$searchFilters[$filter['type']][] = $conditionId;
}
}
break;
case self::TYPE_FEATURE:
$features = $this->dataAccessor->getFeatures($idLang);
foreach ($features as $feature) {
if ($filter['id_value'] != $feature['id_feature']) {
continue;
}
if (isset($facetAndFiltersLabels[$feature['url_name']])) {
$featureValueLabels = $facetAndFiltersLabels[$feature['url_name']];
} elseif (isset($facetAndFiltersLabels[$feature['name']])) {
$featureValueLabels = $facetAndFiltersLabels[$feature['name']];
} else {
continue;
}
$featureValues = $this->dataAccessor->getFeatureValues($feature['id_feature'], $idLang);
foreach ($featureValues as $featureValue) {
if (in_array($featureValue['url_name'], $featureValueLabels)
|| in_array($featureValue['value'], $featureValueLabels)
) {
$searchFilters['id_feature'][$feature['id_feature']][] = $featureValue['id_feature_value'];
}
}
}
break;
case self::TYPE_ATTRIBUTE_GROUP:
$attributesGroup = $this->dataAccessor->getAttributesGroups($idLang);
foreach ($attributesGroup as $attributeGroup) {
if ($filter['id_value'] != $attributeGroup['id_attribute_group']) {
continue;
}
if (isset($facetAndFiltersLabels[$attributeGroup['url_name']])) {
$attributeLabels = $facetAndFiltersLabels[$attributeGroup['url_name']];
} elseif (isset($facetAndFiltersLabels[$attributeGroup['attribute_group_name']])) {
$attributeLabels = $facetAndFiltersLabels[$attributeGroup['attribute_group_name']];
} else {
continue;
}
$attributes = $this->dataAccessor->getAttributes($idLang, $attributeGroup['id_attribute_group']);
foreach ($attributes as $attribute) {
if (in_array($attribute['url_name'], $attributeLabels)
|| in_array($attribute['name'], $attributeLabels)
) {
$searchFilters['id_attribute_group'][$attributeGroup['id_attribute_group']][] = $attribute['id_attribute'];
}
}
}
break;
case self::TYPE_PRICE:
case self::TYPE_WEIGHT:
if (isset($facetAndFiltersLabels[$filterLabel])) {
$filters = $facetAndFiltersLabels[$filterLabel];
if (isset($filters[1]) && isset($filters[2])) {
$from = $filters[1];
$to = $filters[2];
$searchFilters[$filter['type']][0] = $from;
$searchFilters[$filter['type']][1] = $to;
}
}
break;
case self::TYPE_CATEGORY:
if (isset($facetAndFiltersLabels[$filterLabel])) {
foreach ($facetAndFiltersLabels[$filterLabel] as $queryFilter) {
$categories = Category::searchByNameAndParentCategoryId($idLang, $queryFilter, (int) $query->getIdCategory());
if ($categories) {
$searchFilters[$filter['type']][] = $categories['id_category'];
}
}
}
break;
default:
if (isset($facetAndFiltersLabels[$filterLabel])) {
foreach ($facetAndFiltersLabels[$filterLabel] as $queryFilter) {
$searchFilters[$filter['type']][] = $queryFilter;
}
}
}
}
// Remove all empty selected filters
foreach ($searchFilters as $key => $value) {
switch ($key) {
case self::TYPE_PRICE:
case self::TYPE_WEIGHT:
if ($value[0] === '' && $value[1] === '') {
unset($searchFilters[$key]);
}
break;
default:
if ($value == '' || $value == []) {
unset($searchFilters[$key]);
}
break;
}
}
return $searchFilters;
}
/**
* Convert filter type to label
*
* @param string $filterType
*/
private function convertFilterTypeToLabel($filterType)
{
switch ($filterType) {
case self::TYPE_PRICE:
return $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_WEIGHT:
return $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_CONDITION:
return $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_QUANTITY:
return $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_MANUFACTURER:
return $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_CATEGORY:
return $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop');
case self::TYPE_FEATURE:
case self::TYPE_ATTRIBUTE_GROUP:
default:
return null;
}
}
/**
* Hide entries with 0 results
* Hide depending of show limit parameter
*
* @param array $filters
*
* @return array
*/
private function hideZeroValuesAndShowLimit(array $filters, $showLimit)
{
$count = 0;
foreach ($filters as $filter) {
if ($filter->getMagnitude() === 0
|| ($showLimit > 0 && $count >= $showLimit)
) {
$filter->setDisplayed(false);
continue;
}
++$count;
}
return $filters;
}
/**
* Sort filters by magnitude
*
* @param Filter $a
* @param Filter $b
*
* @return int
*/
private function sortFiltersByMagnitude(Filter $a, Filter $b)
{
$aMagnitude = $a->getMagnitude();
$bMagnitude = $b->getMagnitude();
if ($aMagnitude == $bMagnitude) {
// Same magnitude, sort by label
return $this->sortFiltersByLabel($a, $b);
}
return $aMagnitude > $bMagnitude ? -1 : +1;
}
/**
* Sort filters by label
*
* @param Filter $a
* @param Filter $b
*
* @return int
*/
private function sortFiltersByLabel(Filter $a, Filter $b)
{
return strnatcasecmp($a->getLabel(), $b->getLabel());
}
}

View File

@@ -0,0 +1,184 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Combination;
use Db;
use Shop;
/**
* Data accessor for features and attributes
*/
class DataAccessor
{
/**
* @var array
*/
private $attributesGroup;
/**
* @var array
*/
private $attributes;
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* @param int $idLang
*
* @return array|false|\PDOStatement|resource|null
*/
public function getAttributes($idLang, $idAttributeGroup)
{
if (!Combination::isFeatureActive()) {
return [];
}
if (!isset($this->attributes[$idLang][$idAttributeGroup])) {
$this->attributes[$idLang] = [$idAttributeGroup => []];
$tempAttributes = $this->database->executeS(
'SELECT DISTINCT a.`id_attribute`, ' .
'a.`color`, ' .
'al.`name`, ' .
'agl.`id_attribute_group`, ' .
'IF(lialv.`url_name` IS NULL OR lialv.`url_name` = "", NULL, lialv.`url_name`) AS url_name, ' .
'IF(lialv.`meta_title` IS NULL OR lialv.`meta_title` = "", NULL, lialv.`meta_title`) AS meta_title ' .
'FROM `' . _DB_PREFIX_ . 'attribute_group` ag ' .
'INNER JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ' .
'ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
'INNER JOIN `' . _DB_PREFIX_ . 'attribute` a ' .
'ON a.`id_attribute_group` = ag.`id_attribute_group` ' .
'INNER JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ' .
'ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $idLang . ')' .
Shop::addSqlAssociation('attribute_group', 'ag') . ' ' .
Shop::addSqlAssociation('attribute', 'a') . ' ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value` lialv ' .
'ON (a.`id_attribute` = lialv.`id_attribute` AND lialv.`id_lang` = ' . (int) $idLang . ') ' .
'WHERE ag.id_attribute_group = ' . (int) $idAttributeGroup . ' ' .
'ORDER BY agl.`name` ASC, a.`position` ASC'
);
foreach ($tempAttributes as $key => $attribute) {
$this->attributes[$idLang][$idAttributeGroup][$attribute['id_attribute']] = $attribute;
}
}
return $this->attributes[$idLang][$idAttributeGroup];
}
/**
* Get all attributes groups for a given language
*
* @param int $idLang Language id
*
* @return array Attributes groups
*/
public function getAttributesGroups($idLang)
{
if (!Combination::isFeatureActive()) {
return [];
}
if (!isset($this->attributesGroup[$idLang])) {
$this->attributesGroup[$idLang] = [];
$tempAttributesGroup = $this->database->executeS(
'SELECT ag.id_attribute_group, ' .
'agl.public_name as attribute_group_name, ' .
'is_color_group, ' .
'IF(liaglv.`url_name` IS NULL OR liaglv.`url_name` = "", NULL, liaglv.`url_name`) AS url_name, ' .
'IF(liaglv.`meta_title` IS NULL OR liaglv.`meta_title` = "", NULL, liaglv.`meta_title`) AS meta_title, ' .
'IFNULL(liag.indexable, TRUE) AS indexable ' .
'FROM `' . _DB_PREFIX_ . 'attribute_group` ag ' .
Shop::addSqlAssociation('attribute_group', 'ag') . ' ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ' .
'ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_group` liag ' .
'ON (ag.`id_attribute_group` = liag.`id_attribute_group`) ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value` AS liaglv ' .
'ON (ag.`id_attribute_group` = liaglv.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
'GROUP BY ag.id_attribute_group ORDER BY ag.`position` ASC'
);
foreach ($tempAttributesGroup as $key => $attributeGroup) {
$this->attributesGroup[$idLang][$attributeGroup['id_attribute_group']] = $attributeGroup;
}
}
return $this->attributesGroup[$idLang];
}
/**
* Get features with their associated layered information
*
* @param int $idLang
*
* @return array|false|\PDOStatement|resource|null
*/
public function getFeatures($idLang)
{
return $this->database->executeS(
'SELECT DISTINCT f.id_feature, f.*, fl.*, ' .
'IF(liflv.`url_name` IS NULL OR liflv.`url_name` = "", NULL, liflv.`url_name`) AS url_name, ' .
'IF(liflv.`meta_title` IS NULL OR liflv.`meta_title` = "", NULL, liflv.`meta_title`) AS meta_title, ' .
'lif.indexable ' .
'FROM `' . _DB_PREFIX_ . 'feature` f ' .
'' . Shop::addSqlAssociation('feature', 'f') . ' ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'feature_lang` fl ON (f.`id_feature` = fl.`id_feature` AND fl.`id_lang` = ' . (int) $idLang . ') ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature` lif ' .
'ON (f.`id_feature` = lif.`id_feature`) ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value` liflv ' .
'ON (f.`id_feature` = liflv.`id_feature` AND liflv.`id_lang` = ' . (int) $idLang . ') ' .
'ORDER BY f.`position` ASC'
);
}
/**
* Get feature values with their associated layered information
*
* @param int $idFeature
* @param int $idLang
*
* @return array|false|\PDOStatement|resource|null
*/
public function getFeatureValues($idFeature, $idLang)
{
return $this->database->executeS(
'SELECT v.*, vl.*, ' .
'IF(lifvlv.`url_name` IS NULL OR lifvlv.`url_name` = "", NULL, lifvlv.`url_name`) AS url_name, ' .
'IF(lifvlv.`meta_title` IS NULL OR lifvlv.`meta_title` = "", NULL, lifvlv.`meta_title`) AS meta_title ' .
'FROM `' . _DB_PREFIX_ . 'feature_value` v ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` vl ' .
'ON (v.`id_feature_value` = vl.`id_feature_value` AND vl.`id_lang` = ' . (int) $idLang . ') ' .
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value` lifvlv ' .
'ON (v.`id_feature_value` = lifvlv.`id_feature_value` AND lifvlv.`id_lang` = ' . (int) $idLang . ') ' .
'WHERE v.`id_feature` = ' . (int) $idFeature . ' ' .
'ORDER BY vl.`value` ASC'
);
}
}

View File

@@ -0,0 +1,168 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use Configuration;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use PrestaShop\Module\FacetedSearch\Product\Search;
use Product;
use Validate;
class Products
{
/**
* Use price tax filter
*
* @var bool
*/
private $psLayeredFilterPriceUsetax;
/**
* Use price rounding
*
* @var bool
*/
private $psLayeredFilterPriceRounding;
/**
* @var AbstractAdapter
*/
private $searchAdapter;
public function __construct(Search $productSearch)
{
$this->searchAdapter = $productSearch->getSearchAdapter();
}
/**
* Get the products associated with the current filters
*
* @param int $productsPerPage
* @param int $page
* @param string $orderBy
* @param string $orderWay
* @param array $selectedFilters
*
* @return array
*/
public function getProductByFilters(
$productsPerPage,
$page,
$orderBy,
$orderWay,
$selectedFilters = []
) {
$orderWay = Validate::isOrderWay($orderWay) ? $orderWay : 'ASC';
$orderBy = Validate::isOrderBy($orderBy) ? $orderBy : 'position';
$this->searchAdapter->setLimit((int) $productsPerPage, ((int) $page - 1) * $productsPerPage);
$this->searchAdapter->setOrderField($orderBy);
$this->searchAdapter->setOrderDirection($orderWay);
$this->searchAdapter->addGroupBy('id_product');
if (isset($selectedFilters['price']) || $orderBy === 'price') {
$this->searchAdapter->addSelectField('id_product');
$this->searchAdapter->addSelectField('price');
$this->searchAdapter->addSelectField('price_min');
$this->searchAdapter->addSelectField('price_max');
}
$matchingProductList = $this->searchAdapter->execute();
$this->pricePostFiltering($matchingProductList, $selectedFilters);
$nbrProducts = $this->searchAdapter->count();
if (empty($nbrProducts)) {
$matchingProductList = [];
}
return [
'products' => $matchingProductList,
'count' => $nbrProducts,
];
}
/**
* Post filter product depending on the price and a few extra config variables
*
* @param array $matchingProductList
* @param array $selectedFilters
*/
private function pricePostFiltering(&$matchingProductList, $selectedFilters)
{
if (!isset($selectedFilters['price'])) {
return;
}
$priceFilter['min'] = (float) ($selectedFilters['price'][0]);
$priceFilter['max'] = (float) ($selectedFilters['price'][1]);
if ($this->psLayeredFilterPriceUsetax === null) {
$this->psLayeredFilterPriceUsetax = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_USETAX');
}
if ($this->psLayeredFilterPriceRounding === null) {
$this->psLayeredFilterPriceRounding = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_ROUNDING');
}
if ($this->psLayeredFilterPriceUsetax || $this->psLayeredFilterPriceRounding) {
$this->filterPrice(
$matchingProductList,
$this->psLayeredFilterPriceUsetax,
$this->psLayeredFilterPriceRounding,
$priceFilter
);
}
}
/**
* Remove products from the product list in case of price postFiltering
*
* @param array $matchingProductList
* @param bool $psLayeredFilterPriceUsetax
* @param bool $psLayeredFilterPriceRounding
* @param array $priceFilter
*/
private function filterPrice(
&$matchingProductList,
$psLayeredFilterPriceUsetax,
$psLayeredFilterPriceRounding,
$priceFilter
) {
/* for this case, price could be out of range, so we need to compute the real price */
foreach ($matchingProductList as $key => $product) {
if (($product['price_min'] < (int) $priceFilter['min'] && $product['price_max'] > (int) $priceFilter['min'])
|| ($product['price_max'] > (int) $priceFilter['max'] && $product['price_min'] < (int) $priceFilter['max'])
) {
$price = Product::getPriceStatic($product['id_product'], $psLayeredFilterPriceUsetax);
if ($psLayeredFilterPriceRounding) {
$price = (int) $price;
}
if ($price < $priceFilter['min'] || $price > $priceFilter['max']) {
// out of range price, exclude the product
unset($matchingProductList[$key]);
}
}
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\Feature;
use Db;
use PrestaShopDatabaseException;
/**
* Provides form data
*/
class FormDataProvider
{
/**
* @var Db
*/
private $database;
public function __construct(Db $database)
{
$this->database = $database;
}
/**
* Fills form data
*
* @param array $params
*
* @return array
*
* @throws PrestaShopDatabaseException
*/
public function getData(array $params)
{
$defaultUrl = [];
$defaultMetaTitle = [];
$isIndexable = false;
// if params contains id, gets data for edit form
if (!empty($params['id'])) {
$featureId = (int) $params['id'];
// returns false if request failed.
$queryIndexable = $this->database->getValue(
'SELECT `indexable` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
'WHERE `id_feature` = ' . $featureId
);
$isIndexable = (bool) $queryIndexable;
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'WHERE `id_feature` = ' . $featureId
);
if (!empty($result) && is_array($result)) {
foreach ($result as $data) {
$defaultUrl[$data['id_lang']] = $data['url_name'];
$defaultMetaTitle[$data['id_lang']] = $data['meta_title'];
}
}
}
return [
'url' => $defaultUrl,
'meta_title' => $defaultMetaTitle,
'is_indexable' => $isIndexable,
];
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Form\Feature;
use Context;
use PrestaShop\Module\FacetedSearch\Constraint\UrlSegment;
use PrestaShopBundle\Form\Admin\Type\SwitchType;
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
use PrestaShopBundle\Translation\TranslatorComponent;
use Symfony\Component\Form\FormBuilderInterface;
/**
* Adds module specific fields to BO form
*/
class FormModifier
{
/**
* @var Context
*/
private $context;
public function __construct(Context $context)
{
$this->context = $context;
}
public function modify(
FormBuilderInterface $formBuilder,
array $data
) {
/**
* @var TranslatorComponent
*/
$translator = $this->context->getTranslator();
$invalidCharsHint = $translator->trans(
'Invalid characters: <>;=#{}_',
[],
'Modules.Facetedsearch.Admin'
);
$urlTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed URLs by choosing ' .
'the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$metaTitleTip = $translator->trans(
'When the Faceted Search module is enabled, you can get more detailed page titles by ' .
'choosing the word that best represents this feature. By default, PrestaShop uses the ' .
'feature\'s name, but you can change that setting using this field.',
[],
'Modules.Facetedsearch.Admin'
);
$formBuilder
->add(
'url_name',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('URL', [], 'Modules.Facetedsearch.Admin'),
'help' => $urlTip . ' ' . $invalidCharsHint,
'options' => [
'constraints' => [
new UrlSegment([
'message' => $translator->trans('%s is invalid.', [], 'Admin.Notifications.Error'),
]),
],
],
'data' => $data['url'],
]
)
->add(
'meta_title',
TranslatableType::class,
[
'required' => false,
'label' => $translator->trans('Meta title', [], 'Modules.Facetedsearch.Admin'),
'help' => $metaTitleTip,
'data' => $data['meta_title'],
]
)
->add(
'layered_indexable',
SwitchType::class,
[
'required' => false,
'label' => $translator->trans('Indexable', [], 'Modules.Facetedsearch.Admin'),
'help' => $translator->trans(
'Use this attribute in URL generated by the Faceted Search module.',
[],
'Modules.Facetedsearch.Admin'
),
'data' => $data['is_indexable'],
]
);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Context;
use Db;
use Ps_Facetedsearch;
abstract class AbstractHook
{
const AVAILABLE_HOOKS = [];
/**
* @var Context
*/
protected $context;
/**
* @var Ps_Facetedsearch
*/
protected $module;
/**
* @var Db
*/
protected $database;
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
$this->context = $module->getContext();
$this->database = $module->getDatabase();
}
/**
* @return array
*/
public function getAvailableHooks()
{
return static::AVAILABLE_HOOKS;
}
}

View File

@@ -0,0 +1,125 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use Tools;
class Attribute extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionAttributeGroupDelete',
'actionAttributeSave',
'displayAttributeForm',
'actionAttributePostProcess',
];
/**
* After save attribute
*
* @param array $params
*/
public function actionAttributeSave(array $params)
{
if (empty($params['id_attribute'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
(`id_attribute`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_attribute'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* After delete attribute
*
* @param array $params
*/
public function actionAttributeGroupDelete(array $params)
{
if (empty($params['id_attribute'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process attribute
*
* @param array $params
*/
public function actionAttributePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Attribute form
*
* @param array $params
*/
public function displayAttributeForm(array $params)
{
$values = [];
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang`
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
WHERE `id_attribute` = ' . (int) $params['id_attribute']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
]);
return $this->module->render('attribute_form.tpl');
}
}

View File

@@ -0,0 +1,145 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use Tools;
class AttributeGroup extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionAttributeGroupDelete',
'actionAttributeGroupSave',
'displayAttributeGroupForm',
'displayAttributeGroupPostProcess',
];
/**
* After save Attributes group
*
* @param array $params
*/
public function actionAttributeGroupSave(array $params)
{
if (empty($params['id_attribute_group']) || Tools::getValue('layered_indexable') === false) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group (`id_attribute_group`, `indexable`)
VALUES (' . (int) $params['id_attribute_group'] . ', ' . (int) Tools::getValue('layered_indexable') . ')'
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
(`id_attribute_group`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_attribute_group'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* After delete attribute group
*
* @param array $params
*/
public function actionAttributeGroupDelete(array $params)
{
if (empty($params['id_attribute_group'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process attribute group
*
* @param array $params
*/
public function displayAttributeGroupPostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Attribute group form
*
* @param array $params
*
* @return string
*/
public function displayAttributeGroupForm(array $params)
{
$values = [];
$isIndexable = $this->database->getValue(
'SELECT `indexable`
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
);
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
'is_indexable' => (bool) $isIndexable,
]);
return $this->module->render('attribute_group_form.tpl');
}
}

View File

@@ -0,0 +1,97 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Tools;
class Category extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionCategoryAdd',
'actionCategoryDelete',
'actionCategoryUpdate',
];
/**
* Category addition
*
* @param array $params
*/
public function actionCategoryAdd(array $params)
{
$this->module->rebuildLayeredCache([], [(int) $params['category']->id]);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Category update
*
* @param array $params
*/
public function actionCategoryUpdate(array $params)
{
/*
* The category status might (active, inactive) have changed,
* we have to update the layered cache table structure
*/
if (isset($params['category']) && !$params['category']->active) {
$this->cleanAndRebuildCategoryFilters($params);
}
}
/**
* Category deletion
*
* @param array $params
*/
public function actionCategoryDelete(array $params)
{
$this->cleanAndRebuildCategoryFilters($params);
}
/**
* Clean and rebuild category filters
*
* @param array $params
*/
private function cleanAndRebuildCategoryFilters(array $params)
{
$layeredFilterList = $this->database->executeS(
'SELECT * FROM ' . _DB_PREFIX_ . 'layered_filter'
);
foreach ($layeredFilterList as $layeredFilter) {
$data = Tools::unSerialize($layeredFilter['filters']);
if (in_array((int) $params['category']->id, $data['categories'])) {
unset($data['categories'][array_search((int) $params['category']->id, $data['categories'])]);
$this->database->execute(
'UPDATE `' . _DB_PREFIX_ . 'layered_filter`
SET `filters` = \'' . pSQL(serialize($data)) . '\'
WHERE `id_layered_filter` = ' . (int) $layeredFilter['id_layered_filter']
);
}
}
$this->module->invalidateLayeredFilterBlockCache();
$this->module->buildLayeredCategories();
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Configuration extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionProductPreferencesPageStockSave',
];
/**
* After save of product stock preferences form
*
* @param array $params
*/
public function actionProductPreferencesPageStockSave(array $params)
{
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Design extends AbstractHook
{
const AVAILABLE_HOOKS = [
'displayLeftColumn',
];
/**
* Force this hook to be called here instance of using WidgetInterface
* because Hook::isHookCallableOn before the instanceof function.
* Which means is_callable always returns true with a __call usage.
*
* @param array $params
*/
public function displayLeftColumn(array $params)
{
return $this->module->fetch('module:ps_facetedsearch/ps_facetedsearch.tpl');
}
}

View File

@@ -0,0 +1,268 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Configuration;
use Language;
use PrestaShop\Module\FacetedSearch\Form\Feature\FormDataProvider;
use PrestaShop\Module\FacetedSearch\Form\Feature\FormModifier;
use PrestaShopDatabaseException;
use Ps_Facetedsearch;
use Tools;
class Feature extends AbstractHook
{
/**
* @var FormModifier
*/
private $formModifier;
/**
* @var FormDataProvider
*/
private $dataProvider;
/**
* @var bool
*/
private $isMigratedPage = false;
public function __construct(Ps_Facetedsearch $module)
{
parent::__construct($module);
$this->formModifier = new FormModifier($module->getContext());
$this->dataProvider = new FormDataProvider($module->getDatabase());
}
const AVAILABLE_HOOKS = [
'actionFeatureSave',
'actionFeatureDelete',
'displayFeatureForm',
'displayFeaturePostProcess',
'actionFeatureFormBuilderModifier',
'actionAfterCreateFeatureFormHandler',
'actionAfterUpdateFeatureFormHandler',
];
/**
* Hook for modifying feature form formBuilder
*
* @param array $params
*
* @throws PrestaShopDatabaseException
*/
public function actionFeatureFormBuilderModifier(array $params)
{
$this->isMigratedPage = true;
$this->formModifier->modify($params['form_builder'], $this->dataProvider->getData($params));
}
/**
* Hook after create feature.
*
* @since PrestaShop 1.7.8.0
*
* @param array $params
*/
public function actionAfterCreateFeatureFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* Hook after update feature.
*
* @since PrestaShop 1.7.8.0
*
* @param array $params
*/
public function actionAfterUpdateFeatureFormHandler(array $params)
{
$this->save($params['id'], $params['form_data']);
}
/**
* Hook after delete a feature
*
* @param array $params
*/
public function actionFeatureDelete(array $params)
{
if (empty($params['id_feature'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . (int) $params['id_feature']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Hook post process feature
*
* @param array $params
*/
public function displayFeaturePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Hook feature form
*
* @param array $params
*/
public function displayFeatureForm(array $params)
{
if ($this->isMigratedPage === true) {
return;
}
$values = [];
$isIndexable = $this->database->getValue(
'SELECT `indexable` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
'WHERE `id_feature` = ' . (int) $params['id_feature']
);
$result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang` ' .
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'WHERE `id_feature` = ' . (int) $params['id_feature']
);
if ($result) {
foreach ($result as $data) {
$values[$data['id_lang']] = [
'url_name' => $data['url_name'],
'meta_title' => $data['meta_title'],
];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
'is_indexable' => (bool) $isIndexable,
]);
return $this->module->render('feature_form.tpl');
}
/**
* After save feature
*
* @param array $params
*/
public function actionFeatureSave(array $params)
{
if (empty($params['id_feature']) || Tools::getValue('layered_indexable') === false) {
return;
}
$featureId = (int) $params['id_feature'];
$formData = [
'layered_indexable' => Tools::getValue('layered_indexable'),
];
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$seoUrl = Tools::getValue('url_name_' . $langId);
$metaTitle = Tools::getValue('meta_title_' . $langId);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$formData['meta_title'][$langId] = $metaTitle;
$formData['url_name'][$langId] = $seoUrl;
}
$this->save($featureId, $formData);
}
/**
* Saves feature form.
*
* @param int $featureId
* @param array $formData
*
* @since PrestaShop 1.7.8.0
*/
private function save($featureId, array $formData)
{
$this->cleanLayeredIndexableTables($featureId);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature
(`id_feature`, `indexable`)
VALUES (' . (int) $featureId . ', ' . (int) $formData['layered_indexable'] . ')'
);
$defaultLangId = (int) Configuration::get('PS_LANG_DEFAULT');
$query = 'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
'(`id_feature`, `id_lang`, `url_name`, `meta_title`) ' .
'VALUES (%d, %d, \'%s\', \'%s\')';
foreach (Language::getLanguages(false) as $language) {
$langId = (int) $language['id_lang'];
$metaTitle = pSQL($formData['meta_title'][$langId]);
$seoUrl = $formData['url_name'][$langId];
$name = $formData['name'][$langId] ?: $formData['name'][$defaultLangId];
if (!empty($seoUrl)) {
$seoUrl = pSQL(Tools::link_rewrite($seoUrl));
}
$this->database->execute(
sprintf(
$query,
$featureId,
$langId,
$seoUrl,
$metaTitle
)
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Deletes from layered_indexable_feature and layered_indexable_feature_lang_value by feature id
*
* @param int $featureId
*/
private function cleanLayeredIndexableTables($featureId)
{
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . $featureId
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
WHERE `id_feature` = ' . $featureId
);
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Language;
use Tools;
class FeatureValue extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionFeatureValueSave',
'actionFeatureValueDelete',
'displayFeatureValueForm',
'displayFeatureValuePostProcess',
];
/**
* After save feature value
*
* @param array $params
*/
public function actionFeatureValueSave(array $params)
{
if (empty($params['id_feature_value'])) {
return;
}
//Removing all indexed language data for this attribute value id
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
if (empty($seoUrl) && empty($metaTitle)) {
continue;
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
(`id_feature_value`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_feature_value'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL($metaTitle, true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* After delete Feature value
*
* @param array $params
*/
public function actionFeatureValueDelete(array $params)
{
if (empty($params['id_feature_value'])) {
return;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
);
$this->module->invalidateLayeredFilterBlockCache();
}
/**
* Post process feature value
*
* @param array $params
*/
public function displayFeatureValuePostProcess(array $params)
{
$this->module->checkLinksRewrite($params);
}
/**
* Display feature value form
*
* @param array $params
*
* @return string
*/
public function displayFeatureValueForm(array $params)
{
$values = [];
if ($result = $this->database->executeS(
'SELECT `url_name`, `meta_title`, `id_lang`
FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
)) {
foreach ($result as $data) {
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
}
}
$this->context->smarty->assign([
'languages' => Language::getLanguages(false),
'default_form_language' => (int) $this->context->controller->default_form_language,
'values' => $values,
]);
return $this->module->render('feature_value_form.tpl');
}
}

View File

@@ -0,0 +1,44 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class Product extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionProductSave',
];
/**
* After save product
*
* @param array $params
*/
public function actionProductSave(array $params)
{
if (empty($params['id_product'])) {
return;
}
$this->module->indexProductPrices((int) $params['id_product']);
$this->module->indexAttributes((int) $params['id_product']);
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,79 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\Module\FacetedSearch\Filters\DataAccessor;
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
use PrestaShop\Module\FacetedSearch\URLSerializer;
class ProductSearch extends AbstractHook
{
const AVAILABLE_HOOKS = [
'productSearchProvider',
];
/**
* Hook project search provider
*
* @param array $params
*
* @return SearchProvider|null
*/
public function productSearchProvider(array $params)
{
$query = $params['query'];
// do something with query,
// e.g. use $query->getIdCategory()
// to choose a template for filters.
// Query is an instance of:
// PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery
if ($query->getIdCategory()) {
$this->context->controller->addJqueryUi('slider');
$this->context->controller->registerStylesheet(
'facetedsearch_front',
'/modules/ps_facetedsearch/views/dist/front.css'
);
$this->context->controller->registerJavascript(
'facetedsearch_front',
'/modules/ps_facetedsearch/views/dist/front.js',
['position' => 'bottom', 'priority' => 100]
);
$urlSerializer = new URLSerializer();
$dataAccessor = new DataAccessor($this->module->getDatabase());
return new SearchProvider(
$this->module,
new Converter(
$this->module->getContext(),
$this->module->getDatabase(),
$urlSerializer,
$dataAccessor
),
$urlSerializer,
$dataAccessor
);
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
class SpecificPrice extends AbstractHook
{
/**
* @var array
*/
protected $productsBefore = null;
const AVAILABLE_HOOKS = [
'actionObjectSpecificPriceRuleUpdateBefore',
'actionAdminSpecificPriceRuleControllerSaveAfter',
];
/**
* Before saving a specific price rule
*
* @param array $params
*/
public function actionObjectSpecificPriceRuleUpdateBefore(array $params)
{
if (empty($params['object']->id)) {
return;
}
/** @var \SpecificPriceRule */
$specificPrice = $params['object'];
$this->productsBefore = $specificPrice->getAffectedProducts();
}
/**
* After saving a specific price rule
*
* @param array $params
*/
public function actionAdminSpecificPriceRuleControllerSaveAfter(array $params)
{
if (empty($params['return']->id) || empty($this->productsBefore)) {
return;
}
/** @var \SpecificPriceRule */
$specificPrice = $params['return'];
$affectedProducts = array_merge($this->productsBefore, $specificPrice->getAffectedProducts());
foreach ($affectedProducts as $product) {
$this->module->indexProductPrices($product['id_product']);
$this->module->indexAttributes($product['id_product']);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,113 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch;
use Ps_Facetedsearch;
/**
* Class works with Hook\AbstractHook instances in order to reduce ps_facetedsearch.php size.
*
* The dispatch method is called from the __call method in the module class.
*/
class HookDispatcher
{
const CLASSES = [
Hook\Attribute::class,
Hook\AttributeGroup::class,
Hook\Category::class,
Hook\Configuration::class,
Hook\Design::class,
Hook\Feature::class,
Hook\FeatureValue::class,
Hook\Product::class,
Hook\ProductSearch::class,
Hook\SpecificPrice::class,
];
/**
* List of available hooks
*
* @var string[]
*/
private $availableHooks = [];
/**
* Hook classes
*
* @var Hook\AbstractHook[]
*/
private $hooks = [];
/**
* Module
*
* @var Ps_Facetedsearch
*/
private $module;
/**
* Init hooks
*
* @param Ps_Facetedsearch $module
*/
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
foreach (self::CLASSES as $hookClass) {
$hook = new $hookClass($this->module);
$this->availableHooks = array_merge($this->availableHooks, $hook->getAvailableHooks());
$this->hooks[] = $hook;
}
}
/**
* Get available hooks
*
* @return string[]
*/
public function getAvailableHooks()
{
return $this->availableHooks;
}
/**
* Find hook and dispatch it
*
* @param string $hookName
* @param array $params
*
* @return mixed
*/
public function dispatch($hookName, array $params = [])
{
$hookName = preg_replace('~^hook~', '', $hookName);
foreach ($this->hooks as $hook) {
if (method_exists($hook, $hookName)) {
return call_user_func([$hook, $hookName], $params);
}
}
// No hook found, render it as a widget
return $this->module->renderWidget($hookName, $params);
}
}

View File

@@ -0,0 +1,332 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Category;
use Configuration;
use Context;
use FrontController;
use Group;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL as MySQLAdapter;
use Tools;
class Search
{
const STOCK_MANAGEMENT_FILTER = 'with_stock_management';
/**
* @var bool
*/
protected $psStockManagement;
/**
* @var bool
*/
protected $psOrderOutOfStock;
/**
* @var AbstractAdapter
*/
protected $searchAdapter;
/**
* @var Context
*/
protected $context;
/**
* Search constructor.
*
* @param Context $context
* @param string $adapterType
*/
public function __construct(Context $context, $adapterType = MySQLAdapter::TYPE)
{
$this->context = $context;
switch ($adapterType) {
case MySQLAdapter::TYPE:
default:
$this->searchAdapter = new MySQLAdapter();
}
if ($this->psStockManagement === null) {
$this->psStockManagement = (bool) Configuration::get('PS_STOCK_MANAGEMENT');
}
if ($this->psOrderOutOfStock === null) {
$this->psOrderOutOfStock = (bool) Configuration::get('PS_ORDER_OUT_OF_STOCK');
}
}
/**
* @return AbstractAdapter
*/
public function getSearchAdapter()
{
return $this->searchAdapter;
}
/**
* Init the initial population of the search filter
*
* @param array $selectedFilters
*/
public function initSearch($selectedFilters)
{
$homeCategory = Configuration::get('PS_HOME_CATEGORY');
/* If the current category isn't defined or if it's homepage, we have nothing to display */
$idParent = (int) Tools::getValue(
'id_category',
Tools::getValue('id_category_layered', $homeCategory)
);
$parent = new Category((int) $idParent);
$psLayeredFullTree = Configuration::get('PS_LAYERED_FULL_TREE');
if (!$psLayeredFullTree) {
$this->addFilter('id_category', [$parent->id]);
}
$psLayeredFilterByDefaultCategory = Configuration::get('PS_LAYERED_FILTER_BY_DEFAULT_CATEGORY');
if ($psLayeredFilterByDefaultCategory) {
$this->addFilter('id_category_default', [$parent->id]);
}
// Visibility of a product must be in catalog or both (search & catalog)
$this->addFilter('visibility', ['both', 'catalog']);
// User must belong to one of the groups that can access the product
if (Group::isFeatureActive()) {
$groups = FrontController::getCurrentCustomerGroups();
$this->addFilter('id_group', empty($groups) ? [Group::getCurrent()->id] : $groups);
}
$this->addSearchFilters(
$selectedFilters,
$psLayeredFullTree ? $parent : null,
(int) $this->context->shop->id
);
}
/**
* @param array $selectedFilters
* @param Category $parent
* @param int $idShop
*/
private function addSearchFilters($selectedFilters, $parent, $idShop)
{
$hasCategory = false;
foreach ($selectedFilters as $key => $filterValues) {
if (!count($filterValues)) {
continue;
}
switch ($key) {
case 'id_feature':
$operationsFilter = [];
foreach ($filterValues as $featureId => $filterValue) {
$this->getSearchAdapter()->addOperationsFilter(
'with_features_' . $featureId,
[[['id_feature_value', $filterValue]]]
);
}
break;
case 'id_attribute_group':
$operationsFilter = [];
foreach ($filterValues as $attributeId => $filterValue) {
$this->getSearchAdapter()->addOperationsFilter(
'with_attributes_' . $attributeId,
[[['id_attribute', $filterValue]]]
);
}
break;
case 'category':
$this->addFilter('id_category', $filterValues);
$this->getSearchAdapter()->resetFilter('id_category_default');
$hasCategory = true;
break;
case 'quantity':
/*
* $filterValues options can have following values:
* 0 - Not available - 0 or less quantity and disabled backorders
* 1 - Available - Positive quantity or enabled backorders
* 2 - In stock - Positive quantity
*/
// If all three values are checked, we show everything
if (count($filterValues) == 3) {
break;
}
// If stock management is deactivated, we show everything
if (!$this->psStockManagement) {
break;
}
$operationsFilter = [];
// Simple cases with 1 option selected
if (count($filterValues) == 1) {
// Not available
if ($filterValues[0] == 0) {
$operationsFilter[] = [
['quantity', [0], '<='],
['out_of_stock', $this->psOrderOutOfStock ? [0] : [0, 2], '='],
];
// Available
} elseif ($filterValues[0] == 1) {
$operationsFilter[] = [
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
// In stock
} elseif ($filterValues[0] == 2) {
$operationsFilter[] = [
['quantity', [0], '>'],
];
}
// Cases with 2 options selected
} elseif (count($filterValues) == 2) {
// Not available and available, we show everything
if (in_array(0, $filterValues) && in_array(1, $filterValues)) {
break;
// Not available or in stock
} elseif (in_array(0, $filterValues) && in_array(2, $filterValues)) {
$operationsFilter[] = [
['quantity', [0], '<='],
['out_of_stock', $this->psOrderOutOfStock ? [0] : [0, 2], '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
// Available or in stock
} elseif (in_array(1, $filterValues) && in_array(2, $filterValues)) {
$operationsFilter[] = [
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
}
}
$this->getSearchAdapter()->addOperationsFilter(
self::STOCK_MANAGEMENT_FILTER,
$operationsFilter
);
break;
case 'manufacturer':
$this->addFilter('id_manufacturer', $filterValues);
break;
case 'condition':
if (count($selectedFilters['condition']) == 3) {
break;
}
$this->addFilter('condition', $filterValues);
break;
case 'weight':
if (!empty($selectedFilters['weight'][0]) || !empty($selectedFilters['weight'][1])) {
$this->getSearchAdapter()->addFilter(
'weight',
[(float) $selectedFilters['weight'][0]],
'>='
);
$this->getSearchAdapter()->addFilter(
'weight',
[(float) $selectedFilters['weight'][1]],
'<='
);
}
break;
case 'price':
if (isset($selectedFilters['price'])
&& (
$selectedFilters['price'][0] !== '' || $selectedFilters['price'][1] !== ''
)
) {
$this->addPriceFilter(
(float) $selectedFilters['price'][0],
(float) $selectedFilters['price'][1]
);
}
break;
}
}
if (!$hasCategory && $parent !== null) {
$this->getSearchAdapter()->addFilter('nleft', [$parent->nleft], '>=');
$this->getSearchAdapter()->addFilter('nright', [$parent->nright], '<=');
}
$this->getSearchAdapter()->addFilter('id_shop', [$idShop]);
$this->getSearchAdapter()->addGroupBy('id_product');
$this->getSearchAdapter()->useFiltersAsInitialPopulation();
}
/**
* Add a filter with the filterValues extracted from the selectedFilters
*
* @param string $filterName
* @param array $filterValues
*/
public function addFilter($filterName, array $filterValues)
{
$values = [];
foreach ($filterValues as $filterValue) {
if (is_array($filterValue)) {
foreach ($filterValue as $subFilterValue) {
$values[] = (int) $subFilterValue;
}
} else {
$values[] = $filterValue;
}
}
if (!empty($values)) {
$this->getSearchAdapter()->addFilter($filterName, $values);
}
}
/**
* Add a price filter
*
* @param float $minPrice
* @param float $maxPrice
*/
private function addPriceFilter($minPrice, $maxPrice)
{
$this->getSearchAdapter()->addFilter('price_min', [$maxPrice], '<=');
$this->getSearchAdapter()->addFilter('price_max', [$minPrice], '>=');
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Context;
class SearchFactory
{
/**
* Returns an instance of Search for this context
*
* @param Context $context
*
* @return Search
*/
public function build(Context $context)
{
return new Search($context);
}
}

View File

@@ -0,0 +1,522 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Configuration;
use PrestaShop\Module\FacetedSearch\Filters;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;
use PrestaShop\PrestaShop\Core\Product\Search\FacetsRendererInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
use Ps_Facetedsearch;
use Tools;
class SearchProvider implements FacetsRendererInterface, ProductSearchProviderInterface
{
/**
* @var Ps_Facetedsearch
*/
private $module;
/**
* @var Filters\Converter
*/
private $filtersConverter;
/**
* @var Filters\DataAccessor
*/
private $dataAccessor;
/**
* @var URLSerializer
*/
private $urlSerializer;
/**
* @var SearchFactory
*/
private $searchFactory;
public function __construct(
Ps_Facetedsearch $module,
Filters\Converter $converter,
URLSerializer $serializer,
Filters\DataAccessor $dataAccessor,
SearchFactory $searchFactory = null
) {
$this->module = $module;
$this->filtersConverter = $converter;
$this->urlSerializer = $serializer;
$this->dataAccessor = $dataAccessor;
$this->searchFactory = $searchFactory === null ? new SearchFactory() : $searchFactory;
}
/**
* @return array
*/
private function getAvailableSortOrders()
{
$sortSalesDesc = new SortOrder('product', 'sales', 'desc');
$sortPosAsc = new SortOrder('product', 'position', 'asc');
$sortNameAsc = new SortOrder('product', 'name', 'asc');
$sortNameDesc = new SortOrder('product', 'name', 'desc');
$sortPriceAsc = new SortOrder('product', 'price', 'asc');
$sortPriceDesc = new SortOrder('product', 'price', 'desc');
$translator = $this->module->getTranslator();
return [
$sortSalesDesc->setLabel(
$translator->trans('Best sellers', [], 'Modules.Facetedsearch.Shop')
),
$sortPosAsc->setLabel(
$translator->trans('Relevance', [], 'Modules.Facetedsearch.Shop')
),
$sortNameAsc->setLabel(
$translator->trans('Name, A to Z', [], 'Shop.Theme.Catalog')
),
$sortNameDesc->setLabel(
$translator->trans('Name, Z to A', [], 'Shop.Theme.Catalog')
),
$sortPriceAsc->setLabel(
$translator->trans('Price, low to high', [], 'Shop.Theme.Catalog')
),
$sortPriceDesc->setLabel(
$translator->trans('Price, high to low', [], 'Shop.Theme.Catalog')
),
];
}
/**
* @param ProductSearchContext $context
* @param ProductSearchQuery $query
*
* @return ProductSearchResult
*/
public function runQuery(
ProductSearchContext $context,
ProductSearchQuery $query
) {
$result = new ProductSearchResult();
// extract the filter array from the Search query
$facetedSearchFilters = $this->filtersConverter->createFacetedSearchFiltersFromQuery($query);
$context = $this->module->getContext();
$facetedSearch = $this->searchFactory->build($context);
// init the search with the initial population associated with the current filters
$facetedSearch->initSearch($facetedSearchFilters);
$orderBy = $query->getSortOrder()->toLegacyOrderBy(false);
$orderWay = $query->getSortOrder()->toLegacyOrderWay();
$filterProductSearch = new Filters\Products($facetedSearch);
// get the product associated with the current filter
$productsAndCount = $filterProductSearch->getProductByFilters(
$query->getResultsPerPage(),
$query->getPage(),
$orderBy,
$orderWay,
$facetedSearchFilters
);
$result
->setProducts($productsAndCount['products'])
->setTotalProductsCount($productsAndCount['count'])
->setAvailableSortOrders($this->getAvailableSortOrders());
// now get the filter blocks associated with the current search
$filterBlockSearch = new Filters\Block(
$facetedSearch->getSearchAdapter(),
$context,
$this->module->getDatabase(),
$this->dataAccessor
);
$idShop = (int) $context->shop->id;
$idLang = (int) $context->language->id;
$idCurrency = (int) $context->currency->id;
$idCountry = (int) $context->country->id;
$idCategory = (int) $query->getIdCategory();
$filterHash = md5(
sprintf(
'%d-%d-%d-%d-%d-%s',
$idShop,
$idCurrency,
$idLang,
$idCategory,
$idCountry,
serialize($facetedSearchFilters)
)
);
$filterBlock = $filterBlockSearch->getFromCache($filterHash);
if (empty($filterBlock)) {
$filterBlock = $filterBlockSearch->getFilterBlock($productsAndCount['count'], $facetedSearchFilters);
$filterBlockSearch->insertIntoCache($filterHash, $filterBlock);
}
$facets = $this->filtersConverter->getFacetsFromFilterBlocks(
$filterBlock['filters']
);
$this->labelRangeFilters($facets);
$this->addEncodedFacetsToFilters($facets);
$this->hideUselessFacets($facets, (int) $result->getTotalProductsCount());
$facetCollection = new FacetCollection();
$nextMenu = $facetCollection->setFacets($facets);
$result->setFacetCollection($nextMenu);
$facetFilters = $this->urlSerializer->getActiveFacetFiltersFromFacets($facets);
$result->setEncodedFacets($this->urlSerializer->serialize($facetFilters));
return $result;
}
/**
* Renders an product search result.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return string the HTML of the facets
*/
public function renderFacets(ProductSearchContext $context, ProductSearchResult $result)
{
list($activeFilters, $displayedFacets, $facetsVar) = $this->prepareActiveFiltersForRender($context, $result);
// No need to render without facets
if (empty($facetsVar)) {
return '';
}
$this->module->getContext()->smarty->assign(
[
'show_quantities' => Configuration::get('PS_LAYERED_SHOW_QTIES'),
'facets' => $facetsVar,
'js_enabled' => $this->module->isAjax(),
'displayedFacets' => $displayedFacets,
'activeFilters' => $activeFilters,
'sort_order' => $result->getCurrentSortOrder()->toString(),
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
return $this->module->fetch(
'module:ps_facetedsearch/views/templates/front/catalog/facets.tpl'
);
}
/**
* Renders an product search result of active filters.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return string the HTML of the facets
*/
public function renderActiveFilters(ProductSearchContext $context, ProductSearchResult $result)
{
list($activeFilters) = $this->prepareActiveFiltersForRender($context, $result);
$this->module->getContext()->smarty->assign(
[
'activeFilters' => $activeFilters,
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
return $this->module->fetch(
'module:ps_facetedsearch/views/templates/front/catalog/active-filters.tpl'
);
}
/**
* Prepare active filters for renderer.
*
* @param ProductSearchContext $context
* @param ProductSearchResult $result
*
* @return array|null
*/
private function prepareActiveFiltersForRender(ProductSearchContext $context, ProductSearchResult $result)
{
$facetCollection = $result->getFacetCollection();
// not all search providers generate menus
if (empty($facetCollection)) {
return null;
}
$facetsVar = array_map(
[$this, 'prepareFacetForTemplate'],
$facetCollection->getFacets()
);
$displayedFacets = [];
$activeFilters = [];
foreach ($facetsVar as $idx => $facet) {
// Remove undisplayed facets
if (!empty($facet['displayed'])) {
$displayedFacets[] = $facet;
}
// Check if a filter is active
foreach ($facet['filters'] as $filter) {
if ($filter['active']) {
$activeFilters[] = $filter;
}
}
}
return [
$activeFilters,
$displayedFacets,
$facetsVar,
];
}
/**
* Converts a Facet to an array with all necessary
* information for templating.
*
* @param Facet $facet
*
* @return array ready for templating
*/
protected function prepareFacetForTemplate(Facet $facet)
{
$facetsArray = $facet->toArray();
foreach ($facetsArray['filters'] as &$filter) {
$filter['facetLabel'] = $facet->getLabel();
if ($filter['nextEncodedFacets'] || $facet->getWidgetType() === 'slider') {
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
'q' => $filter['nextEncodedFacets'],
'page' => null,
]);
} else {
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
'q' => null,
]);
}
}
unset($filter);
return $facetsArray;
}
/**
* Add a label associated with the facets
*
* @param array $facets
*/
private function labelRangeFilters(array $facets)
{
$context = $this->module->getContext();
foreach ($facets as $facet) {
if (!in_array($facet->getType(), Filters\Converter::RANGE_FILTERS)) {
continue;
}
foreach ($facet->getFilters() as $filter) {
$filterValue = $filter->getValue();
$min = empty($filterValue[0]) ? $facet->getProperty('min') : $filterValue[0];
$max = empty($filterValue[1]) ? $facet->getProperty('max') : $filterValue[1];
if ($facet->getType() === 'weight') {
$unit = Configuration::get('PS_WEIGHT_UNIT');
$filter->setLabel(
sprintf(
'%1$s%2$s - %3$s%4$s',
Tools::displayNumber($min),
$unit,
Tools::displayNumber($max),
$unit
)
);
} elseif ($facet->getType() === 'price') {
$filter->setLabel(
sprintf(
'%1$s - %2$s',
$context->getCurrentLocale()->formatPrice($min, $context->currency->iso_code),
$context->getCurrentLocale()->formatPrice($max, $context->currency->iso_code)
)
);
}
}
}
}
/**
* This method generates a URL stub for each filter inside the given facets
* and assigns this stub to the filters.
* The URL stub is called 'nextEncodedFacets' because it is used
* to generate the URL of the search once a filter is activated.
*/
private function addEncodedFacetsToFilters(array $facets)
{
// first get the currently active facetFilter in an array
$originalFacetFilters = $this->urlSerializer->getActiveFacetFiltersFromFacets($facets);
foreach ($facets as $facet) {
$activeFacetFilters = $originalFacetFilters;
// If only one filter can be selected, we keep track of
// the current active filter to disable it before generating the url stub
// and not select two filters in a facet that can have only one active filter.
if (!$facet->isMultipleSelectionAllowed() && !$facet->getProperty('range')) {
foreach ($facet->getFilters() as $filter) {
if ($filter->isActive()) {
// we have a currently active filter is the facet, remove it from the facetFilter array
$activeFacetFilters = $this->urlSerializer->removeFilterFromFacetFilters(
$originalFacetFilters,
$filter,
$facet
);
break;
}
}
}
foreach ($facet->getFilters() as $filter) {
// toggle the current filter
if ($filter->isActive() || $facet->getProperty('range')) {
$facetFilters = $this->urlSerializer->removeFilterFromFacetFilters(
$activeFacetFilters,
$filter,
$facet
);
} else {
$facetFilters = $this->urlSerializer->addFilterToFacetFilters(
$activeFacetFilters,
$filter,
$facet
);
}
// We've toggled the filter, so the call to serialize
// returns the "URL" for the search when user has toggled
// the filter.
$filter->setNextEncodedFacets(
$this->urlSerializer->serialize($facetFilters)
);
}
}
}
/**
* Remove the facet when there's only 1 result.
* Keep facet status when it's a slider
*
* @param array $facets
* @param int $totalProducts
*/
private function hideUselessFacets(array $facets, $totalProducts)
{
foreach ($facets as $facet) {
if ($facet->getWidgetType() === 'slider') {
$facet->setDisplayed(
$facet->getProperty('min') != $facet->getProperty('max')
);
continue;
}
$totalFacetProducts = 0;
$usefulFiltersCount = 0;
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() > 0 && $filter->isDisplayed()) {
$totalFacetProducts += $filter->getMagnitude();
++$usefulFiltersCount;
}
}
$facet->setDisplayed(
// There are two filters displayed
$usefulFiltersCount > 1
||
/*
* There is only one fitler and the
* magnitude is different than the
* total products
*/
(
count($facet->getFilters()) === 1
&& $totalFacetProducts < $totalProducts
&& $usefulFiltersCount > 0
)
);
}
}
/**
* Generate a URL corresponding to the current page but
* with the query string altered.
*
* Params from $extraParams that have a null value are stripped,
* and other params are added. Params not in $extraParams are unchanged.
*/
private function updateQueryString(array $extraParams = [])
{
$uriWithoutParams = explode('?', $_SERVER['REQUEST_URI'])[0];
$url = Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $uriWithoutParams;
$params = [];
$paramsFromUri = '';
if (strpos($_SERVER['REQUEST_URI'], '?') !== false) {
$paramsFromUri = explode('?', $_SERVER['REQUEST_URI'])[1];
}
parse_str($paramsFromUri, $params);
foreach ($extraParams as $key => $value) {
if (null === $value) {
// Force clear param if null value is passed
unset($params[$key]);
} else {
$params[$key] = $value;
}
}
foreach ($params as $key => $param) {
if (null === $param || '' === $param) {
unset($params[$key]);
}
}
$queryString = str_replace('%2F', '/', http_build_query($params, '', '&'));
return $url . ($queryString ? "?$queryString" : '');
}
}

View File

@@ -0,0 +1,245 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
namespace PrestaShop\Module\FacetedSearch;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class URLSerializer
{
/**
* Add filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function addFilterToFacetFilters(array $facetFilters, Filter $facetFilter, Facet $facet)
{
$facetLabel = $this->getFacetLabel($facet);
$filterLabel = $this->getFilterLabel($facetFilter);
if ($facet->getProperty('range')) {
$facetValue = $facet->getProperty('values');
$facetFilters[$facetLabel] = [
$facetFilter->getProperty('symbol'),
isset($facetValue[0]) ? $facetValue[0] : $facet->getProperty('min'),
isset($facetValue[1]) ? $facetValue[1] : $facet->getProperty('max'),
];
} else {
$facetFilters[$facetLabel][$filterLabel] = $filterLabel;
}
return $facetFilters;
}
/**
* Remove filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function removeFilterFromFacetFilters(array $facetFilters, Filter $facetFilter, $facet)
{
$facetLabel = $this->getFacetLabel($facet);
if ($facet->getProperty('range')) {
unset($facetFilters[$facetLabel]);
} else {
$filterLabel = $this->getFilterLabel($facetFilter);
unset($facetFilters[$facetLabel][$filterLabel]);
if (empty($facetFilters[$facetLabel])) {
unset($facetFilters[$facetLabel]);
}
}
return $facetFilters;
}
/**
* Get active facet filters
*
* @return array
*/
public function getActiveFacetFiltersFromFacets(array $facets)
{
$facetFilters = [];
foreach ($facets as $facet) {
foreach ($facet->getFilters() as $facetFilter) {
if (!$facetFilter->isActive()) {
// Filter is not active
continue;
}
$facetLabel = $this->getFacetLabel($facet);
$filterLabel = $this->getFilterLabel($facetFilter);
if (!$facet->getProperty('range')) {
$facetFilters[$facetLabel][$filterLabel] = $filterLabel;
continue;
}
$facetValue = $facetFilter->getValue();
$facetFilters[$facetLabel] = [
$facetFilter->getProperty('symbol'),
$facetValue[0],
$facetValue[1],
];
}
}
return $facetFilters;
}
/**
* Get Facet label
*
* @param Facet $facet
*
* @return string
*/
private function getFacetLabel(Facet $facet)
{
if ($facet->getProperty(Converter::PROPERTY_URL_NAME) !== null) {
return $facet->getProperty(Converter::PROPERTY_URL_NAME);
}
return $facet->getLabel();
}
/**
* Get Facet Filter label
*
* @param Filter $facetFilter
*
* @return string
*/
private function getFilterLabel(Filter $facetFilter)
{
if ($facetFilter->getProperty(Converter::PROPERTY_URL_NAME) !== null) {
return $facetFilter->getProperty(Converter::PROPERTY_URL_NAME);
}
return $facetFilter->getLabel();
}
/**
* @param array $fragment
*
* @return string
*/
public function serialize(array $fragment)
{
$parts = [];
foreach ($fragment as $key => $values) {
array_unshift($values, $key);
$parts[] = $this->serializeListOfStrings($values, '-');
}
return $this->serializeListOfStrings($parts, '/');
}
/**
* @param string $string
*
* @return array
*/
public function unserialize($string)
{
$fragment = [];
$parts = $this->unserializeListOfStrings($string, '/');
foreach ($parts as $part) {
$values = $this->unserializeListOfStrings($part, '-');
$key = array_shift($values);
$fragment[$key] = $values;
}
return $fragment;
}
/**
* @param string $separator the string separator
* @param string $escape the string escape
* @param array $list
*
* @return string
*/
private function serializeListOfStrings($list, $separator, $escape = '\\')
{
return implode($separator, array_map(function ($item) use ($separator, $escape) {
return strtr(
$item,
[
$separator => $escape . $separator,
]
);
}, $list));
}
/**
* @param string $separator the string separator
* @param string $escape the string escape
* @param string $string the UTF8 string
*
* @return array
*/
private function unserializeListOfStrings($string, $separator, $escape = '\\')
{
$list = [];
$currentString = '';
$escaping = false;
// get UTF-8 chars, inspired from http://stackoverflow.com/questions/9438158/split-utf8-string-into-array-of-chars
$arrayOfCharacters = [];
preg_match_all('/./u', $string, $arrayOfCharacters);
$characters = $arrayOfCharacters[0];
foreach ($characters as $index => $character) {
if ($character === $escape
&& isset($characters[$index + 1])
&& $characters[$index + 1] === $separator
) {
$escaping = true;
continue;
}
if ($character === $separator && $escaping === false) {
$list[] = $currentString;
$currentString = '';
continue;
}
$currentString .= $character;
$escaping = false;
}
if ('' !== $currentString) {
$list[] = $currentString;
}
return $list;
}
}

View File

@@ -0,0 +1,64 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_0_0(Ps_Facetedsearch $module)
{
// Clear legacy hook names
$oldHooks = [
'categoryAddition',
'categoryUpdate',
'attributeGroupForm',
'afterSaveAttributeGroup',
'afterDeleteAttributeGroup',
'featureForm',
'afterDeleteFeature',
'afterSaveFeature',
'categoryDeletion',
'afterSaveProduct',
'postProcessAttributeGroup',
'postProcessFeature',
'featureValueForm',
'postProcessFeatureValue',
'afterDeleteFeatureValue',
'afterSaveFeatureValue',
'attributeForm',
'postProcessAttribute',
'afterDeleteAttribute',
'afterSaveAttribute',
'productSearchProvider',
'displayLeftColumn',
];
foreach ($oldHooks as $hookName) {
$module->unregisterHook($hookName);
}
// These methods have no return value
// If something failed an exception will be raised and
// the upgrade will stop
$module->rebuildLayeredStructure();
$module->rebuildPriceIndexTable();
$module->invalidateLayeredFilterBlockCache();
return $module->registerHook($module->getHookDispatcher()->getAvailableHooks());
}

View File

@@ -0,0 +1,43 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_0_3(Ps_Facetedsearch $module)
{
// Clear legacy hook names
$oldHooks = [
'afterDeleteFeatureValue',
'afterSaveFeatureValue',
'postProcessFeatureValue',
];
foreach ($oldHooks as $hookName) {
$module->unregisterHook($hookName);
}
$newHooks = [
'actionFeatureSave',
'actionFeatureValueDelete',
'displayFeatureValuePostProcess',
];
return $module->registerHook($newHooks);
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_3_0(Ps_Facetedsearch $module)
{
// These methods have no return value
// If something failed an exception will be raised and
// the upgrade will stop
$module->rebuildPriceIndexTable();
$module->invalidateLayeredFilterBlockCache();
return $module->registerHook($module->getHookDispatcher()->getAvailableHooks());
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_4_0(Ps_Facetedsearch $module)
{
$newHooks = [
'actionFeatureFormBuilderModifier',
'actionAfterCreateFeatureFormHandler',
'actionAfterUpdateFeatureFormHandler',
];
return $module->registerHook($newHooks);
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
/**
* Removes files or directories.
*
* @param array $files An array of files to remove
*
* @return true|string True if everything goes fine, error details otherwise
*/
function removeFromFsDuringUpgrade(array $files)
{
$files = array_reverse($files);
foreach ($files as $file) {
if (is_dir($file)) {
$iterator = new FilesystemIterator($file, FilesystemIterator::CURRENT_AS_PATHNAME | FilesystemIterator::SKIP_DOTS);
removeFromFsDuringUpgrade(iterator_to_array($iterator));
if (!rmdir($file) && file_exists($file)) {
return 'Deletion of directory ' . $file . 'failed';
}
} elseif (!unlink($file) && file_exists($file)) {
return 'Deletion of file ' . $file . 'failed';
}
}
return true;
}
/**
* This upgrade file removes the folder vendor/phpunit, when added from a previous release installed on the shop.
*
* @return bool
*/
function upgrade_module_3_4_1($module)
{
$path = __DIR__ . '/../vendor/phpunit';
if (file_exists($path)) {
$result = removeFromFsDuringUpgrade([$path]);
if ($result !== true) {
PrestaShopLogger::addLog('Could not delete PHPUnit from module. ' . $result, 3);
return false;
}
}
return true;
}

View File

@@ -0,0 +1,29 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_6_0($module)
{
Configuration::updateValue('PS_LAYERED_CACHE_ENABLED', 1);
return true;
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_3_8_0($module)
{
$module->registerHook('actionProductPreferencesPageStockSave');
return Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'layered_price_index`
CHANGE `price_min` `price_min` decimal(20,6) NOT NULL,
CHANGE `price_max` `price_max` decimal(20,6) NOT NULL;');
}

View File

@@ -0,0 +1,4 @@
.bootstrap .filter_list .filter_list_item{display:table;width:100%;padding:5px 0;margin-bottom:4px;background-color:white;-webkit-box-shadow:rgba(0,0,0,0.3) 0 0 3px,rgba(0,0,0,0.1) 0 -2px 0 inset;box-shadow:rgba(0,0,0,0.3) 0 0 3px,rgba(0,0,0,0.1) 0 -2px 0 inset;-webkit-border-radius:3px;-moz-border-radius:3px;-ms-border-radius:3px;-o-border-radius:3px;border-radius:3px;cursor:pointer}.bootstrap .filter_panel{min-height:20px;padding:7px 7px 0px 7px;margin-bottom:20px;background-color:#ebebeb;border:1px solid #d9d9d9;border-radius:3px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.bootstrap .filter_panel header{margin-bottom:7px}.bootstrap .prestashop-switch span{display:none}
/*# sourceMappingURL=back.css.map*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
.faceted-overlay{left:0;top:0;width:100%;height:100%;position:fixed;background-color:rgba(25,25,25,0.5);z-index:100}.faceted-overlay .overlay__inner{left:0;top:0;width:100%;height:100%;position:absolute}.faceted-overlay .overlay__content{left:50%;position:absolute;top:50%;transform:translate(-50%, -50%)}.faceted-overlay .spinner{width:75px;height:75px;display:inline-block;border-width:2px;border-color:rgba(255,255,255,0.05);border-top-color:#fff;animation:spin 1s infinite linear;border-radius:100%;border-style:solid}@keyframes spin{100%{transform:rotate(360deg)}}
#search_filters .ui-slider-horizontal .ui-slider-handle{margin-left:-1px;cursor:pointer}#search_filters .ui-widget-header{background:#555}#search_filters .ui-slider .ui-slider-handle{top:-.45em;width:0.4em;background:#fff;border:1px solid #555}#search_filters .ui-slider-horizontal{height:.4em}
#search_filters .facet .title{display:flex}#search_filters .facet .title .collapse-icons{margin-left:auto}#search_filters .facet .facet-title{width:calc(100% - 30px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}#search_filters .facet .facet-label{width:100%;text-align:left}#search_filters .facet .facet-label .custom-checkbox,#search_filters .facet .facet-label .custom-radio{top:-7px;margin-right:0}#search_filters .facet .facet-label .color{margin-left:0}#search_filters .facet .facet-label a{width:calc(100% - 30px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/*# sourceMappingURL=front.css.map*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../');
exit;

View File

@@ -0,0 +1,28 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{function get_limit_select element=''}
{assign var="name" value="{$element}_filter_show_limit"}
<select name="{$name}">
<option value="0">{l s='No limit' d='Modules.Facetedsearch.Admin'}</option>
{for $index=2 to 20}
<option value="{$index}">{$index}</option>
{/for}
</select>
{/function}

View File

@@ -0,0 +1,30 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
<div class="form-group">
<label class="control-label col-lg-3">{l s='Categories used for this template:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
{if trim($categories_tree) != ''}
{$categories_tree}
{else}
<div class="alert alert-warning">
{l s='Categories selection is disabled because you have no categories or you are in a "all shops" context.' d='Modules.Facetedsearch.Admin'}
</div>
{/if}
</div>
</div>

View File

@@ -0,0 +1,31 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
<div class="panel-footer" id="toolbar-footer">
<button class="btn btn-default pull-right" id="submit-filter" name="SubmitFilter" type="submit"><i class="process-icon-save"></i> <span>{l s='Save' d='Admin.Actions'}</span></button>
<a class="btn btn-default" href="{$current_url}">
<i class="process-icon-cancel"></i> <span>{l s='Cancel' d='Admin.Actions'}</span>
</a>
</div>
<script type="text/javascript">
var translations = new Array();
{if isset($filters)}var filters = '{$filters|@json_encode}';{/if}
translations['no_selected_categories'] = "{l s='You must select at least one category' d='Modules.Facetedsearch.Admin'}";
translations['no_selected_filters'] = "{l s='You must select at least one filter' d='Modules.Facetedsearch.Admin'}";
</script>

View File

@@ -0,0 +1,25 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
<div class="form-group">
<label class="control-label col-lg-3">{l s='Template name:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<input type="text" id="layered_tpl_name" name="layered_tpl_name" maxlength="64" value="{$template_name}" />
<p class="help-block">{l s='Only as a reminder' d='Modules.Facetedsearch.Admin'}</p>
</div>
</div>

View File

@@ -0,0 +1,43 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{if isset($message)}{$message}{/if}
<div id="ajax-message-ok" class="conf ajax-message alert alert-success" style="display: none">
<span class="message"></span>
</div>
<div id="ajax-message-ko" class="error ajax-message alert alert-danger" style="display: none">
<span class="message"></span>
</div>
{if !empty($limit_warning)}
<div class="alert alert-danger">
{if $limit_warning['error_type'] == 'suhosin'}
{l s='Warning! Your hosting provider is using the Suhosin patch for PHP, which limits the maximum number of fields allowed in a form:' d='Modules.Facetedsearch.Admin'}
<b>{$limit_warning['post.max_vars']}</b> {l s='for suhosin.post.max_vars.' d='Modules.Facetedsearch.Admin'}<br/>
<b>{$limit_warning['request.max_vars']}</b> {l s='for suhosin.request.max_vars.' d='Modules.Facetedsearch.Admin'}<br/>
{l s='Please ask your hosting provider to increase the Suhosin limit to' d='Modules.Facetedsearch.Admin'}
{else}
{l s='Warning! Your PHP configuration limits the maximum number of fields allowed in a form:' d='Modules.Facetedsearch.Admin'}<br/>
<b>{$limit_warning['max_input_vars']}</b> {l s='for max_input_vars.' d='Modules.Facetedsearch.Admin'}<br/>
{l s='Please ask your hosting provider to increase this limit to' d='Modules.Facetedsearch.Admin'}
{/if}
{l s='%s at least, or you will have to edit the translation files manually.' sprintf=$limit_warning['needed_limit'] d='Modules.Facetedsearch.Admin'}
</div>
{/if}

View File

@@ -0,0 +1,24 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{if isset($asso_shops)}
<div class="form-group">
<label class="control-label col-lg-3">{l s='Choose shop association:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">{$asso_shops}</div>
</div>
{/if}

View File

@@ -0,0 +1,345 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{include file='./_partials/messages.tpl'}
{include file='./_functions/show_limit.tpl'}
<div class="panel">
<h3><i class="icon-cogs"></i> {l s='New filters template' d='Modules.Facetedsearch.Admin'}</h3>
<form action="{$current_url}" method="post" class="form-horizontal" onsubmit="return checkForm();">
<input type="hidden" name="id_layered_filter" id="id_layered_filter" value="{$id_layered_filter}" />
{include file='./_partials/header.tpl'}
{include file='./_partials/categories-tree.tpl'}
{include file='./_partials/shops.tpl'}
<div class="form-group">
<label class="control-label col-lg-3">
<span class="badge" id="selected_filters">0</span>
<span class="label-tooltip" data-toggle="tooltip" title="" data-original-title="{l s='You can drag and drop filters to adjust position' d='Modules.Facetedsearch.Admin'}">{l s='Filters:' d='Modules.Facetedsearch.Admin'}</span>
</label>
<div class="col-lg-9">
<section class="filter_panel">
<header class="clearfix">
<span class="badge pull-right">
{l
s='Total filters: %s'
sprintf=[$total_filters]
d='Modules.Facetedsearch.Admin'
}
</span>
</header>
<section class="filter_list">
<ul class="list-unstyled sortable">
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_subcategories" id="layered_selection_subcategories" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<h4>{l s='Sub-categories filter' d='Modules.Facetedsearch.Admin'}</h4>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_subcategories"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_subcategories_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_stock" id="layered_selection_stock" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">{l s='Product stock filter' d='Modules.Facetedsearch.Admin'}</span>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_stock"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_stock_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_condition" id="layered_selection_condition" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">{l s='Product condition filter' d='Modules.Facetedsearch.Admin'}</span>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_condition"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_condition_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_manufacturer" id="layered_selection_manufacturer" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">{l s='Product brand filter' d='Modules.Facetedsearch.Admin'}</span>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_manufacturer"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_manufacturer_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_weight_slider" id="layered_selection_weight_slider" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">{l s='Product weight filter (slider)' d='Modules.Facetedsearch.Admin'}</span>
</div>
<div class="col-lg-3 pull-right">
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<input type="hidden" name="layered_selection_weight_slider_filter_type" value="1">
<p class="form-control-static">{l s='List of ranges' d='Modules.Facetedsearch.Admin'}</p>
</div>
</div>
</li>
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_price_slider" id="layered_selection_price_slider" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">{l s='Product price filter (slider)' d='Modules.Facetedsearch.Admin'}</span>
</div>
<div class="col-lg-3 pull-right">
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<input type="hidden" name="layered_selection_price_slider_filter_type" value="1">
<p class="form-control-static">{l s='List of ranges' d='Modules.Facetedsearch.Admin'}</p>
</div>
</div>
</li>
{if $attribute_groups|count > 0}
{foreach $attribute_groups as $attribute_group}
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}" id="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">
{if $attribute_group['n'] > 1}
{l
s='Attribute group: %name% (%count% attributes)'
sprintf=[
'%name%' => $attribute_group['name'],
'%count%' => $attribute_group['n']
]
d='Modules.Facetedsearch.Admin'
}
{else}
{l
s='Attribute group: %name% (%count% attribute)'
sprintf=[
'%name%' => $attribute_group['name'],
'%count%' => $attribute_group['n']
]
d='Modules.Facetedsearch.Admin'
}
{/if}
{if $attribute_group['is_color_group']}
<img src="../img/admin/color_swatch.png" alt="" title="{l s='This group will allow user to select a color' d='Modules.Facetedsearch.Admin'}" />
{/if}
</span>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
{/foreach}
{/if}
{if $features|count > 0}
{foreach $features as $feature}
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_feat_{(int)$feature['id_feature']}" id="layered_selection_feat_{(int)$feature['id_feature']}" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">
{if $feature['n'] > 1}
{l
s='Feature: %name% (%count% values)'
sprintf=[
'%name%' => $feature['name'],
'%count%' => $feature['n']
]
d='Modules.Facetedsearch.Admin'
}
{else}
{l
s='Feature: %name% (%count% value)'
sprintf=[
'%name%' => $feature['name'],
'%count%' => $feature['n']
]
d='Modules.Facetedsearch.Admin'
}
{/if}
</span>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_feat_{(int)$feature['id_feature']}"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_feat_{(int)$feature['id_feature']}_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
{/foreach}
{/if}
</ul>
</section>
</section>
</div>
</div>
{include file='./_partials/footer.tpl'}
</form>
</div>

View File

@@ -0,0 +1,28 @@
<?php
/**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*/
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Cache-Control: post-check=0, pre-check=0', false);
header('Pragma: no-cache');
header('Location: ../../../../');
exit;

View File

@@ -0,0 +1,266 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{include file='./_partials/messages.tpl'}
<div class="panel">
<h3><i class="icon-cogs"></i> {l s='Indexes and caches' d='Modules.Facetedsearch.Admin'}</h3>
<div id="indexing-warning" class="alert alert-warning" style="display: none">
{l s='Indexing is in progress. Please do not leave this page' d='Modules.Facetedsearch.Admin'}
</div>
<div class="row">
<p>
<a class="ajaxcall-recurcive btn btn-default" href="{$price_indexer_url}">{l s='Index all missing prices' d='Modules.Facetedsearch.Admin'}</a>
<a class="ajaxcall-recurcive btn btn-default" href="{$full_price_indexer_url}">{l s='Rebuild entire price index' d='Modules.Facetedsearch.Admin'}</a>
<a class="ajaxcall btn btn-default" href="{$attribute_indexer_url}">{l s='Build attributes and features indexes' d='Modules.Facetedsearch.Admin'}</a>
<a class="ajaxcall btn btn-default" href="{$clear_cache_url}">{l s='Clear cache' d='Modules.Facetedsearch.Admin'}</a>
</p>
</div>
<div class="row">
<div class="alert alert-info">
{l s='You can set a cron job that will rebuild price index using the following URL:' d='Modules.Facetedsearch.Admin'}
<br>
<strong>{$price_indexer_url}</strong>
<br>
<br>
{l s='You can set a cron job that will rebuild attribute index using the following URL:' d='Modules.Facetedsearch.Admin'}
<br>
<strong>{$attribute_indexer_url}</strong>
</div>
</div>
<div class="row">
<div class="alert alert-info">{l s='A nightly rebuild is recommended.' d='Modules.Facetedsearch.Admin'}</div>
</div>
</div>
<div class="panel">
<h3><i class="icon-cogs"></i> {l s='Filters templates' d='Modules.Facetedsearch.Admin'}<span class="badge">{$filters_templates|count}</span></h3>
{if $filters_templates|count > 0}
<div class="row">
<table class="table">
<thead>
<tr>
<th class="fixed-width-xs center"><span class="title_box">{l s='ID' d='Admin.Global'}</span></th>
<th><span class="title_box text-left">{l s='Name' d='Admin.Global'}</span></th>
<th class="fixed-width-sm center"><span class="title_box">{l s='Categories' d='Admin.Global'}</span></th>
<th class="fixed-width-lg"><span class="title_box">{l s='Created on' d='Modules.Facetedsearch.Admin'}</span></th>
<th class="fixed-width-sm"><span class="title_box text-right">{l s='Actions' d='Modules.Facetedsearch.Admin'}</span></th>
</tr>
</thead>
<tbody>
{foreach $filters_templates as $template}
<tr>
<td class="center">{(int)$template['id_layered_filter']}</td>
<td class="text-left">{$template['name']}</td>
<td class="center">{(int)$template['n_categories']}</td>
<td>{Tools::displayDate($template['date_add'],null , true)}</td>
<td>
{if empty($limit_warning)}
<div class="btn-group-action">
<div class="btn-group pull-right">
<a href="{$current_url}&amp;edit_filters_template=1&amp;id_layered_filter={(int)$template['id_layered_filter']}" class="btn btn-default">
<i class="icon-pencil"></i> {l s='Edit' d='Admin.Actions'}
</a>
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>&nbsp;
</button>
<ul class="dropdown-menu">
<li>
<a href="{$current_url}&amp;deleteFilterTemplate=1&amp;id_layered_filter={(int)$template['id_layered_filter']}"
onclick="return confirm('{l s='Do you really want to delete this filter template?' d='Modules.Facetedsearch.Admin'}');">
<i class="icon-trash"></i> {l s='Delete' d='Admin.Actions'}
</a>
</li>
</ul>
</div>
</div>
{/if}
</td>
</tr>
{/foreach}
</tbody>
</table>
<div class="clearfix">&nbsp;</div>
</div>
{else}
<div class="row alert alert-warning">{l s='No filter template found.' d='Modules.Facetedsearch.Admin'}</div>
{/if}
{if empty($limit_warning)}
<div class="panel-footer">
<a class="btn btn-default pull-right" href="{$current_url}&amp;add_new_filters_template=1"><i class="process-icon-plus"></i> {l s='Add new template' d='Modules.Facetedsearch.Admin'}</a>
</div>
{/if}
</div>
<div class="panel">
<h3><i class="icon-cogs"></i> {l s='Configuration' d='Admin.Global'}</h3>
<form action="{$current_url}" method="post" class="form-horizontal">
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Enable cache system' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" name="ps_layered_cache_enabled" id="ps_layered_cache_enabled_on" value="1"{if $cache_enabled} checked="checked"{/if}>
<label for="ps_layered_cache_enabled_on" class="radioCheck">
<i class="color_success"></i> {l s='Yes' d='Admin.Global'}
</label>
<input type="radio" name="ps_layered_cache_enabled" id="ps_layered_cache_enabled_off" value="0"{if !$cache_enabled} checked="checked"{/if}>
<label for="ps_layered_cache_enabled_off" class="radioCheck">
<i class="color_danger"></i> {l s='No' d='Admin.Global'}
</label>
<a class="slide-button btn"></a>
</span>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Show the number of matching products' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" name="ps_layered_show_qties" id="ps_layered_show_qties_on" value="1"{if $show_quantities} checked="checked"{/if}>
<label for="ps_layered_show_qties_on" class="radioCheck">
<i class="color_success"></i> {l s='Yes' d='Admin.Global'}
</label>
<input type="radio" name="ps_layered_show_qties" id="ps_layered_show_qties_off" value="0"{if !$show_quantities} checked="checked"{/if}>
<label for="ps_layered_show_qties_off" class="radioCheck">
<i class="color_danger"></i> {l s='No' d='Admin.Global'}
</label>
<a class="slide-button btn"></a>
</span>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Show products from subcategories' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" name="ps_layered_full_tree" id="ps_layered_full_tree_on" value="1"{if $full_tree} checked="checked"{/if}>
<label for="ps_layered_full_tree_on" class="radioCheck">
<i class="color_success"></i> {l s='Yes' d='Admin.Global'}
</label>
<input type="radio" name="ps_layered_full_tree" id="ps_layered_full_tree_off" value="0"{if !$full_tree} checked="checked"{/if}>
<label for="ps_layered_full_tree_off" class="radioCheck">
<i class="color_danger"></i> {l s='No' d='Admin.Global'}
</label>
<a class="slide-button btn"></a>
</span>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Show products only from default category' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" name="ps_layered_filter_by_default_category" id="ps_layered_filter_by_default_category_on" value="1"{if $filter_by_default_category} checked="checked"{/if}>
<label for="ps_layered_filter_by_default_category_on" class="radioCheck">
<i class="color_success"></i> {l s='Yes' d='Admin.Global'}
</label>
<input type="radio" name="ps_layered_filter_by_default_category" id="ps_layered_filter_by_default_category_off" value="0"{if !$filter_by_default_category} checked="checked"{/if}>
<label for="ps_layered_filter_by_default_category_off" class="radioCheck">
<i class="color_danger"></i> {l s='No' d='Admin.Global'}
</label>
<a class="slide-button btn"></a>
</span>
</div>
<div class="col-lg-9 col-lg-offset-3">
<div class="help-block">
{l s='Works only if "Show products from subcategories" is off.' d='Modules.Facetedsearch.Admin'}
</div>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Category filter depth (0 for no limits, 1 by default)' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<input type="text" name="ps_layered_filter_category_depth" value="{if $category_depth !== false}{$category_depth}{else}1{/if}" class="fixed-width-sm" />
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Use tax to filter price' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" name="ps_layered_filter_price_usetax" id="ps_layered_filter_price_usetax_on" value="1"{if $price_use_tax} checked="checked"{/if}>
<label for="ps_layered_filter_price_usetax_on" class="radioCheck">
<i class="color_success"></i> {l s='Yes' d='Admin.Global'}
</label>
<input type="radio" name="ps_layered_filter_price_usetax" id="ps_layered_filter_price_usetax_off" value="0"{if !$price_use_tax} checked="checked"{/if}>
<label for="ps_layered_filter_price_usetax_off" class="radioCheck">
<i class="color_danger"></i> {l s='No' d='Admin.Global'}
</label>
<a class="slide-button btn"></a>
</span>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Use rounding to filter price' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" name="ps_layered_filter_price_rounding" id="ps_layered_filter_price_rounding_on" value="1"{if $price_use_rounding} checked="checked"{/if}/>
<label for="ps_layered_filter_price_rounding_on" class="radioCheck">
<i class="color_success"></i> {l s='Yes' d='Admin.Global'}
</label>
<input type="radio" name="ps_layered_filter_price_rounding" id="ps_layered_filter_price_rounding_off" value="0"{if !$price_use_rounding} checked="checked"{/if}/>
<label for="ps_layered_filter_price_rounding_off" class="radioCheck">
<i class="color_danger"></i> {l s='No' d='Admin.Global'}
</label>
<a class="slide-button btn"></a>
</span>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">{l s='Show unavailable, out of stock last' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-9">
<span class="switch prestashop-switch fixed-width-lg">
<input type="radio" name="ps_layered_filter_show_out_of_stock_last" id="ps_layered_filter_show_out_of_stock_last_on" value="1"{if $show_out_of_stock_last} checked="checked"{/if}/>
<label for="ps_layered_filter_show_out_of_stock_last_on" class="radioCheck">
<i class="color_success"></i> {l s='Yes' d='Admin.Global'}
</label>
<input type="radio" name="ps_layered_filter_show_out_of_stock_last" id="ps_layered_filter_show_out_of_stock_last_off" value="0"{if !$show_out_of_stock_last} checked="checked"{/if}/>
<label for="ps_layered_filter_show_out_of_stock_last_off" class="radioCheck">
<i class="color_danger"></i> {l s='No' d='Admin.Global'}
</label>
<a class="slide-button btn"></a>
</span>
</div>
</div>
<div class="panel-footer">
<button type="submit" class="btn btn-default pull-right" name="submitLayeredSettings"><i class="process-icon-save"></i> {l s='Save' d='Admin.Actions'}</button>
</div>
</form>
</div>
<script type="text/javascript">
{if isset($PS_LAYERED_INDEXED)}var PS_LAYERED_INDEXED = {$PS_LAYERED_INDEXED};{/if}
var token = '{$token}';
var id_lang = {$id_lang};
var base_folder = '{$base_folder}';
var translations = new Object();
translations.in_progress = '{l s='(in progress)' js=1 d='Modules.Facetedsearch.Admin'}';
translations.url_indexation_finished = '{l s='URL indexing finished' js=1 d='Modules.Facetedsearch.Admin'}';
translations.attribute_indexation_finished = '{l s='Attribute indexing finished' js=1 d='Modules.Facetedsearch.Admin'}';
translations.url_indexation_failed = '{l s='URL indexing failed' js=1 d='Modules.Facetedsearch.Admin'}';
translations.attribute_indexation_failed = '{l s='Attribute indexing failed' js=1 d='Modules.Facetedsearch.Admin'}';
translations.price_indexation_finished = '{l s='Price indexing finished' js=1 d='Modules.Facetedsearch.Admin'}';
translations.price_indexation_failed = '{l s='Price indexing failed' js=1 d='Modules.Facetedsearch.Admin'}';
translations.price_indexation_in_progress = '{l s='(in progress, %s products price to index)' js=1 d='Modules.Facetedsearch.Admin'}';
translations.loading = '{l s='Loading...' js=1 d='Modules.Facetedsearch.Admin'}';
translations.delete_all_filters_templates = '{l s='You selected -All categories-: all existing filter templates will be deleted. Is it OK?' js=1 d='Modules.Facetedsearch.Admin'}';
translations.no_selected_categories = '{l s='You must select at least one category' js=1 d='Modules.Facetedsearch.Admin'}';
</script>

View File

@@ -0,0 +1,215 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{include file='./_partials/messages.tpl'}
{include file='./_functions/show_limit.tpl'}
<div class="panel">
<h3><i class="icon-cogs"></i> {l s='New filters template' d='Modules.Facetedsearch.Admin'}</h3>
<form action="{$current_url}" method="post" class="form-horizontal" onsubmit="return checkForm();">
<input type="hidden" name="id_layered_filter" id="id_layered_filter" value="{$id_layered_filter}" />
{include file='./_partials/header.tpl'}
{include file='./_partials/categories-tree.tpl'}
{include file='./_partials/shops.tpl'}
<div class="form-group">
<label class="control-label col-lg-3">
<span class="badge" id="selected_filters">0</span>
<span class="label-tooltip" data-toggle="tooltip" title="" data-original-title="{l s='You can drag and drop filters to adjust position' d='Modules.Facetedsearch.Admin'}">{l s='Filters:' d='Modules.Facetedsearch.Admin'}</span>
</label>
<div class="col-lg-9">
<section class="filter_panel">
<header class="clearfix">
<span class="badge pull-right">
{l
s='Total filters: %s'
sprintf=[$total_filters]
d='Modules.Facetedsearch.Admin'
}
</span>
</header>
<section class="filter_list">
<ul id="" class="list-unstyled sortable">
{foreach from=$default_filters item=filter key=filterId}
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="{$filterId}" id="{$filterId}" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<h4>{l s=$filter['label'] d='Modules.Facetedsearch.Admin'}</h4>
</div>
<div class="col-lg-3">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
{if !empty($filter['slider'])}
<p class="form-control-static">{l s='List of ranges' d='Modules.Facetedsearch.Admin'}</p>
{else}
<div class="col-lg-6">
<select name="{$filterId}_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
{/if}
</div>
<div class="col-lg-3">
{if empty($filter['slider'])}
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element=$filterId}
</div>
{/if}
</div>
</li>
{/foreach}
{if $attribute_groups|count > 0}
{foreach $attribute_groups as $attribute_group}
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}" id="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">
{if $attribute_group['n'] > 1}
{l
s='Attribute group: %name% (%count% attributes)'
sprintf=[
'%name%' => $attribute_group['name'],
'%count%' => $attribute_group['n']
]
d='Modules.Facetedsearch.Admin'
}
{else}
{l
s='Attribute group: %name% (%count% attribute)'
sprintf=[
'%name%' => $attribute_group['name'],
'%count%' => $attribute_group['n']
]
d='Modules.Facetedsearch.Admin'
}
{/if}
{if $attribute_group['is_color_group']}
<img src="../img/admin/color_swatch.png" alt="" title="{l s='This group will allow user to select a color' d='Modules.Facetedsearch.Admin'}" />
{/if}
</span>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_ag_{(int)$attribute_group['id_attribute_group']}_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
{/foreach}
{/if}
{if $features|count > 0}
{foreach $features as $feature}
<li class="filter_list_item row" draggable="true">
<div class="col-lg-2">
<label class="switch-light prestashop-switch fixed-width-lg">
<input name="layered_selection_feat_{(int)$feature['id_feature']}" id="layered_selection_feat_{(int)$feature['id_feature']}" type="checkbox" />
<span>
<span>{l s='Yes' d='Admin.Global'}</span>
<span>{l s='No' d='Admin.Global'}</span>
</span>
<a class="slide-button btn"></a>
</label>
</div>
<div class="col-lg-4">
<span class="module_name">
{if $feature['n'] > 1}
{l
s='Feature: %name% (%count% values)'
sprintf=[
'%name%' => $feature['name'],
'%count%' => $feature['n']
]
d='Modules.Facetedsearch.Admin'
}
{else}
{l
s='Feature: %name% (%count% value)'
sprintf=[
'%name%' => $feature['name'],
'%count%' => $feature['n']
]
d='Modules.Facetedsearch.Admin'
}
{/if}
</span>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter result limit:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
{call get_limit_select element="layered_selection_feat_{(int)$feature['id_feature']}"}
</div>
</div>
<div class="col-lg-3 pull-right">
<label class="control-label col-lg-6">{l s='Filter style:' d='Modules.Facetedsearch.Admin'}</label>
<div class="col-lg-6">
<select name="layered_selection_feat_{(int)$feature['id_feature']}_filter_type">
<option value="0">{l s='Checkbox' d='Modules.Facetedsearch.Admin'}</option>
<option value="1">{l s='Radio button' d='Modules.Facetedsearch.Admin'}</option>
<option value="2">{l s='Drop-down list' d='Modules.Facetedsearch.Admin'}</option>
</select>
</div>
</div>
</li>
{/foreach}
{/if}
</ul>
</section>
</section>
</div>
</div>
{include file='./_partials/footer.tpl'}
</form>
</div>

View File

@@ -0,0 +1,37 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
<section id="js-active-search-filters" class="{if $activeFilters|count}active_filters{else}hide{/if}">
{block name='active_filters_title'}
<p class="h6 {if $activeFilters|count}active-filter-title{else}hidden-xs-up{/if}">{l s='Active filters' d='Shop.Theme.Global'}</p>
{/block}
{if $activeFilters|count}
<ul>
{foreach from=$activeFilters item="filter"}
{block name='active_filters_item'}
<li class="filter-block">
{l s='%1$s:' d='Shop.Theme.Catalog' sprintf=[$filter.facetLabel]}
{$filter.label}
<a class="js-search-link" href="{$filter.nextEncodedFacetsURL}"><i class="material-icons close">&#xE5CD;</i></a>
</li>
{/block}
{/foreach}
</ul>
{/if}
</section>

View File

@@ -0,0 +1,185 @@
{**
* Copyright since 2007 PrestaShop SA and Contributors
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
*
* NOTICE OF LICENSE
*
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
* that is bundled with this package in the file LICENSE.md.
* It is also available through the world-wide-web at this URL:
* https://opensource.org/licenses/AFL-3.0
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to license@prestashop.com so we can send you a copy immediately.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright Since 2007 PrestaShop SA and Contributors
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
*}
{if $displayedFacets|count}
<div id="search_filters">
{block name='facets_clearall_button'}
{if $activeFilters|count}
<div id="_desktop_search_filters_clear_all" class="hidden-sm-down clear-all-wrapper">
<button data-search-url="{$clear_all_link}" class="btn btn-tertiary js-search-filters-clear-all">
<i class="material-icons">&#xE14C;</i>
{l s='Clear all' d='Shop.Theme.Actions'}
</button>
</div>
{/if}
{/block}
{foreach from=$displayedFacets item="facet" key="k"}
{assign var="facet_color" value=false}
{foreach from=$facet.filters key=filter_key item="filter"}
{if isset($filter.properties.color)}
{$facet_color = true}
{/if}
{/foreach}
<section class="facet {if $facet_color == true}facet-color{/if} clearfix" type="{$facet.type}">
<p class="h6 facet-title hidden-sm-down">{$facet.label}</p>
{assign var=_expand_id value=10|mt_rand:100000}
{assign var=_collapse value=true}
{foreach from=$facet.filters item="filter"}
{if $filter.active}{assign var=_collapse value=false}{/if}
{/foreach}
<div class="title hidden-md-up" data-target="#facet_{$_expand_id}" data-toggle="collapse"{if !$_collapse} aria-expanded="true"{/if}>
<p class="h6 facet-title">{$facet.label}</p>
<span class="navbar-toggler collapse-icons">
<i class="material-icons add">&#xE313;</i>
<i class="material-icons remove">&#xE316;</i>
</span>
</div>
{if in_array($facet.widgetType, ['radio', 'checkbox'])}
{block name='facet_item_other'}
<ul id="facet_{$_expand_id}" class="collapse{if !$_collapse} in{/if}">
{foreach from=$facet.filters key=filter_key item="filter"}
{if !$filter.displayed}
{continue}
{/if}
<li>
<label class="facet-label{if $filter.active} active {/if}" for="facet_input_{$_expand_id}_{$filter_key}" id="filter_{$filter_key}">
{if $facet.multipleSelectionAllowed}
<span class="custom-checkbox">
<input
id="facet_input_{$_expand_id}_{$filter_key}"
data-search-url="{$filter.nextEncodedFacetsURL}"
type="checkbox"
{if $filter.active }checked{/if}
>
{if isset($filter.properties.color)}
<span class="color" style="background-color:{$filter.properties.color}"></span>
{elseif isset($filter.properties.texture)}
<span class="color texture" style="background-image:url({$filter.properties.texture})"></span>
{else}
<span {if !$js_enabled} class="ps-shown-by-js" {/if}><i class="material-icons rtl-no-flip checkbox-checked">&#xE5CA;</i></span>
{/if}
</span>
{else}
<span class="custom-radio">
<input
id="facet_input_{$_expand_id}_{$filter_key}"
data-search-url="{$filter.nextEncodedFacetsURL}"
type="radio"
name="filter {$facet.label}"
{if $filter.active }checked{/if}
>
<span {if !$js_enabled} class="ps-shown-by-js" {/if}></span>
</span>
{/if}
<a
href="{$filter.nextEncodedFacetsURL}"
class="_gray-darker search-link js-search-link"
rel="nofollow"
>
{$filter.label}
{if $filter.magnitude and $show_quantities}
<span class="magnitude">({$filter.magnitude})</span>
{/if}
</a>
</label>
</li>
{/foreach}
</ul>
{/block}
{elseif $facet.widgetType == 'dropdown'}
{block name='facet_item_dropdown'}
<ul id="facet_{$_expand_id}" class="collapse{if !$_collapse} in{/if}">
<li>
<div class="col-sm-12 col-xs-12 col-md-12 facet-dropdown dropdown">
<a class="select-title" rel="nofollow" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{$active_found = false}
<span>
{foreach from=$facet.filters item="filter"}
{if $filter.active}
{$filter.label}
{if $filter.magnitude and $show_quantities}
({$filter.magnitude})
{/if}
{$active_found = true}
{/if}
{/foreach}
{if !$active_found}
{l s='(no filter)' d='Shop.Theme.Global'}
{/if}
</span>
<i class="material-icons float-xs-right">&#xE5C5;</i>
</a>
<div class="dropdown-menu">
{foreach from=$facet.filters item="filter"}
{if !$filter.active}
<a
rel="nofollow"
href="{$filter.nextEncodedFacetsURL}"
class="select-list js-search-link"
>
{$filter.label}
{if $filter.magnitude and $show_quantities}
({$filter.magnitude})
{/if}
</a>
{/if}
{/foreach}
</div>
</div>
</li>
</ul>
{/block}
{elseif $facet.widgetType == 'slider'}
{block name='facet_item_slider'}
{foreach from=$facet.filters item="filter"}
<ul id="facet_{$_expand_id}"
class="faceted-slider collapse{if !$_collapse} in{/if}"
data-slider-min="{$facet.properties.min}"
data-slider-max="{$facet.properties.max}"
data-slider-id="{$_expand_id}"
data-slider-values="{$filter.value|@json_encode}"
data-slider-unit="{$facet.properties.unit}"
data-slider-label="{$facet.label}"
data-slider-specifications="{$facet.properties.specifications|@json_encode}"
data-slider-encoded-url="{$filter.nextEncodedFacetsURL}"
>
<li>
<p id="facet_label_{$_expand_id}">
{$filter.label}
</p>
<div id="slider-range_{$_expand_id}"></div>
</li>
</ul>
{/foreach}
{/block}
{/if}
</section>
{/foreach}
</div>
{/if}

Some files were not shown because too many files have changed in this diff Show More