/* 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($('
  • ').append(self._renderHeaderButton(child))); } }); return $container; } }); return { 'AppDrawer': AppDrawer, 'SearchView': SearchView, 'Menu': Menu, 'ViewManager': ViewManager, 'FieldStatus': RelationalFields.FieldStatus, 'FormRenderer': FormRenderer, }; });