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

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