mirror of
https://github.com/OCA/web.git
synced 2025-02-22 13:21:25 +02:00
[IMP] web_responsive: Add search feature (#838)
* Add feature to search menus in the app drawer * Switch model to RPC
This commit is contained in:
committed by
Pedro M. Baeza
parent
b10e3d2d90
commit
bcdce5aaf5
@@ -6,6 +6,7 @@ odoo.define('web_responsive', function(require) {
|
||||
|
||||
var Menu = require('web.Menu');
|
||||
var Class = require('web.Class');
|
||||
var rpc = require('web.rpc');
|
||||
var SearchView = require('web.SearchView');
|
||||
var core = require('web.core');
|
||||
var config = require('web.config');
|
||||
@@ -29,7 +30,7 @@ odoo.define('web_responsive', function(require) {
|
||||
this._super(id);
|
||||
if (allowOpen) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
var $clicked_menu = this.$secondary_menus.find('a[data-menu=' + id + ']');
|
||||
$clicked_menu.parents('.oe_secondary_submenu').css('display', '');
|
||||
}
|
||||
@@ -65,19 +66,43 @@ odoo.define('web_responsive', function(require) {
|
||||
|
||||
var AppDrawer = Class.extend({
|
||||
|
||||
/* Provides all features inside of the application drawer navigation.
|
||||
|
||||
Attributes:
|
||||
directionCodes (str): Canonical key name to direction mappings.
|
||||
deleteCodes
|
||||
*/
|
||||
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right',
|
||||
UP: 'up',
|
||||
DOWN: 'down',
|
||||
|
||||
// These keys are ignored when presented as single input
|
||||
MODIFIERS: [
|
||||
'Alt',
|
||||
'ArrowDown',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'ArrowUp',
|
||||
'Control',
|
||||
'Enter',
|
||||
'Escape',
|
||||
'Meta',
|
||||
'Shift',
|
||||
'Tab',
|
||||
],
|
||||
|
||||
isOpen: false,
|
||||
keyBuffer: '',
|
||||
keyBufferTime: 500,
|
||||
keyBufferTimeoutEvent: false,
|
||||
dropdownHeightFactor: 0.90,
|
||||
initialized: false,
|
||||
searching: false,
|
||||
|
||||
init: function() {
|
||||
|
||||
this.directionCodes = {
|
||||
'left': this.LEFT,
|
||||
'right': this.RIGHT,
|
||||
@@ -88,6 +113,10 @@ odoo.define('web_responsive', function(require) {
|
||||
'+': this.RIGHT,
|
||||
'-': this.LEFT
|
||||
};
|
||||
this.$searchAction = $('.app-drawer-search-action');
|
||||
this.$searchAction.hide();
|
||||
this.$searchResultsContainer = $('#appDrawerSearchResults');
|
||||
this.$searchInput = $('#appDrawerSearchInput');
|
||||
this.initDrawer();
|
||||
this.handleWindowResize();
|
||||
var $clickZones = $('.odoo_webclient_container, ' +
|
||||
@@ -97,49 +126,75 @@ odoo.define('web_responsive', function(require) {
|
||||
'i.oe_logo_edit'
|
||||
);
|
||||
$clickZones.click($.proxy(this.handleClickZones, this));
|
||||
this.$searchResultsContainer.click($.proxy(this.searchMenus, this));
|
||||
this.$el.find('.drawer-search-open').click(
|
||||
$.proxy(this.searchMenus, this)
|
||||
);
|
||||
this.$el.find('.drawer-search-close').hide().click(
|
||||
$.proxy(this.closeSearchMenus, this)
|
||||
);
|
||||
|
||||
core.bus.on('resize', this, this.handleWindowResize);
|
||||
core.bus.on('keydown', this, this.handleNavKeys);
|
||||
core.bus.on('keydown', this, this.handleKeyDown);
|
||||
core.bus.on('keyup', this, this.redirectKeyPresses);
|
||||
core.bus.on('keypress', this, this.redirectKeyPresses);
|
||||
},
|
||||
|
||||
// It provides initialization handlers for Drawer
|
||||
// Provides initialization handlers for Drawer
|
||||
initDrawer: function() {
|
||||
this.$el = $('.drawer');
|
||||
this.$el.drawer();
|
||||
this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
|
||||
this.$el.on('drawer.opened', function setIScrollProbes() {
|
||||
var onIScroll = function() {
|
||||
var transform = this.iScroll.y ? this.iScroll.y * -1 : 0;
|
||||
$(this).find('#appDrawerAppPanelHead').css(
|
||||
'transform', 'matrix(1, 0, 0, 1, 0, ' + transform + ')'
|
||||
|
||||
// Setup the iScroll options.
|
||||
// You should be able to pass these to ``.drawer``, but scroll freezes.
|
||||
this.$el.on(
|
||||
'drawer.opened',
|
||||
function setIScrollProbes(){
|
||||
var onIScroll = $.proxy(
|
||||
function() {
|
||||
this.iScroll.refresh();
|
||||
},
|
||||
this
|
||||
);
|
||||
};
|
||||
// Scroll probe aggressiveness level
|
||||
// 2 == always executes the scroll event except during momentum and bounce.
|
||||
this.iScroll.options.probeType = 2;
|
||||
// Set options because
|
||||
this.iScroll.on('scroll', $.proxy(onIScroll, this));
|
||||
});
|
||||
// Scroll probe aggressiveness level
|
||||
// 2 == always executes the scroll event except during momentum and bounce.
|
||||
this.iScroll.options.probeType = 2;
|
||||
this.iScroll.on('scroll', onIScroll);
|
||||
// Initialize Scrollbars manually
|
||||
this.iScroll.options.scrollbars = true;
|
||||
this.iScroll.options.fadeScrollbars = true;
|
||||
this.iScroll._initIndicators();
|
||||
}
|
||||
);
|
||||
this.initialized = true;
|
||||
},
|
||||
|
||||
// It provides handlers to hide drawer when "unfocused"
|
||||
// Provides handlers to hide drawer when "unfocused"
|
||||
handleClickZones: function() {
|
||||
this.$el.drawer('close');
|
||||
$('.o_sub_menu_content')
|
||||
.parent()
|
||||
.collapse('hide');
|
||||
$('.navbar-collapse').collapse('hide');
|
||||
},
|
||||
|
||||
// It resizes bootstrap dropdowns for screen
|
||||
// Resizes bootstrap dropdowns for screen
|
||||
handleWindowResize: function() {
|
||||
$('.dropdown-scrollable').css(
|
||||
'max-height', $(window).height() * this.dropdownHeightFactor
|
||||
);
|
||||
},
|
||||
|
||||
// It provides keyboard shortcuts for app drawer nav
|
||||
handleNavKeys: function(e) {
|
||||
if (!this.isOpen) {
|
||||
/* Provide keyboard shortcuts for app drawer nav.
|
||||
*
|
||||
* It is required to perform this functionality only on the ``keydown``
|
||||
* event in order to prevent duplication of the arrow events.
|
||||
*
|
||||
* @param e The ``keydown`` event triggered by ``core.bus``.
|
||||
*/
|
||||
handleKeyDown: function(e) {
|
||||
if (!this.isOpen){
|
||||
return;
|
||||
}
|
||||
var directionCode = $.hotkeys.specialKeys[e.keyCode.toString()];
|
||||
@@ -151,36 +206,87 @@ odoo.define('web_responsive', function(require) {
|
||||
this.selectAppLink($link);
|
||||
} else if ($.hotkeys.specialKeys[e.keyCode.toString()] === 'esc') {
|
||||
this.handleClickZones();
|
||||
if (this.searching) {
|
||||
var $collection = this.$el.find('#appDrawerMenuSearch a');
|
||||
var $link = this.findAdjacentLink(
|
||||
this.$el.find('#appDrawerMenuSearch a:first, #appDrawerMenuSearch a.web-responsive-focus').last(),
|
||||
this.directionCodes[directionCode],
|
||||
$collection,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
var $link = this.findAdjacentLink(
|
||||
this.$el.find('#appDrawerApps a:first, #appDrawerApps a.web-responsive-focus').last(),
|
||||
this.directionCodes[directionCode]
|
||||
);
|
||||
}
|
||||
this.selectLink($link);
|
||||
} else if ($.hotkeys.specialKeys[e.keyCode.toString()] == 'esc') {
|
||||
// We either back out of the search, or close the app drawer.
|
||||
if (this.searching) {
|
||||
this.closeSearchMenus();
|
||||
} else {
|
||||
this.handleClickZones();
|
||||
}
|
||||
} else {
|
||||
var buffer = this.handleKeyBuffer(e.keyCode);
|
||||
this.selectAppLink(this.searchAppLinks(buffer));
|
||||
this.redirectKeyPresses(e);
|
||||
}
|
||||
},
|
||||
|
||||
/* It adds to keybuffer, sets expire timer, and returns buffer
|
||||
* @returns str of current buffer
|
||||
/* Provide centralized key event redirects for the App Drawer.
|
||||
*
|
||||
* This method is for all key events not related to arrow navigation.
|
||||
*
|
||||
* @param e The key event that was triggered by ``core.bus``.
|
||||
*/
|
||||
handleKeyBuffer: function(keyCode) {
|
||||
this.keyBuffer += String.fromCharCode(keyCode);
|
||||
if (this.keyBufferTimeoutEvent) {
|
||||
clearTimeout(this.keyBufferTimeoutEvent);
|
||||
redirectKeyPresses: function(e) {
|
||||
|
||||
if ( !this.isOpen ) {
|
||||
// Drawer isn't open; Ignore.
|
||||
return;
|
||||
}
|
||||
this.keyBufferTimeoutEvent = setTimeout(
|
||||
$.proxy(this.clearKeyBuffer, this),
|
||||
this.keyBufferTime
|
||||
);
|
||||
return this.keyBuffer;
|
||||
|
||||
// Trigger navigation to pseudo-focused link
|
||||
// & fake a click (in case of anchor link).
|
||||
if (e.key === 'Enter') {
|
||||
window.location.href = $('.web-responsive-focus').attr('href');
|
||||
this.handleClickZones();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore any other modifier keys.
|
||||
if (this.MODIFIERS.indexOf(e.key) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Event is already targeting the search input.
|
||||
// Perform search, then stop processing.
|
||||
if ( e.target === this.$searchInput[0] ) {
|
||||
this.searchMenus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent default event,
|
||||
// redirect it to the search input,
|
||||
// and search.
|
||||
e.preventDefault();
|
||||
this.$searchInput.trigger({
|
||||
type: e.type,
|
||||
key: e.key,
|
||||
keyCode: e.keyCode,
|
||||
which: e.which,
|
||||
});
|
||||
this.searchMenus();
|
||||
|
||||
},
|
||||
|
||||
clearKeyBuffer: function() {
|
||||
this.keyBuffer = '';
|
||||
},
|
||||
|
||||
/* It performs close actions
|
||||
/* Performs close actions
|
||||
* @fires ``drawer.closed`` to the ``core.bus``
|
||||
* @listens ``drawer.opened`` and sends to onDrawerOpen
|
||||
*/
|
||||
onDrawerClose: function() {
|
||||
this.closeSearchMenus();
|
||||
this.$searchAction.hide();
|
||||
core.bus.trigger('drawer.closed');
|
||||
this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
|
||||
this.isOpen = false;
|
||||
@@ -188,36 +294,88 @@ odoo.define('web_responsive', function(require) {
|
||||
this.$el.css("overflow", "");
|
||||
},
|
||||
|
||||
/* It finds app links and register event handlers
|
||||
/* Finds app links and register event handlers
|
||||
* @fires ``drawer.opened`` to the ``core.bus``
|
||||
* @listens ``drawer.closed`` and sends to :meth:``onDrawerClose``
|
||||
*/
|
||||
onDrawerOpen: function() {
|
||||
this.$appLinks = $('.app-drawer-icon-app').parent();
|
||||
this.selectAppLink($(this.$appLinks[0]));
|
||||
this.selectLink($(this.$appLinks[0]));
|
||||
this.$el.one('drawer.closed', $.proxy(this.onDrawerClose, this));
|
||||
core.bus.trigger('drawer.opened');
|
||||
this.isOpen = true;
|
||||
},
|
||||
|
||||
// It selects an app link visibly
|
||||
selectAppLink: function($appLink) {
|
||||
if ($appLink) {
|
||||
$appLink.focus();
|
||||
// Selects a link visibly & deselects others.
|
||||
selectLink: function($link) {
|
||||
$('.web-responsive-focus').removeClass('web-responsive-focus');
|
||||
if ($link) {
|
||||
$link.addClass('web-responsive-focus');
|
||||
}
|
||||
},
|
||||
|
||||
/* It returns first App Link by its name according to query
|
||||
/* Searches for menus by name, then triggers showFoundMenus
|
||||
* @param query str to search
|
||||
* @return jQuery obj
|
||||
*/
|
||||
searchAppLinks: function(query) {
|
||||
return this.$appLinks.filter(function() {
|
||||
return $(this).data('menuName').toUpperCase().startsWith(query);
|
||||
}).first();
|
||||
searchMenus: function() {
|
||||
this.$searchInput = $('#appDrawerSearchInput').focus();
|
||||
var domain = [['name', 'ilike', this.$searchInput.val()],
|
||||
['action', '!=', false]];
|
||||
rpc.query({
|
||||
model: 'ir.ui.menu',
|
||||
method: 'search_read',
|
||||
args: [{
|
||||
fields: ['action', 'display_name', 'id'],
|
||||
domain: domain
|
||||
|
||||
}]
|
||||
}).then(
|
||||
$.proxy(this.showFoundMenus, this)
|
||||
);
|
||||
},
|
||||
|
||||
/* It returns the link adjacent to $appLink in provided direction.
|
||||
/* Display the menus that are provided as input.
|
||||
*/
|
||||
showFoundMenus: function(menus) {
|
||||
this.searching = true;
|
||||
this.$el.find('#appDrawerApps').hide();
|
||||
this.$searchAction.hide();
|
||||
this.$el.find('.drawer-search-close').show();
|
||||
this.$el.find('.drawer-search-open').hide();
|
||||
this.$searchResultsContainer
|
||||
// Render the results
|
||||
.html(
|
||||
core.qweb.render(
|
||||
'AppDrawerMenuSearchResults',
|
||||
{menus: menus}
|
||||
)
|
||||
)
|
||||
// Get the parent container and show it.
|
||||
.closest('#appDrawerMenuSearch')
|
||||
.show()
|
||||
// Find the input, set focus.
|
||||
.find('.menu-search-query')
|
||||
.focus()
|
||||
;
|
||||
var $menuLinks = this.$searchResultsContainer.find('a');
|
||||
$menuLinks.click($.proxy(this.handleClickZones, this));
|
||||
this.selectLink($menuLinks.first());
|
||||
},
|
||||
|
||||
/* Close search menu and switch back to app menu.
|
||||
*/
|
||||
closeSearchMenus: function() {
|
||||
this.searching = false;
|
||||
this.$el.find('#appDrawerApps').show();
|
||||
this.$el.find('.drawer-search-close').hide();
|
||||
this.$el.find('.drawer-search-open').show();
|
||||
this.$searchResultsContainer.closest('#appDrawerMenuSearch').hide();
|
||||
this.$searchAction.show();
|
||||
$('#appDrawerSearchInput').val('');
|
||||
},
|
||||
|
||||
/* Returns the link adjacent to $link in provided direction.
|
||||
* It also handles edge cases in the following ways:
|
||||
* * Moves to last link if LEFT on first
|
||||
* * Moves to first link if PREV on last
|
||||
@@ -225,40 +383,46 @@ odoo.define('web_responsive', function(require) {
|
||||
* * Moves to last link of previous row if LEFT on first in row
|
||||
* * Moves to top link in same column if DOWN on bottom row
|
||||
* * Moves to bottom link in same column if UP on top row
|
||||
* @param $appLink jQuery obj of App icon link
|
||||
* @param $link jQuery obj of App icon link
|
||||
* @param direction str of direction to go (constants LEFT, UP, etc.)
|
||||
* @return jQuery obj for adjacent applink
|
||||
* @param $objs jQuery obj representing the collection of links. Defaults
|
||||
* to `this.$appLinks`.
|
||||
* @param restrictHorizontal bool Set to true if the collection consists
|
||||
* only of vertical elements.
|
||||
* @return jQuery obj for adjacent link
|
||||
*/
|
||||
findAdjacentAppLink: function($appLink, direction) {
|
||||
findAdjacentLink: function($link, direction, $objs, restrictHorizontal) {
|
||||
|
||||
var obj = [],
|
||||
if ($objs === undefined) {
|
||||
$objs = this.$appLinks;
|
||||
}
|
||||
|
||||
var obj = [];
|
||||
var $rows = (restrictHorizontal) ? $objs : this.getRowObjs($link, this.$appLinks);
|
||||
|
||||
switch (direction) {
|
||||
case this.LEFT:
|
||||
obj = $objs[$objs.index($appLink) - 1];
|
||||
obj = $objs[$objs.index($link) - 1];
|
||||
if (!obj) {
|
||||
obj = $objs[$objs.length - 1];
|
||||
}
|
||||
break;
|
||||
case this.RIGHT:
|
||||
obj = $objs[$objs.index($appLink) + 1];
|
||||
obj = $objs[$objs.index($link) + 1];
|
||||
if (!obj) {
|
||||
obj = $objs[0];
|
||||
}
|
||||
break;
|
||||
case this.UP:
|
||||
$objs = this.getRowObjs($appLink, this.$appLinks);
|
||||
obj = $objs[$objs.index($appLink) - 1];
|
||||
obj = $rows[$rows.index($link) - 1];
|
||||
if (!obj) {
|
||||
obj = $objs[$objs.length - 1];
|
||||
obj = $rows[$rows.length - 1];
|
||||
}
|
||||
break;
|
||||
case this.DOWN:
|
||||
$objs = this.getRowObjs($appLink, this.$appLinks);
|
||||
obj = $objs[$objs.index($appLink) + 1];
|
||||
obj = $rows[$rows.index($link) + 1];
|
||||
if (!obj) {
|
||||
obj = $objs[0];
|
||||
obj = $rows[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -271,7 +435,7 @@ odoo.define('web_responsive', function(require) {
|
||||
|
||||
},
|
||||
|
||||
/* It returns els in the same row
|
||||
/* Returns els in the same row
|
||||
* @param @obj jQuery object to get row for
|
||||
* @param $grid jQuery objects representing grid
|
||||
* @return $objs jQuery objects of row
|
||||
@@ -292,9 +456,9 @@ odoo.define('web_responsive', function(require) {
|
||||
|
||||
});
|
||||
|
||||
// It inits a new AppDrawer when the web client is ready
|
||||
core.bus.on('web_client_ready', null, function() {
|
||||
return new AppDrawer();
|
||||
// Init a new AppDrawer when the web client is ready
|
||||
core.bus.on('web_client_ready', null, function () {
|
||||
new AppDrawer();
|
||||
});
|
||||
|
||||
// if we are in small screen change default view to kanban if exists
|
||||
|
||||
@@ -78,6 +78,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-drawer-search-panel {
|
||||
|
||||
.panel-body {
|
||||
padding-top: @padding-base-vertical;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.drawer-nav {
|
||||
@@ -113,3 +121,9 @@
|
||||
.app-drawer-toggle.navbar-toggle {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
/* Icon Focusing */
|
||||
|
||||
.web-responsive-focus {
|
||||
.tab-focus();
|
||||
}
|
||||
|
||||
16
web_responsive/static/src/xml/app_drawer_menu_search.xml
Normal file
16
web_responsive/static/src/xml/app_drawer_menu_search.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2017 LasLabs Inc.
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="AppDrawerMenuSearchResults">
|
||||
<li class="menu-search-element" t-foreach="menus" t-as="menu">
|
||||
<a t-att-id="menu.id"
|
||||
t-attf-href="#action={{ menu.action and menu.action.split(',')[1] or ''}}&menu_id={{ menu.id }}">
|
||||
<h2 class="text-center">
|
||||
<t t-esc="menu.display_name" />
|
||||
</h2>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user