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.

471 lines
16 KiB

  1. /* Copyright 2016 LasLabs Inc.
  2. * License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
  3. odoo.define('web_responsive', function(require) {
  4. 'use strict';
  5. var Menu = require('web.Menu');
  6. var Class = require('web.Class');
  7. var DataModel = require('web.DataModel');
  8. var SearchView = require('web.SearchView');
  9. var core = require('web.core');
  10. var config = require('web.config');
  11. var FieldOne2Many = core.form_widget_registry.get('one2many');
  12. var ViewManager = require('web.ViewManager');
  13. Menu.include({
  14. // Force all_outside to prevent app icons from going into more menu
  15. reflow: function() {
  16. this._super('all_outside');
  17. },
  18. /* Overload to collapse unwanted visible submenus
  19. * @param allow_open bool Switch to allow submenus to be opened
  20. */
  21. open_menu: function(id, allowOpen) {
  22. this._super(id);
  23. if (allowOpen) return;
  24. var $clicked_menu = this.$secondary_menus.find('a[data-menu=' + id + ']');
  25. $clicked_menu.parents('.oe_secondary_submenu').css('display', '');
  26. },
  27. });
  28. SearchView.include({
  29. // Prevent focus of search field on mobile devices
  30. toggle_visibility: function (is_visible) {
  31. $('div.oe_searchview_input').last()
  32. .one('focus', $.proxy(this.preventMobileFocus, this));
  33. return this._super(is_visible);
  34. },
  35. // It prevents focusing of search el on mobile
  36. preventMobileFocus: function(event) {
  37. if (this.isMobile()) {
  38. event.preventDefault();
  39. }
  40. },
  41. // For lack of Modernizr, TouchEvent will do
  42. isMobile: function () {
  43. try{
  44. document.createEvent('TouchEvent');
  45. return true;
  46. } catch (ex) {
  47. return false;
  48. }
  49. },
  50. });
  51. var AppDrawer = Class.extend({
  52. /* Provides all features inside of the application drawer navigation.
  53. Attributes:
  54. directionCodes (str): Canonical key name to direction mappings.
  55. deleteCodes
  56. */
  57. LEFT: 'left',
  58. RIGHT: 'right',
  59. UP: 'up',
  60. DOWN: 'down',
  61. // These keys are ignored when presented as single input
  62. MODIFIERS: [
  63. 'Alt',
  64. 'ArrowDown',
  65. 'ArrowLeft',
  66. 'ArrowRight',
  67. 'ArrowUp',
  68. 'Control',
  69. 'Enter',
  70. 'Escape',
  71. 'Meta',
  72. 'Shift',
  73. 'Tab',
  74. ],
  75. isOpen: false,
  76. keyBuffer: '',
  77. keyBufferTime: 500,
  78. keyBufferTimeoutEvent: false,
  79. dropdownHeightFactor: 0.90,
  80. initialized: false,
  81. searching: false,
  82. init: function() {
  83. this.directionCodes = {
  84. 'left': this.LEFT,
  85. 'right': this.RIGHT,
  86. 'up': this.UP,
  87. 'pageup': this.UP,
  88. 'down': this.DOWN,
  89. 'pagedown': this.DOWN,
  90. '+': this.RIGHT,
  91. '-': this.LEFT,
  92. };
  93. this.$searchAction = $('.app-drawer-search-action');
  94. this.$searchAction.hide();
  95. this.$searchResultsContainer = $('#appDrawerSearchResults');
  96. this.$searchInput = $('#appDrawerSearchInput');
  97. this.initDrawer();
  98. this.handleWindowResize();
  99. var $clickZones = $('.odoo_webclient_container, ' +
  100. 'a.oe_menu_leaf, ' +
  101. 'a.oe_menu_toggler, ' +
  102. 'a.oe_logo, ' +
  103. 'i.oe_logo_edit'
  104. );
  105. $clickZones.click($.proxy(this.handleClickZones, this));
  106. this.$searchResultsContainer.click($.proxy(this.searchMenus, this));
  107. this.$el.find('.drawer-search-open').click(
  108. $.proxy(this.searchMenus, this)
  109. );
  110. this.$el.find('.drawer-search-close').hide().click(
  111. $.proxy(this.closeSearchMenus, this)
  112. );
  113. core.bus.on('resize', this, this.handleWindowResize);
  114. core.bus.on('keydown', this, this.handleKeyDown);
  115. core.bus.on('keyup', this, this.redirectKeyPresses);
  116. core.bus.on('keypress', this, this.redirectKeyPresses);
  117. },
  118. // Provides initialization handlers for Drawer
  119. initDrawer: function() {
  120. this.$el = $('.drawer');
  121. this.$el.drawer();
  122. this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
  123. // Setup the iScroll options.
  124. // You should be able to pass these to ``.drawer``, but scroll freezes.
  125. this.$el.on(
  126. 'drawer.opened',
  127. function setIScrollProbes(){
  128. var onIScroll = $.proxy(
  129. function() {
  130. this.iScroll.refresh();
  131. },
  132. this
  133. );
  134. // Scroll probe aggressiveness level
  135. // 2 == always executes the scroll event except during momentum and bounce.
  136. this.iScroll.options.probeType = 2;
  137. this.iScroll.on('scroll', onIScroll);
  138. // Initialize Scrollbars manually
  139. this.iScroll.options.scrollbars = true;
  140. this.iScroll.options.fadeScrollbars = true;
  141. this.iScroll._initIndicators();
  142. }
  143. );
  144. this.initialized = true;
  145. },
  146. // Provides handlers to hide drawer when "unfocused"
  147. handleClickZones: function() {
  148. this.$el.drawer('close');
  149. $('.o_sub_menu_content')
  150. .parent()
  151. .collapse('hide');
  152. $('.navbar-collapse').collapse('hide');
  153. },
  154. // Resizes bootstrap dropdowns for screen
  155. handleWindowResize: function() {
  156. $('.dropdown-scrollable').css(
  157. 'max-height', $(window).height() * this.dropdownHeightFactor
  158. );
  159. },
  160. /* Provide keyboard shortcuts for app drawer nav.
  161. *
  162. * It is required to perform this functionality only on the ``keydown``
  163. * event in order to prevent duplication of the arrow events.
  164. *
  165. * @param e The ``keydown`` event triggered by ``core.bus``.
  166. */
  167. handleKeyDown: function(e) {
  168. if (!this.isOpen){
  169. return;
  170. }
  171. var directionCode = $.hotkeys.specialKeys[e.keyCode.toString()];
  172. if (Object.keys(this.directionCodes).indexOf(directionCode) !== -1) {
  173. if (this.searching) {
  174. var $collection = this.$el.find('#appDrawerMenuSearch a');
  175. var $link = this.findAdjacentLink(
  176. this.$el.find('#appDrawerMenuSearch a:first, #appDrawerMenuSearch a.web-responsive-focus').last(),
  177. this.directionCodes[directionCode],
  178. $collection,
  179. true
  180. );
  181. } else {
  182. var $link = this.findAdjacentLink(
  183. this.$el.find('#appDrawerApps a:first, #appDrawerApps a.web-responsive-focus').last(),
  184. this.directionCodes[directionCode]
  185. );
  186. }
  187. this.selectLink($link);
  188. } else if ($.hotkeys.specialKeys[e.keyCode.toString()] == 'esc') {
  189. // We either back out of the search, or close the app drawer.
  190. if (this.searching) {
  191. this.closeSearchMenus();
  192. } else {
  193. this.handleClickZones();
  194. }
  195. } else {
  196. this.redirectKeyPresses(e);
  197. }
  198. },
  199. /* Provide centralized key event redirects for the App Drawer.
  200. *
  201. * This method is for all key events not related to arrow navigation.
  202. *
  203. * @param e The key event that was triggered by ``core.bus``.
  204. */
  205. redirectKeyPresses: function(e) {
  206. if ( !this.isOpen ) {
  207. // Drawer isn't open; Ignore.
  208. return;
  209. }
  210. // Trigger navigation to pseudo-focused link
  211. // & fake a click (in case of anchor link).
  212. if (e.key === 'Enter') {
  213. window.location.href = $('.web-responsive-focus').attr('href');
  214. this.handleClickZones();
  215. return;
  216. }
  217. // Ignore any other modifier keys.
  218. if (this.MODIFIERS.indexOf(e.key) !== -1) {
  219. return;
  220. }
  221. // Event is already targeting the search input.
  222. // Perform search, then stop processing.
  223. if ( e.target === this.$searchInput[0] ) {
  224. this.searchMenus();
  225. return;
  226. }
  227. // Prevent default event,
  228. // redirect it to the search input,
  229. // and search.
  230. e.preventDefault();
  231. this.$searchInput.trigger({
  232. type: e.type,
  233. key: e.key,
  234. keyCode: e.keyCode,
  235. which: e.which,
  236. });
  237. this.searchMenus();
  238. },
  239. /* Performs close actions
  240. * @fires ``drawer.closed`` to the ``core.bus``
  241. * @listens ``drawer.opened`` and sends to onDrawerOpen
  242. */
  243. onDrawerClose: function() {
  244. this.closeSearchMenus();
  245. this.$searchAction.hide();
  246. core.bus.trigger('drawer.closed');
  247. this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
  248. this.isOpen = false;
  249. // Remove inline style inserted by drawer.js
  250. this.$el.css("overflow", "");
  251. },
  252. /* Finds app links and register event handlers
  253. * @fires ``drawer.opened`` to the ``core.bus``
  254. * @listens ``drawer.closed`` and sends to :meth:``onDrawerClose``
  255. */
  256. onDrawerOpen: function() {
  257. this.$appLinks = $('.app-drawer-icon-app').parent();
  258. this.selectLink($(this.$appLinks[0]));
  259. this.$el.one('drawer.closed', $.proxy(this.onDrawerClose, this));
  260. core.bus.trigger('drawer.opened');
  261. this.isOpen = true;
  262. },
  263. // Selects a link visibly & deselects others.
  264. selectLink: function($link) {
  265. $('.web-responsive-focus').removeClass('web-responsive-focus');
  266. if ($link) {
  267. $link.addClass('web-responsive-focus');
  268. }
  269. },
  270. /* Searches for menus by name, then triggers showFoundMenus
  271. * @param query str to search
  272. * @return jQuery obj
  273. */
  274. searchMenus: function() {
  275. this.$searchInput = $('#appDrawerSearchInput').focus();
  276. var Menus = new DataModel('ir.ui.menu');
  277. Menus.query(['action', 'display_name', 'id'])
  278. .filter([['name', 'ilike', this.$searchInput.val()],
  279. ['action', '!=', false]
  280. ])
  281. .all()
  282. .then($.proxy(this.showFoundMenus, this));
  283. },
  284. /* Display the menus that are provided as input.
  285. */
  286. showFoundMenus: function(menus) {
  287. this.searching = true;
  288. this.$el.find('#appDrawerApps').hide();
  289. this.$searchAction.hide();
  290. this.$el.find('.drawer-search-close').show();
  291. this.$el.find('.drawer-search-open').hide();
  292. this.$searchResultsContainer
  293. // Render the results
  294. .html(
  295. core.qweb.render(
  296. 'AppDrawerMenuSearchResults',
  297. {menus: menus}
  298. )
  299. )
  300. // Get the parent container and show it.
  301. .closest('#appDrawerMenuSearch')
  302. .show()
  303. // Find the input, set focus.
  304. .find('.menu-search-query')
  305. .focus()
  306. ;
  307. var $menuLinks = this.$searchResultsContainer.find('a');
  308. $menuLinks.click($.proxy(this.handleClickZones, this));
  309. this.selectLink($menuLinks.first());
  310. },
  311. /* Close search menu and switch back to app menu.
  312. */
  313. closeSearchMenus: function() {
  314. this.searching = false;
  315. this.$el.find('#appDrawerApps').show();
  316. this.$el.find('.drawer-search-close').hide();
  317. this.$el.find('.drawer-search-open').show();
  318. this.$searchResultsContainer.closest('#appDrawerMenuSearch').hide();
  319. this.$searchAction.show();
  320. $('#appDrawerSearchInput').val('');
  321. },
  322. /* Returns the link adjacent to $link in provided direction.
  323. * It also handles edge cases in the following ways:
  324. * * Moves to last link if LEFT on first
  325. * * Moves to first link if PREV on last
  326. * * Moves to first link of following row if RIGHT on last in row
  327. * * Moves to last link of previous row if LEFT on first in row
  328. * * Moves to top link in same column if DOWN on bottom row
  329. * * Moves to bottom link in same column if UP on top row
  330. * @param $link jQuery obj of App icon link
  331. * @param direction str of direction to go (constants LEFT, UP, etc.)
  332. * @param $objs jQuery obj representing the collection of links. Defaults
  333. * to `this.$appLinks`.
  334. * @param restrictHorizontal bool Set to true if the collection consists
  335. * only of vertical elements.
  336. * @return jQuery obj for adjacent link
  337. */
  338. findAdjacentLink: function($link, direction, $objs, restrictHorizontal) {
  339. if ($objs === undefined) {
  340. $objs = this.$appLinks;
  341. }
  342. var obj = [];
  343. var $rows = (restrictHorizontal) ? $objs : this.getRowObjs($link, this.$appLinks);
  344. switch(direction){
  345. case this.LEFT:
  346. obj = $objs[$objs.index($link) - 1];
  347. if (!obj) {
  348. obj = $objs[$objs.length - 1];
  349. }
  350. break;
  351. case this.RIGHT:
  352. obj = $objs[$objs.index($link) + 1];
  353. if (!obj) {
  354. obj = $objs[0];
  355. }
  356. break;
  357. case this.UP:
  358. obj = $rows[$rows.index($link) - 1];
  359. if (!obj) {
  360. obj = $rows[$rows.length - 1];
  361. }
  362. break;
  363. case this.DOWN:
  364. obj = $rows[$rows.index($link) + 1];
  365. if (!obj) {
  366. obj = $rows[0];
  367. }
  368. break;
  369. }
  370. if (obj.length) {
  371. event.preventDefault();
  372. }
  373. return $(obj);
  374. },
  375. /* Returns els in the same row
  376. * @param @obj jQuery object to get row for
  377. * @param $grid jQuery objects representing grid
  378. * @return $objs jQuery objects of row
  379. */
  380. getRowObjs: function($obj, $grid) {
  381. // Filter by object which middle lies within left/right bounds
  382. function filterWithin(left, right) {
  383. return function() {
  384. var $this = $(this),
  385. thisMiddle = $this.offset().left + ($this.width() / 2);
  386. return thisMiddle >= left && thisMiddle <= right;
  387. };
  388. }
  389. var left = $obj.offset().left,
  390. right = left + $obj.outerWidth();
  391. return $grid.filter(filterWithin(left, right));
  392. },
  393. });
  394. // Init a new AppDrawer when the web client is ready
  395. core.bus.on('web_client_ready', null, function () {
  396. new AppDrawer();
  397. });
  398. // if we are in small screen change default view to kanban if exists
  399. ViewManager.include({
  400. get_default_view: function() {
  401. var default_view = this._super()
  402. if (config.device.size_class <= config.device.SIZES.XS &&
  403. default_view.type != 'kanban' &&
  404. this.views['kanban'])
  405. {
  406. default_view.type = 'kanban';
  407. };
  408. return default_view;
  409. },
  410. });
  411. return {
  412. 'AppDrawer': AppDrawer,
  413. 'SearchView': SearchView,
  414. 'Menu': Menu,
  415. 'ViewManager': ViewManager,
  416. };
  417. });