You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

526 lines
18 KiB

/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define('web_responsive', function(require) {
'use strict';
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');
var ViewManager = require('web.ViewManager');
var RelationalFields = require('web.relational_fields');
var FormRenderer = require('web.FormRenderer');
var qweb = core.qweb;
Menu.include({
// Force all_outside to prevent app icons from going into more menu
reflow: function() {
this._super('all_outside');
},
/* Overload to collapse unwanted visible submenus
* @param allow_open bool Switch to allow submenus to be opened
*/
open_menu: function(id, allowOpen) {
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', '');
}
});
SearchView.include({
// Prevent focus of search field on mobile devices
toggle_visibility: function(is_visible) {
$('div.oe_searchview_input').last().one(
'focus', $.proxy(this.preventMobileFocus, this));
return this._super(is_visible);
},
// It prevents focusing of search el on mobile
preventMobileFocus: function(event) {
if (this.isMobile()) {
event.preventDefault();
}
},
// For lack of Modernizr, TouchEvent will do
isMobile: function() {
try {
document.createEvent('TouchEvent');
return true;
} catch (ex) {
return false;
}
}
});
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,
'up': this.UP,
'pageup': this.UP,
'down': this.DOWN,
'pagedown': this.DOWN,
'+': 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, ' +
'a.oe_menu_leaf, ' +
'a.oe_menu_toggler, ' +
'a.oe_logo, ' +
'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.handleKeyDown);
core.bus.on('keyup', this, this.redirectKeyPresses);
core.bus.on('keypress', this, this.redirectKeyPresses);
},
// Provides initialization handlers for Drawer
initDrawer: function() {
this.$el = $('.drawer');
this.$el.drawer();
this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
// 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;
this.iScroll.on('scroll', onIScroll);
// Initialize Scrollbars manually
this.iScroll.options.scrollbars = true;
this.iScroll.options.fadeScrollbars = true;
this.iScroll._initIndicators();
}
);
this.initialized = true;
},
// Provides handlers to hide drawer when "unfocused"
handleClickZones: function() {
this.$el.drawer('close');
$('.o_sub_menu_content')
.parent()
.collapse('hide');
$('.navbar-collapse').collapse('hide');
},
// Resizes bootstrap dropdowns for screen
handleWindowResize: function() {
$('.dropdown-scrollable').css(
'max-height', $(window).height() * this.dropdownHeightFactor
);
},
/* 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()];
if (Object.keys(this.directionCodes).indexOf(directionCode) !== -1) {
var $link = this.findAdjacentAppLink(
this.$el.find('a:first, a:focus').last(),
this.directionCodes[directionCode]
);
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 {
this.redirectKeyPresses(e);
}
},
/* 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``.
*/
redirectKeyPresses: function(e) {
if ( !this.isOpen ) {
// Drawer isn't open; Ignore.
return;
}
// 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();
},
/* 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;
// Remove inline style inserted by drawer.js
this.$el.css("overflow", "");
},
/* 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.selectLink($(this.$appLinks[0]));
this.$el.one('drawer.closed', $.proxy(this.onDrawerClose, this));
core.bus.trigger('drawer.opened');
this.isOpen = true;
},
// Selects a link visibly & deselects others.
selectLink: function($link) {
$('.web-responsive-focus').removeClass('web-responsive-focus');
if ($link) {
$link.addClass('web-responsive-focus');
}
},
/* Searches for menus by name, then triggers showFoundMenus
* @param query str to search
* @return jQuery obj
*/
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)
);
},
/* 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
* * Moves to first link of following row if RIGHT on last in row
* * 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 $link jQuery obj of App icon link
* @param direction str of direction to go (constants LEFT, UP, etc.)
* @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
*/
findAdjacentLink: function($link, direction, $objs, restrictHorizontal) {
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($link) - 1];
if (!obj) {
obj = $objs[$objs.length - 1];
}
break;
case this.RIGHT:
obj = $objs[$objs.index($link) + 1];
if (!obj) {
obj = $objs[0];
}
break;
case this.UP:
obj = $rows[$rows.index($link) - 1];
if (!obj) {
obj = $rows[$rows.length - 1];
}
break;
case this.DOWN:
obj = $rows[$rows.index($link) + 1];
if (!obj) {
obj = $rows[0];
}
break;
}
if (obj.length) {
event.preventDefault();
}
return $(obj);
},
/* 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
*/
getRowObjs: function($obj, $grid) {
// Filter by object which middle lies within left/right bounds
function filterWithin(left, right) {
return function() {
var $this = $(this),
thisMiddle = $this.offset().left + $this.width() / 2;
return thisMiddle >= left && thisMiddle <= right;
};
}
var left = $obj.offset().left,
right = left + $obj.outerWidth();
return $grid.filter(filterWithin(left, right));
}
});
// 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
ViewManager.include({
get_default_view: function() {
var default_view = this._super();
if (config.device.size_class <= config.device.SIZES.XS &&
default_view.type !== 'kanban' &&
this.views.kanban) {
default_view.type = 'kanban';
}
return default_view;
},
});
// FieldStatus (responsive fold)
RelationalFields.FieldStatus.include({
_renderQWebValues: function () {
return {
selections: this.status_information, // Needed to preserve order
has_folded: _.filter(this.status_information, {'selected': false}).length > 0,
clickable: !!this.attrs.clickable,
};
},
_render: function () {
// FIXME: Odoo framework creates view values & render qweb in the
// same method. This cause a "double render" process to use
// new custom values.
this._super.apply(this, arguments);
this.$el.html(qweb.render("FieldStatus.content", this._renderQWebValues()));
}
});
// Responsive view "action" buttons
FormRenderer.include({
_renderHeaderButtons: function (node) {
var self = this;
var $buttons = this._super(node);
var $container = $(qweb.render('web_responsive.MenuStatusbarButtons'));
$container.find('.o_statusbar_buttons_base').append($buttons);
var $dropdownMenu = $container.find('.dropdown-menu');
_.each(node.children, function (child) {
if (child.tag === 'button') {
$dropdownMenu.append($('<LI>').append(self._renderHeaderButton(child)));
}
});
return $container;
}
});
return {
'AppDrawer': AppDrawer,
'SearchView': SearchView,
'Menu': Menu,
'ViewManager': ViewManager,
'FieldStatus': RelationalFields.FieldStatus,
'FormRenderer': FormRenderer,
};
});