[IMP] website_sale, *: add products searchbar snippet
* website When the e-commerce is installed, you can now add a new snippet that can be dropped anywhere: a search bar to search for products with an autocomplete feature. That snippet is also used as default "/shop" search bar. Finally, you have the possibility to customize the snippet via snippet options. task-1866973 closes odoo/odoo#30366 Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com> Co-authored-by: qsm-odoo <qsm@odoo.com>
This commit is contained in:
committed by
qsm-odoo
co-authored by
qsm-odoo
parent
c26eb41f9e
commit
ce1da231b9
Binary file not shown.
|
After Width: | Height: | Size: 295 B |
@@ -893,11 +893,13 @@
|
||||
<template id="external_snippets" inherit_id="website.snippets" priority="8">
|
||||
<xpath expr="//div[@id='snippet_feature']//t[@t-snippet][last()]" position="after">
|
||||
<t t-install="website_twitter" string="Twitter Scroller" t-thumbnail="/website/static/src/img/snippets_thumbs/s_twitter_scroll.png"/>
|
||||
<t id="s_products_searchbar" t-install="website_sale" string="Products Search" t-thumbnail="/website/static/src/img/snippets_thumbs/s_products_searchbar.png"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='snippet_content']//t[@t-snippet][last()]" position="after">
|
||||
<t t-install="website_event" string="Local Events" t-thumbnail="/website/static/src/img/snippets_thumbs/s_local_events.png"/>
|
||||
<t t-install="website_mail_channel" string="Discussion Group" t-thumbnail="/website/static/src/img/snippets_thumbs/s_button_channel_subscribe.png"/>
|
||||
<t t-install="mass_mailing" string="Newsletter" t-thumbnail="/website/static/src/img/snippets_thumbs/s_newsletter_subscribe_form.png"/>
|
||||
<t id="s_products_searchbar_input" t-install="website_sale" string="Products Search Input" t-thumbnail="/website/static/src/img/snippets_thumbs/s_products_searchbar.png"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -157,18 +157,24 @@ class WebsiteSale(http.Controller):
|
||||
def _get_search_order(self, post):
|
||||
# OrderBy will be parsed in orm and so no direct sql injection
|
||||
# id is added to be sure that order is a unique sort key
|
||||
return 'is_published desc,%s , id desc' % post.get('order', 'website_sequence desc')
|
||||
order = post.get('order') or 'website_sequence desc'
|
||||
return 'is_published desc, %s, id desc' % order
|
||||
|
||||
def _get_search_domain(self, search, category, attrib_values):
|
||||
domain = request.website.sale_product_domain()
|
||||
def _get_search_domain(self, search, category, attrib_values, search_in_description=True):
|
||||
domains = [request.website.sale_product_domain()]
|
||||
if search:
|
||||
for srch in search.split(" "):
|
||||
domain += [
|
||||
'|', '|', '|', ('name', 'ilike', srch), ('description', 'ilike', srch),
|
||||
('description_sale', 'ilike', srch), ('product_variant_ids.default_code', 'ilike', srch)]
|
||||
subdomains = [
|
||||
[('name', 'ilike', srch)],
|
||||
[('product_variant_ids.default_code', 'ilike', srch)]
|
||||
]
|
||||
if search_in_description:
|
||||
subdomains.append([('description', 'ilike', srch)])
|
||||
subdomains.append([('description_sale', 'ilike', srch)])
|
||||
domains.append(expression.OR(subdomains))
|
||||
|
||||
if category:
|
||||
domain += [('public_categ_ids', 'child_of', int(category))]
|
||||
domains.append([('public_categ_ids', 'child_of', int(category))])
|
||||
|
||||
if attrib_values:
|
||||
attrib = None
|
||||
@@ -180,13 +186,13 @@ class WebsiteSale(http.Controller):
|
||||
elif value[0] == attrib:
|
||||
ids.append(value[1])
|
||||
else:
|
||||
domain += [('attribute_line_ids.value_ids', 'in', ids)]
|
||||
domains.append([('attribute_line_ids.value_ids', 'in', ids)])
|
||||
attrib = value[0]
|
||||
ids = [value[1]]
|
||||
if attrib:
|
||||
domain += [('attribute_line_ids.value_ids', 'in', ids)]
|
||||
domains.append([('attribute_line_ids.value_ids', 'in', ids)])
|
||||
|
||||
return domain
|
||||
return expression.AND(domains)
|
||||
|
||||
@http.route([
|
||||
'''/shop''',
|
||||
@@ -1114,3 +1120,75 @@ class WebsiteSale(http.Controller):
|
||||
lang = request.env['res.lang']._lang_get(request.env.context.get('lang') or 'en_US')
|
||||
return lang.format(fmt, currency.round(amount), grouping=True, monetary=True)\
|
||||
.replace(r' ', u'\N{NO-BREAK SPACE}').replace(r'-', u'-\N{ZERO WIDTH NO-BREAK SPACE}')
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Products Search Bar
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@http.route('/shop/products/autocomplete', type='json', auth='public', website=True)
|
||||
def products_autocomplete(self, term, options={}, **kwargs):
|
||||
"""
|
||||
Returns list of products according to the term and product options
|
||||
|
||||
Params:
|
||||
term (str): search term written by the user
|
||||
options (dict)
|
||||
- 'limit' (int), default to 5: number of products to consider
|
||||
- 'display_description' (bool), default to True
|
||||
- 'display_price' (bool), default to True
|
||||
- 'order' (str)
|
||||
- 'max_nb_chars' (int): max number of characters for the
|
||||
description if returned
|
||||
|
||||
Returns:
|
||||
dict (or False if no result)
|
||||
- 'products' (list): products (only their needed field values)
|
||||
note: the prices will be strings properly formatted and
|
||||
already containing the currency
|
||||
- 'products_count' (int): the number of products in the database
|
||||
that matched the search query
|
||||
"""
|
||||
ProductTemplate = request.env['product.template']
|
||||
|
||||
display_description = options.get('display_description', True)
|
||||
display_price = options.get('display_price', True)
|
||||
order = self._get_search_order(options)
|
||||
max_nb_chars = options.get('max_nb_chars', 999)
|
||||
|
||||
category = options.get('category')
|
||||
attrib_values = options.get('attrib_values')
|
||||
|
||||
domain = self._get_search_domain(term, category, attrib_values, display_description)
|
||||
products = ProductTemplate.search(
|
||||
domain,
|
||||
limit=min(20, options.get('limit', 5)),
|
||||
order=order
|
||||
)
|
||||
|
||||
fields = ['id', 'name', 'website_url']
|
||||
if display_description:
|
||||
fields.append('description_sale')
|
||||
|
||||
res = {
|
||||
'products': products.read(fields),
|
||||
'products_count': ProductTemplate.search_count(domain),
|
||||
}
|
||||
|
||||
if display_description:
|
||||
for res_product in res['products']:
|
||||
desc = res_product['description_sale']
|
||||
if desc and len(desc) > max_nb_chars:
|
||||
res_product['description_sale'] = "%s..." % desc[:(max_nb_chars - 3)]
|
||||
|
||||
if display_price:
|
||||
FieldMonetary = request.env['ir.qweb.field.monetary']
|
||||
monetary_options = {
|
||||
'display_currency': request.website.get_current_pricelist().currency_id,
|
||||
}
|
||||
for res_product, product in zip(res['products'], products):
|
||||
combination_info = product._get_combination_info(only_template=True)
|
||||
res_product.update(combination_info)
|
||||
res_product['list_price'] = FieldMonetary.value_to_html(res_product['list_price'], monetary_options)
|
||||
res_product['price'] = FieldMonetary.value_to_html(res_product['price'], monetary_options)
|
||||
|
||||
return res
|
||||
|
||||
@@ -52,15 +52,31 @@ WebsiteNewMenu.include({
|
||||
odoo.define('website_sale.editor', function (require) {
|
||||
'use strict';
|
||||
|
||||
require('web.dom_ready');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var options = require('web_editor.snippets.options');
|
||||
var publicWidget = require('web.public.widget');
|
||||
|
||||
if (!$('.js_sale').length) {
|
||||
return Promise.reject("DOM doesn't contain '.js_sale'");
|
||||
}
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
|
||||
$('.oe_website_sale').on('click', '.oe_currency_value:o_editable', function (ev) {
|
||||
$(ev.currentTarget).selectContent();
|
||||
publicWidget.registry.websiteSaleCurrency = publicWidget.Widget.extend({
|
||||
selector: '.oe_website_sale',
|
||||
disabledInEditableMode: false,
|
||||
edit_events: {
|
||||
'click .oe_currency_value:o_editable': '_onCurrencyValueClick',
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onCurrencyValueClick: function (ev) {
|
||||
$(ev.currentTarget).selectContent();
|
||||
},
|
||||
});
|
||||
|
||||
options.registry.website_sale = options.Class.extend({
|
||||
@@ -160,4 +176,64 @@ options.registry.website_sale = options.Class.extend({
|
||||
}).then(this.reload);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles the edition of products search bar snippet.
|
||||
*/
|
||||
options.registry.ProductsSearchBar = options.Class.extend({
|
||||
xmlDependencies: ['/website_sale/static/src/xml/website_sale.editor.xml'],
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$searchProductsInput = this.$('.search-query');
|
||||
this.$searchOrderField = this.$('.o_wsale_search_order_by');
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @see this.selectClass for parameters
|
||||
*/
|
||||
openSearchbarSettings: function (previewMode, value, $opt) {
|
||||
var self = this;
|
||||
new Dialog(this, {
|
||||
title: _t("Products Search Bar"),
|
||||
$content: $(qweb.render('website_sale.dialog.productsSearchBar', {
|
||||
currentOrderBy: this.$searchOrderField.val(),
|
||||
currentLimit: parseInt(this.$searchProductsInput.attr('data-limit')),
|
||||
currentDisplayDescription: this.$searchProductsInput.attr('data-display-description') === 'true',
|
||||
currentDisplayPrice: this.$searchProductsInput.attr('data-display-price') === 'true',
|
||||
currentDisplayImage: this.$searchProductsInput.attr('data-display-image') === 'true',
|
||||
})),
|
||||
buttons: [
|
||||
{
|
||||
text: _t("Save"),
|
||||
classes: 'btn-primary',
|
||||
click: function () {
|
||||
self.$searchOrderField.attr({
|
||||
'value': this.$('#order_by').val(),
|
||||
});
|
||||
self.$searchProductsInput.attr({
|
||||
'data-limit': this.$('#use_autocomplete').is(':checked') ? this.$('#limit').val() : 0,
|
||||
'data-display-description': this.$('#display_description').is(':checked'),
|
||||
'data-display-price': this.$('#display_price').is(':checked'),
|
||||
'data-display-image': this.$('#display_image').is(':checked'),
|
||||
});
|
||||
self.$target.trigger('content_changed');
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _t("Discard"),
|
||||
close: true,
|
||||
},
|
||||
],
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,10 +120,13 @@ odoo.define('website_sale.website_sale', function (require) {
|
||||
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
var concurrency = require('web.concurrency');
|
||||
var publicWidget = require('web.public.widget');
|
||||
var VariantMixin = require('sale.VariantMixin');
|
||||
require("website.content.zoomodoo");
|
||||
|
||||
var qweb = core.qweb;
|
||||
|
||||
publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, {
|
||||
selector: '.oe_website_sale',
|
||||
events: _.extend({}, VariantMixin.events || {}, {
|
||||
@@ -139,7 +142,7 @@ publicWidget.registry.WebsiteSale = publicWidget.Widget.extend(VariantMixin, {
|
||||
'touchend form.js_add_cart_json label': '_onMouseupAddCartLabel',
|
||||
'change .css_attribute_color input': '_onChangeColorAttribute',
|
||||
'click .show_coupon': '_onClickShowCoupon',
|
||||
'submit .o_website_sale_search': '_onSubmitSaleSearch',
|
||||
'submit .o_wsale_products_searchbar_form': '_onSubmitSaleSearch',
|
||||
'change select[name="country_id"]': '_onChangeCountry',
|
||||
'change #shipping_use_same': '_onChangeShippingUseSame',
|
||||
'click .toggle_summary': '_onToggleSummary',
|
||||
@@ -733,4 +736,132 @@ publicWidget.registry.websiteSaleCart = publicWidget.Widget.extend({
|
||||
$(ev.currentTarget).closest('tr').find('.js_quantity').val(0).trigger('change');
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @todo maybe the custom autocomplete logic could be extract to be reusable
|
||||
*/
|
||||
publicWidget.registry.productsSearchBar = publicWidget.Widget.extend({
|
||||
selector: '.o_wsale_products_searchbar_form',
|
||||
xmlDependencies: ['/website_sale/static/src/xml/website_sale_utils.xml'],
|
||||
events: {
|
||||
'input .search-query': '_onInput',
|
||||
'focusout': '_onFocusOut',
|
||||
'keydown .search-query': '_onKeydown',
|
||||
},
|
||||
autocompleteMinWidth: 300,
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._dp = new concurrency.DropPrevious();
|
||||
|
||||
this._onInput = _.debounce(this._onInput, 400);
|
||||
this._onFocusOut = _.debounce(this._onFocusOut, 100);
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.$input = this.$('.search-query');
|
||||
|
||||
this.order = this.$('.o_wsale_search_order_by').val();
|
||||
this.limit = parseInt(this.$input.data('limit'));
|
||||
this.displayDescription = !!this.$input.data('displayDescription');
|
||||
this.displayPrice = !!this.$input.data('displayPrice');
|
||||
this.displayImage = !!this.$input.data('displayImage');
|
||||
|
||||
if (this.limit) {
|
||||
this.$input.attr('autocomplete', 'off');
|
||||
}
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_fetch: function () {
|
||||
return this._rpc({
|
||||
route: '/shop/products/autocomplete',
|
||||
params: {
|
||||
'term': this.$input.val(),
|
||||
'options': {
|
||||
'order': this.order,
|
||||
'limit': this.limit,
|
||||
'display_description': this.displayDescription,
|
||||
'display_price': this.displayPrice,
|
||||
'max_nb_chars': Math.round(Math.max(this.autocompleteMinWidth, parseInt(this.$el.width())) * 0.22),
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_render: function (res) {
|
||||
var $prevMenu = this.$menu;
|
||||
this.$el.toggleClass('dropdown show', !!res);
|
||||
if (res) {
|
||||
var products = res['products'];
|
||||
this.$menu = $(qweb.render('website_sale.productsSearchBar.autocomplete', {
|
||||
products: products,
|
||||
hasMoreProducts: products.length < res['products_count'],
|
||||
currency: res['currency'],
|
||||
widget: this,
|
||||
}));
|
||||
this.$menu.css('min-width', this.autocompleteMinWidth);
|
||||
this.$el.append(this.$menu);
|
||||
}
|
||||
if ($prevMenu) {
|
||||
$prevMenu.remove();
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onInput: function () {
|
||||
if (!this.limit) {
|
||||
return;
|
||||
}
|
||||
this._dp.add(this._fetch()).then(this._render.bind(this));
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onFocusOut: function () {
|
||||
if (!this.$el.has(document.activeElement).length) {
|
||||
this._render();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onKeydown: function (ev) {
|
||||
switch (ev.which) {
|
||||
case $.ui.keyCode.ESCAPE:
|
||||
this._render();
|
||||
break;
|
||||
case $.ui.keyCode.UP:
|
||||
ev.preventDefault();
|
||||
this.$menu.children().last().focus();
|
||||
break;
|
||||
case $.ui.keyCode.DOWN:
|
||||
ev.preventDefault();
|
||||
this.$menu.children().first().focus();
|
||||
break;
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<!-- Products Search Bar settings dialog -->
|
||||
<div t-name="website_sale.dialog.productsSearchBar">
|
||||
<div class="form-group">
|
||||
<label for="order_by">Search filter to use <b>by default</b></label>
|
||||
<select class="custom-select" id="order_by">
|
||||
<option value="" t-att-selected="!currentOrderBy ? 'selected' : undefined">Default order</option>
|
||||
<option value="name asc" t-att-selected="currentOrderBy == 'name asc' ? 'selected' : undefined">Order by name (A-Z)</option>
|
||||
<option value="name desc" t-att-selected="currentOrderBy == 'name desc' ? 'selected' : undefined">Order by name (Z-A)</option>
|
||||
<option value="list_price asc" t-att-selected="currentOrderBy == 'list_price asc' ? 'selected' : undefined">Order by price: low to high</option>
|
||||
<option value="list_price desc" t-att-selected="currentOrderBy == 'list_price desc' ? 'selected' : undefined">Order by price: high to low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch" data-toggle="collapse" data-target="#use_autocomplete_options">
|
||||
<input type="checkbox" class="custom-control-input" id="use_autocomplete" t-att-checked="currentLimit > 0 ? 'checked' : undefined"/>
|
||||
<label class="custom-control-label" for="use_autocomplete">Add product suggestions</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="use_autocomplete_options" t-attf-class="collapse #{currentLimit > 0 ? 'show' : ''}">
|
||||
<div class="form-group">
|
||||
<label for="limit">Maximum number of products to display</label>
|
||||
<select class="custom-select" id="limit">
|
||||
<option t-foreach="20" t-as="limit"
|
||||
t-esc="limit + 1" t-att-value="limit + 1"
|
||||
t-att-selected="currentLimit == (limit + 1) ? 'selected' : undefined"/>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="display_description" t-att-checked="currentDisplayDescription ? 'checked' : undefined"/>
|
||||
<label class="custom-control-label" for="display_description">Display product <b>description</b> and allow to search in</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="display_price" t-att-checked="currentDisplayPrice ? 'checked' : undefined"/>
|
||||
<label class="custom-control-label" for="display_price">Display product <b>price</b></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="display_image" t-att-checked="currentDisplayImage ? 'checked' : undefined"/>
|
||||
<label class="custom-control-label" for="display_image">Display product <b>image</b></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<!-- Products Search Bar autocomplete item -->
|
||||
<div t-name="website_sale.productsSearchBar.autocomplete"
|
||||
class="dropdown-menu show w-100">
|
||||
<t t-if="!products.length">
|
||||
<span class="dropdown-item-text text-muted">No results found. Please try another search.</span>
|
||||
</t>
|
||||
<a t-foreach="products" t-as="product"
|
||||
t-att-href="product['website_url']" class="dropdown-item p-2 text-wrap">
|
||||
<div class="media align-items-center o_search_product_item">
|
||||
<t t-if="widget.displayImage">
|
||||
<img t-attf-src="/web/image/product.template/#{product['id']}/image_small"
|
||||
class="flex-shrink-0"/>
|
||||
</t>
|
||||
<div class="media-body px-3">
|
||||
<t t-set="description" t-value="widget.displayDescription and product['description_sale']"/>
|
||||
<h6 t-attf-class="font-weight-bold #{description ? '' : 'mb-0'}" t-esc="product['name']"/>
|
||||
<p t-if="description" class="mb-0" t-esc="description"/>
|
||||
</div>
|
||||
<div t-if="widget.displayPrice" class="flex-shrink-0">
|
||||
<t t-if="product['has_discounted_price']">
|
||||
<span class="text-danger text-nowrap" style="text-decoration: line-through;">
|
||||
<t t-raw="product['list_price']"/>
|
||||
</span>
|
||||
<br/>
|
||||
</t>
|
||||
<b class="text-nowrap">
|
||||
<t t-raw="product['price']"/>
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<t t-if="hasMoreProducts">
|
||||
<button type="submit" class="dropdown-item text-center text-primary">All results</button>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -1,8 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="website_sale" inherit_id="website.snippets" name="Snippet Ecommerce">
|
||||
|
||||
<xpath expr="//div[@id='snippet_options']" position="inside">
|
||||
<template id="s_products_searchbar_input" name="Products Search Input">
|
||||
<t t-call="website_sale.website_sale_products_search_box">
|
||||
<t t-set="_classes" t-value="'s_wsale_products_searchbar_input mb-3'"/>
|
||||
</t>
|
||||
</template>
|
||||
<template id="s_products_searchbar" name="Products Search">
|
||||
<section class="s_wsale_products_searchbar bg-200 pt24 pb24">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-8 offset-lg-2">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h2>Search for a product</h2>
|
||||
<p>We have amazing products in our shop, check them now !</p>
|
||||
<t t-call="website_sale.s_products_searchbar_input"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template id="snippets" inherit_id="website.snippets" name="e-commerce snippets">
|
||||
<xpath expr="//div[@id='snippet_feature']//t[@t-snippet][last()]" position="after">
|
||||
<t t-snippet="website_sale.s_products_searchbar" t-thumbnail="/website/static/src/img/snippets_thumbs/s_products_searchbar.png"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@id='snippet_content']//t[@t-snippet][last()]" position="after">
|
||||
<t t-snippet="website_sale.s_products_searchbar_input" t-thumbnail="/website/static/src/img/snippets_thumbs/s_products_searchbar.png"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="remove_external_snippets" inherit_id="website.external_snippets">
|
||||
<xpath expr="//t[@t-install='website_sale'][@id='s_products_searchbar']" position="replace"/>
|
||||
<xpath expr="//t[@t-install='website_sale'][@id='s_products_searchbar_input']" position="replace"/>
|
||||
</template>
|
||||
|
||||
<template id="snippet_options" inherit_id="website.snippet_options" name="e-commerce snippet options">
|
||||
<xpath expr="." position="inside">
|
||||
<div data-js='website_sale'
|
||||
data-selector=".oe_product.oe_grid"
|
||||
data-no-check="true">
|
||||
@@ -31,7 +68,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<div data-js="ProductsSearchBar"
|
||||
data-selector=".s_wsale_products_searchbar_input">
|
||||
<a tabindex="-1" href="#" class="dropdown-item" data-no-preview="true" data-open-searchbar-settings=""><i class="fa fa-search"></i>Products Search Bar</a>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[@data-js='content']" position="attributes">
|
||||
<attribute name="data-selector" add=".s_wsale_products_searchbar_input" separator=","/>
|
||||
<attribute name="data-drop-near" add=".s_wsale_products_searchbar_input" separator=","/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -65,15 +65,32 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Products Search Bar input-group template -->
|
||||
<template id="website_sale_products_search_box" inherit_id="website.website_search_box" primary="True">
|
||||
<xpath expr="//input[@name='search']" position="attributes">
|
||||
<attribute name="data-limit">5</attribute>
|
||||
<attribute name="data-display-description">true</attribute>
|
||||
<attribute name="data-display-price">true</attribute>
|
||||
<attribute name="data-display-image">true</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('input-group')]" position="replace">
|
||||
<form t-attf-class="o_wsale_products_searchbar_form #{_classes}" t-att-action="action if action else '/shop'" method="get">
|
||||
<t>$0</t>
|
||||
<input name="order" type="hidden" class="o_wsale_search_order_by" value=""/>
|
||||
<t t-raw="0"/>
|
||||
</form>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="search" name="Search Box">
|
||||
<form t-att-action="keep('/shop'+ ('/category/'+slug(category)) if category else None, search=0)" method="get" t-att-class="_classes">
|
||||
<t t-call="website_sale.website_sale_products_search_box">
|
||||
<t t-set="action" t-value="keep('/shop'+ ('/category/'+slug(category)) if category else None, search=0)"/>
|
||||
<t t-if="attrib_values">
|
||||
<t t-foreach="attrib_values" t-as="a">
|
||||
<input type="hidden" name="attrib" t-att-value="'%s-%s' % (a[0], a[1])" />
|
||||
</t>
|
||||
</t>
|
||||
<t t-call="website.website_search_box" />
|
||||
</form>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="search count" inherit_id="website.website_search_box" active="False" customize_show="True" name="Show # found">
|
||||
@@ -189,9 +206,7 @@
|
||||
<div class="oe_structure" id="oe_structure_website_sale_products_1"/>
|
||||
<div class="container py-2 oe_website_sale">
|
||||
<div class="products_pager form-inline justify-content-center">
|
||||
<t t-call="website_sale.search">
|
||||
<t t-set="_classes">o_website_sale_search</t>
|
||||
</t>
|
||||
<t t-call="website_sale.search"/>
|
||||
<t t-call="website_sale.pricelist_list">
|
||||
<t t-set="_classes">ml-2</t>
|
||||
</t>
|
||||
@@ -1694,5 +1709,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user