Browse Source

[ADD] web_responsive: Create new module

* Create App Drawer for primary menu
* Move secondary menu to top bar
* Add keyboard shortcuts for primary menu navigation
* Make web client slightly more mobile compatible (no widgets)
pull/443/head
Dave Lasley 8 years ago
parent
commit
b2353e2671
  1. 79
      web_app_drawer/README.rst
  2. 1
      web_app_drawer/__init__.py
  3. 21
      web_app_drawer/__openerp__.py
  4. BIN
      web_app_drawer/static/description/icon.png
  5. 501
      web_app_drawer/static/lib/css/drawer.3.2.0.css
  6. 177
      web_app_drawer/static/lib/js/drawer.3.2.0.js
  7. 2148
      web_app_drawer/static/lib/js/iscroll-probe.5.2.0.js
  8. 292
      web_app_drawer/static/src/js/web_app_drawer.js
  9. 85
      web_app_drawer/static/src/less/app_drawer.less
  10. 13
      web_app_drawer/static/src/less/main.less
  11. 41
      web_app_drawer/static/src/less/navbar.less
  12. 15
      web_app_drawer/static/src/less/variables.less
  13. 291
      web_app_drawer/static/tests/js/web_app_drawer.js
  14. 16
      web_app_drawer/tests/test_ui.py
  15. 46
      web_app_drawer/views/assets.xml
  16. 236
      web_app_drawer/views/web.xml

79
web_app_drawer/README.rst

@ -0,0 +1,79 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
==============
Web App Drawer
==============
This module moves the side navigation panel to a top menu bar. It also provides
an app drawer that is mobile-compliant.
Installation
============
Configuration
=============
Usage
=====
Keyboard Shortcuts
------------------
The following keyboard shortcuts are implemented:
* Toggle App Drawer - `ActionKey <https://en.wikipedia.org/wiki/Access_key#Access_in_different_browsers`_+``A``
* Navigate Apps Drawer - Arrow Keys
* Type to select App Links
* ``esc`` to close App Drawer
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/154/9.0
Known issues / Roadmap
======================
* Provide keyboard navigation to secondary (top) menu
* Drag drawer from left to open in mobile
* Figure out how to test focus on hidden elements for keyboard nav tests
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/web/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Dave Lasley <dave@laslabs.com>
* Jairo Llopis <jairo.llopis@tecnativa.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
This module is maintained by the OCA.
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
To contribute to this module, please visit https://odoo-community.org.

1
web_app_drawer/__init__.py

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

21
web_app_drawer/__openerp__.py

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
{
"name": "Web App Drawer",
"summary": "It moves the sidebar to a main nav and adds an app drawer",
"version": "9.0.1.0.0",
"category": "Website",
"website": "https://laslabs.com/",
"author": "LasLabs, Odoo Community Association (OCA)",
"license": "LGPL-3",
"installable": True,
"depends": [
'web',
],
"data": [
'views/assets.xml',
'views/web.xml',
],
}

BIN
web_app_drawer/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

501
web_app_drawer/static/lib/css/drawer.3.2.0.css

@ -0,0 +1,501 @@
/*!
* jquery-drawer v3.2.0
* Flexible drawer menu using jQuery, iScroll and CSS.
* http://git.blivesta.com/drawer
* License : MIT
* Author : blivesta <design@blivesta.com> (http://blivesta.com/)
*/
/*!------------------------------------*\
Base
\*!------------------------------------*/
.drawer-nav {
position: fixed;
z-index: 101;
top: 0;
overflow: hidden;
width: 16.25rem;
height: 100%;
color: #222;
background-color: #fff;
}
.drawer-brand {
font-size: 1.5rem;
font-weight: bold;
line-height: 3.75rem;
display: block;
padding-right: .75rem;
padding-left: .75rem;
text-decoration: none;
color: #222;
}
.drawer-menu {
margin: 0;
padding: 0;
list-style: none;
}
.drawer-menu-item {
font-size: 1rem;
display: block;
padding: .75rem;
text-decoration: none;
color: #222;
}
.drawer-menu-item:hover {
text-decoration: underline;
color: #555;
background-color: transparent;
}
/*! overlay */
.drawer-overlay {
position: fixed;
z-index: 100;
top: 0;
left: 0;
display: none;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .2);
}
.drawer-open .drawer-overlay {
display: block;
}
/*!------------------------------------*\
Top
\*!------------------------------------*/
.drawer--top .drawer-nav {
top: -100%;
left: 0;
width: 100%;
height: auto;
max-height: 100%;
-webkit-transition: top .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
transition: top .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
}
.drawer--top.drawer-open .drawer-nav {
top: 0;
}
.drawer--top .drawer-hamburger,
.drawer--top.drawer-open .drawer-hamburger {
right: 0;
}
/*!------------------------------------*\
Left
\*!------------------------------------*/
.drawer--left .drawer-nav {
left: -16.25rem;
-webkit-transition: left .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
transition: left .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
}
.drawer--left.drawer-open .drawer-nav,
.drawer--left .drawer-hamburger,
.drawer--left.drawer-open .drawer-navbar .drawer-hamburger {
left: 0;
}
.drawer--left.drawer-open .drawer-hamburger {
left: 16.25rem;
}
/*!------------------------------------*\
Right
\*!------------------------------------*/
.drawer--right .drawer-nav {
right: -16.25rem;
-webkit-transition: right .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
transition: right .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
}
.drawer--right.drawer-open .drawer-nav,
.drawer--right .drawer-hamburger,
.drawer--right.drawer-open .drawer-navbar .drawer-hamburger {
right: 0;
}
.drawer--right.drawer-open .drawer-hamburger {
right: 16.25rem;
}
/*!------------------------------------*\
Hamburger
\*!------------------------------------*/
.drawer-hamburger {
position: fixed;
z-index: 104;
top: 0;
display: block;
box-sizing: content-box;
width: 2rem;
padding: 0;
padding-top: 18px;
padding-right: .75rem;
padding-bottom: 30px;
padding-left: .75rem;
-webkit-transition: all .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
transition: all .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
border: 0;
outline: 0;
background-color: transparent;
}
.drawer-hamburger:hover {
cursor: pointer;
background-color: transparent;
}
.drawer-hamburger-icon {
position: relative;
display: block;
margin-top: 10px;
}
.drawer-hamburger-icon,
.drawer-hamburger-icon:before,
.drawer-hamburger-icon:after {
width: 100%;
height: 2px;
-webkit-transition: all .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
transition: all .6s cubic-bezier(0.190, 1.000, 0.220, 1.000);
background-color: #222;
}
.drawer-hamburger-icon:before,
.drawer-hamburger-icon:after {
position: absolute;
top: -10px;
left: 0;
content: ' ';
}
.drawer-hamburger-icon:after {
top: 10px;
}
.drawer-open .drawer-hamburger-icon {
background-color: transparent;
}
.drawer-open .drawer-hamburger-icon:before,
.drawer-open .drawer-hamburger-icon:after {
top: 0;
}
.drawer-open .drawer-hamburger-icon:before {
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.drawer-open .drawer-hamburger-icon:after {
-webkit-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
transform: rotate(-45deg);
}
/*!------------------------------------*\
accessibility
\*!------------------------------------*/
/*!
* Only display content to screen readers
* See: http://a11yproject.com/posts/how-to-hide-content
*/
.sr-only {
position: absolute;
overflow: hidden;
clip: rect(0, 0, 0, 0);
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
}
/*!
* Use in conjunction with .sr-only to only display content when it's focused.
* Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
* Credit: HTML5 Boilerplate
*/
.sr-only-focusable:active,
.sr-only-focusable:focus {
position: static;
overflow: visible;
clip: auto;
width: auto;
height: auto;
margin: 0;
}
/*!------------------------------------*\
Sidebar
\*!------------------------------------*/
.drawer--sidebar {
background-color: #fff;
}
.drawer--sidebar .drawer-contents {
background-color: #fff;
}
@media (min-width: 64em) {
.drawer--sidebar .drawer-hamburger {
display: none;
visibility: hidden;
}
.drawer--sidebar .drawer-nav {
display: block;
-webkit-transform: none;
-ms-transform: none;
transform: none;
position: fixed;
width: 12.5rem;
height: 100%;
}
/*! Left */
.drawer--sidebar.drawer--left .drawer-nav {
left: 0;
border-right: 1px solid #ddd;
}
.drawer--sidebar.drawer--left .drawer-contents {
margin-left: 12.5rem;
}
/*! Right */
.drawer--sidebar.drawer--right .drawer-nav {
right: 0;
border-left: 1px solid #ddd;
}
.drawer--sidebar.drawer--right .drawer-contents {
margin-right: 12.5rem;
}
/*! container */
.drawer--sidebar .drawer-container {
max-width: 48rem;
}
}
@media (min-width: 75em) {
.drawer--sidebar .drawer-nav {
width: 16.25rem;
}
.drawer--sidebar.drawer--left .drawer-contents {
margin-left: 16.25rem;
}
.drawer--sidebar.drawer--right .drawer-contents {
margin-right: 16.25rem;
}
/*! container */
.drawer--sidebar .drawer-container {
max-width: 60rem;
}
}
/*!------------------------------------*\
Navbar
\*!------------------------------------*/
.drawer--navbarTopGutter {
padding-top: 3.75rem;
}
.drawer-navbar .drawer-navbar-header {
border-bottom: 1px solid #ddd;
background-color: #fff;
}
.drawer-navbar {
z-index: 102;
top: 0;
width: 100%;
}
/*! .drawer-navbar modifier */
.drawer-navbar--fixed {
position: fixed;
}
.drawer-navbar-header {
position: relative;
z-index: 102;
box-sizing: border-box;
width: 100%;
height: 3.75rem;
padding: 0 .75rem;
text-align: center;
}
.drawer-navbar .drawer-brand {
line-height: 3.75rem;
display: inline-block;
padding-top: 0;
padding-bottom: 0;
text-decoration: none;
}
.drawer-navbar .drawer-brand:hover {
background-color: transparent;
}
.drawer-navbar .drawer-nav {
padding-top: 3.75rem;
}
.drawer-navbar .drawer-menu {
padding-bottom: 7.5rem;
}
@media (min-width: 64em) {
.drawer-navbar {
height: 3.75rem;
border-bottom: 1px solid #ddd;
background-color: #fff;
}
.drawer-navbar .drawer-navbar-header {
position: relative;
display: block;
float: left;
width: auto;
padding: 0;
border: 0;
}
.drawer-navbar .drawer-menu--right {
float: right;
}
.drawer-navbar .drawer-menu li {
float: left;
}
.drawer-navbar .drawer-menu-item {
line-height: 3.75rem;
padding-top: 0;
padding-bottom: 0;
}
.drawer-navbar .drawer-hamburger {
display: none;
}
.drawer-navbar .drawer-nav {
position: relative;
left: 0;
overflow: visible;
width: auto;
height: 3.75rem;
padding-top: 0;
-webkit-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
.drawer-navbar .drawer-menu {
padding: 0;
}
/*! dropdown */
.drawer-navbar .drawer-dropdown-menu {
position: absolute;
width: 16.25rem;
border: 1px solid #ddd;
}
.drawer-navbar .drawer-dropdown-menu-item {
padding-left: .75rem;
}
}
/*!------------------------------------*\
Dropdown
\*!------------------------------------*/
.drawer-dropdown-menu {
display: none;
box-sizing: border-box;
width: 100%;
margin: 0;
padding: 0;
background-color: #fff;
}
.drawer-dropdown-menu > li {
width: 100%;
list-style: none;
}
.drawer-dropdown-menu-item {
line-height: 3.75rem;
display: block;
padding: 0;
padding-right: .75rem;
padding-left: 1.5rem;
text-decoration: none;
color: #222;
}
.drawer-dropdown-menu-item:hover {
text-decoration: underline;
color: #555;
background-color: transparent;
}
/*! open */
.drawer-dropdown.open > .drawer-dropdown-menu {
display: block;
}
/*! drawer-caret */
.drawer-dropdown .drawer-caret {
display: inline-block;
width: 0;
height: 0;
margin-left: 4px;
-webkit-transition: opacity .2s ease, -webkit-transform .2s ease;
transition: opacity .2s ease, -webkit-transform .2s ease;
transition: transform .2s ease, opacity .2s ease;
transition: transform .2s ease, opacity .2s ease, -webkit-transform .2s ease;
-webkit-transform: rotate(0deg);
-ms-transform: rotate(0deg);
transform: rotate(0deg);
vertical-align: middle;
border-top: 4px solid;
border-right: 4px solid transparent;
border-left: 4px solid transparent;
}
/*! open */
.drawer-dropdown.open .drawer-caret {
-webkit-transform: rotate(180deg);
-ms-transform: rotate(180deg);
transform: rotate(180deg);
}
/*!------------------------------------*\
Container
\*!------------------------------------*/
.drawer-container {
margin-right: auto;
margin-left: auto;
}
@media (min-width: 64em) {
.drawer-container {
max-width: 60rem;
}
}
@media (min-width: 75em) {
.drawer-container {
max-width: 70rem;
}
}

177
web_app_drawer/static/lib/js/drawer.3.2.0.js

@ -0,0 +1,177 @@
/*!
* jquery-drawer v3.2.0
* Flexible drawer menu using jQuery, iScroll and CSS.
* http://git.blivesta.com/drawer
* License : MIT
* Author : blivesta <design@blivesta.com> (http://blivesta.com/)
*/
;(function umd(factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('jquery'));
} else {
factory(jQuery);
}
}(function Drawer($) {
'use strict';
var namespace = 'drawer';
var touches = typeof document.ontouchstart != 'undefined';
var __ = {
init: function init(options) {
options = $.extend({
iscroll: {
mouseWheel: true,
preventDefault: false
},
showOverlay: true
}, options);
__.settings = {
state: false,
events: {
opened: 'drawer.opened',
closed: 'drawer.closed'
},
dropdownEvents: {
opened: 'shown.bs.dropdown',
closed: 'hidden.bs.dropdown'
}
};
__.settings.class = $.extend({
nav: 'drawer-nav',
toggle: 'drawer-toggle',
overlay: 'drawer-overlay',
open: 'drawer-open',
close: 'drawer-close',
dropdown: 'drawer-dropdown'
}, options.class);
return this.each(function instantiateDrawer() {
var _this = this;
var $this = $(this);
var data = $this.data(namespace);
if (!data) {
options = $.extend({}, options);
$this.data(namespace, { options: options });
__.refresh.call(_this);
if (options.showOverlay) {
__.addOverlay.call(_this);
}
$('.' + __.settings.class.toggle).on('click.' + namespace, function toggle() {
__.toggle.call(_this);
return _this.iScroll.refresh();
});
$(window).resize(function close() {
__.close.call(_this);
return _this.iScroll.refresh();
});
$('.' + __.settings.class.dropdown)
.on(__.settings.dropdownEvents.opened + ' ' + __.settings.dropdownEvents.closed, function onOpenedOrClosed() {
return _this.iScroll.refresh();
});
}
}); // end each
},
refresh: function refresh() {
this.iScroll = new IScroll(
'.' + __.settings.class.nav,
$(this).data(namespace).options.iscroll
);
},
addOverlay: function addOverlay() {
var _this = this;
var $this = $(this);
var $overlay = $('<div>').addClass(__.settings.class.overlay + ' ' + __.settings.class.toggle);
return $this.append($overlay);
},
toggle: function toggle() {
var _this = this;
if (__.settings.state) {
return __.close.call(_this);
} else {
return __.open.call(_this);
}
},
open: function open() {
var $this = $(this);
if (touches) {
$this.on('touchmove.' + namespace, function disableTouch(event) {
event.preventDefault();
});
}
return $this
.removeClass(__.settings.class.close)
.addClass(__.settings.class.open)
.css({ 'overflow': 'hidden' })
.drawerCallback(function triggerOpenedListeners() {
__.settings.state = true;
$this.trigger(__.settings.events.opened);
});
},
close: function close() {
var $this = $(this);
if (touches) $this.off('touchmove.' + namespace);
return $this
.removeClass(__.settings.class.open)
.addClass(__.settings.class.close)
.css({ 'overflow': 'auto' })
.drawerCallback(function triggerClosedListeners() {
__.settings.state = false;
$this.trigger(__.settings.events.closed);
});
},
destroy: function destroy() {
return this.each(function destroyEach() {
var $this = $(this);
$(window).off('.' + namespace);
$this.removeData(namespace);
});
}
};
$.fn.drawerCallback = function drawerCallback(callback) {
var end = 'transitionend webkitTransitionEnd';
return this.each(function setAnimationEndHandler() {
var $this = $(this);
$this.on(end, function invokeCallbackOnAnimationEnd() {
$this.off(end);
return callback.call(this);
});
});
};
$.fn.drawer = function drawer(method) {
if (__[method]) {
return __[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if (typeof method === 'object' || !method) {
return __.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.' + namespace);
}
};
}));

2148
web_app_drawer/static/lib/js/iscroll-probe.5.2.0.js
File diff suppressed because it is too large
View File

292
web_app_drawer/static/src/js/web_app_drawer.js

@ -0,0 +1,292 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define('web_app_drawer', function(require) {
'use strict';
var $ = require('$');
var Menu = require('web.Menu');
var Class = require('web.Class');
var SearchView = require('web.SearchView');
var core = require('web.core');
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({
LEFT: 'left',
RIGHT: 'right',
UP: 'up',
DOWN: 'down',
isOpen: false,
keyBuffer: '',
keyBufferTime: 500,
keyBufferTimeoutEvent: false,
dropdownHeightFactor: 0.90,
initialized: 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.initDrawer();
var $clickZones = $('.openerp_webclient_container, ' +
'a.oe_menu_leaf, ' +
'a.oe_menu_toggler'
);
$clickZones.click($.proxy(this.handleClickZones, this));
core.bus.on('resize', this, this.handleWindowResize);
core.bus.on('keydown', this, this.handleNavKeys);
},
// It 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 + ')'
);
};
this.iScroll.options.probeType = 2;
this.iScroll.on('scroll', $.proxy(onIScroll, this));
});
this.initialized = true;
},
// It provides handlers to hide drawer when "unfocused"
handleClickZones: function() {
this.$el.drawer('close');
$('.oe_secondary_menus_container')
.parent()
.collapse('hide');
},
// It 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){
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();
} else {
var buffer = this.handleKeyBuffer(e.keyCode);
this.selectAppLink(this.searchAppLinks(buffer));
}
},
/* It adds to keybuffer, sets expire timer, and returns buffer
* @returns str of current buffer
*/
handleKeyBuffer: function(keyCode) {
this.keyBuffer += String.fromCharCode(keyCode);
if (this.keyBufferTimeoutEvent) {
clearTimeout(this.keyBufferTimeoutEvent);
}
this.keyBufferTimeoutEvent = setTimeout(
$.proxy(this.clearKeyBuffer, this),
this.keyBufferTime
);
return this.keyBuffer;
},
clearKeyBuffer: function() {
this.keyBuffer = '';
},
/* It performs close actions
* @fires ``drawer.closed`` to the ``core.bus``
* @listens ``drawer.opened`` and sends to onDrawerOpen
*/
onDrawerClose: function() {
core.bus.trigger('drawer.closed');
this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
this.isOpen = false;
},
/* It 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.$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();
}
},
/* It returns first App Link by its name according to query
* @param query str to search
* @return jQuery obj
*/
searchAppLinks: function(query) {
return this.$appLinks.filter(function() {
return $(this).data('menuName').toUpperCase().startsWith(query);
}).first();
},
/* It returns the link adjacent to $appLink 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 $appLink jQuery obj of App icon link
* @param direction str of direction to go (constants LEFT, UP, etc.)
* @return jQuery obj for adjacent applink
*/
findAdjacentAppLink: function($appLink, direction) {
var obj = [],
$objs = this.$appLinks;
switch(direction){
case this.LEFT:
obj = $objs[$objs.index($appLink) - 1];
if (!obj) {
obj = $objs[$objs.length - 1];
}
break;
case this.RIGHT:
obj = $objs[$objs.index($appLink) + 1];
if (!obj) {
obj = $objs[0];
}
break;
case this.UP:
$objs = this.getRowObjs($appLink, this.$appLinks);
obj = $objs[$objs.index($appLink) - 1];
if (!obj) {
obj = $objs[$objs.length - 1];
}
break;
case this.DOWN:
$objs = this.getRowObjs($appLink, this.$appLinks);
obj = $objs[$objs.index($appLink) + 1];
if (!obj) {
obj = $objs[0];
}
break;
}
if (obj.length) {
event.preventDefault();
}
return $(obj);
},
/* It 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));
},
});
// It inits a new AppDrawer when the web client is ready
core.bus.on('web_client_ready', null, function () {
new AppDrawer();
});
return {
'AppDrawer': AppDrawer,
'SearchView': SearchView,
'Menu': Menu,
};
});

85
web_app_drawer/static/src/less/app_drawer.less

@ -0,0 +1,85 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.app-drawer-nav {
border-color: @dropdown-border;
background-color: @dropdown-bg;
border: 1px solid @dropdown-fallback-border; // IE8 fallback
border: 1px solid @dropdown-border;
-webkit-border-radius: @border-radius-base;
-moz-border-radius: @border-radius-base;
border-radius: @border-radius-base;
.box-shadow(0 6px 12px rgba(0, 0, 0, .175));
background-clip: padding-box;
.navbar-left {
width: 100%;
li {
padding: 0;
}
}
.app-drawer-title {
float: none;
}
.app-drawer-panel-title {
margin-top: 4px;
}
.app-drawer-icon-app {
height: @app-drawer-icon-size;
width: @app-drawer-icon-size;
margin: @app-drawer-icon-margin;
}
.panel-body {
padding-top: @app-drawer-title-height;
}
#appDrawerAppPanelHead {
position: absolute;
height: @app-drawer-title-height;
width: 100%;
z-index: 9999;
}
}
.drawer-nav {
width: @app-drawer-width;
z-index: 9999;
}
.drawer--left .drawer-nav {
left: -@app-drawer-width;
}
.drawer--left.drawer-open .drawer-hamburger {
left: @app-drawer-width;
}
.drawer--right .drawer-nav {
right: -@app-drawer-width;
}
.drawer-open .oe-right-toolbar {
display: none;
}
.drawer-closed .oe-right-toolbar {
display: block;
}
/* App Drawer Toggle */
.app-drawer-toggle {
background-color: transparent;
}
.app-drawer-toggle.navbar-toggle {
margin-left: 1em;
}

13
web_app_drawer/static/src/less/main.less

@ -0,0 +1,13 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
body {
width: 100%;
height: 100%;
}
main {
width: 100%;
height: 100%;
overflow: hidden;
}

41
web_app_drawer/static/src/less/navbar.less

@ -0,0 +1,41 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.main-nav {
margin-bottom: 0;
min-height: @app-drawer-navbar-height;
}
.main-nav ul.nav > li > a {
padding: @app-drawer-navbar-padding-vertical @app-drawer-padding-horizontal;
}
.oe_topbar_avatar {
margin-top: -@app-drawer-padding-horizontal;
height: @app-drawer-navbar-height;
border-radius: 50%;
}
a.navbar-collapse.collapse {
@media (min-width: @screen-sm) {
padding-bottom: @app-drawer-navbar-padding-vertical;
padding-top: @app-drawer-navbar-padding-vertical;
}
}
.dropdown-scrollable {
overflow-x: hidden;
}
.badge {
position: absolute;
right: @app-drawer-padding-horizontal;
}
@media (max-width: @screen-sm - 1px) {
#odooMenuBarNav[aria-expanded="false"] {
/* Hack to hide the visibly expanded mobile menu on load. */
position: absolute;
z-index: -9999;
}
}

15
web_app_drawer/static/src/less/variables.less

@ -0,0 +1,15 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// App Drawer / Icons
@app-drawer-icon-size: 7em;
@app-drawer-icon-margin: 1em;
@app-drawer-width: 80%;
@app-drawer-title-height: @navbar-height;
// Navbar
@app-drawer-navbar-height: @navbar-height / 2;
@app-drawer-navbar-padding-vertical: @navbar-padding-vertical / 2;
@app-drawer-padding-horizontal: @navbar-padding-horizontal / 2;

291
web_app_drawer/static/tests/js/web_app_drawer.js

@ -0,0 +1,291 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define_section('web_app_drawer', ['web_app_drawer'], function(test) {
"use strict";
// It provides a base drawer compatible interface for testing
self.initInterface = function(AppDrawer) {
var $el = $('<div class="drawer drawer--left">');
$el.append(
$('<header role="banner">')
.append(
$('<button class="drawer-toggle"><span class="drawer-hamburger-icon">')
)
.append(
$('<nav class="drawer-nav"><ul class="drawer-menu"><li class="drawer-menu-item">')
)
.append(
$('<div class="panel-title" id="appDrawerAppPanelHead">')
)
).append($('<main role="main">'));
self.$clickZone = $('<a class="oe_menu_leaf">');
self.$secondaryMenu = $('<div><div class="oe_secondary_menus_container">');
self.$dropdown = $('<div class="dropdown-scrollable">');
$el.append(self.$clickZone)
.append(self.$secondaryMenu)
.append(self.$dropdown);
var $document = $("#qunit-fixture");
$document.append($el);
self.drawer = new AppDrawer.AppDrawer();
return $document;
};
self.linkGrid = function() {
for(var i=0; i < 3; i++){
self.drawer.$el.append(
$('<div class="row">').append(
$('<a class="col-md-6" id="a_' + i + '"><span class="app-drawer-icon-app /></a>' +
'<a class="col-md-6" id="b_' + i + '"><span class="app-drawer-icon-app /></a>'
)
)
);
self.drawer.$appLinks = $('a.col-md-6');
}
};
test('It should set initialized after success init',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
assert.ok(self.drawer.initialized);
}
);
test('It should close drawer after click on clickZone',
{asserts: 1},
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.$clickZone.click();
var d = $.Deferred();
setTimeout(function() {
assert.ok(self.drawer.$el.hasClass('drawer-close'));
d.resolve();
}, 100);
return d;
}
);
test('It should collapse open secondary menus during handleClickZones',
{asserts: 1},
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.$clickZone.click();
var d = $.Deferred();
setTimeout(function() {
assert.equal(self.$secondaryMenu.attr('aria-expanded'), 'false');
d.resolve();
}, 100);
return d;
}
);
test('It should update max-height on scrollable dropdowns',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.drawer.handleWindowResize();
var height = $(window).height() * self.drawer.dropdownHeightFactor;
assert.equal(
self.$dropdown.css('max-height'),
height + 'px'
);
}
);
test('It should return keybuffer + new key',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.drawer.keyBuffer = 'TES';
var res = self.drawer.handleKeyBuffer(84);
assert.equal(res, 'TEST');
}
);
test('It should clear keybuffer after timeout',
{asserts: 1},
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.drawer.keyBuffer = 'TES';
self.drawer.keyBufferTime = 10;
self.drawer.handleKeyBuffer(84);
var d = $.Deferred();
setTimeout(function() {
assert.equal(self.drawer.keyBuffer, "");
d.resolve();
}, 100);
return d;
}
);
test('It should trigger core bus event for drawer close',
['web.core'], {asserts: 1},
function(assert, AppDrawer, core) {
self.initInterface(AppDrawer);
self.drawer.onDrawerOpen();
var d = $.Deferred();
core.bus.on('drawer.closed', this, function() {
assert.ok(true);
d.resolve();
});
self.drawer.$el.trigger({type: 'drawer.closed'});
return d;
}
);
test('It should set isOpen to false when closing',
{asserts: 1},
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.drawer.onDrawerOpen();
var d = $.Deferred();
setTimeout(function() {
assert.equal(self.drawer.isOpen, false);
d.resolve();
});
self.drawer.$el.trigger({type: 'drawer.closed'});
return d;
}
);
test('It should set isOpen to true when opening',
{asserts: 1},
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
var d = $.Deferred();
self.drawer.$el.trigger({type: 'drawer.opened'});
setTimeout(function() {
assert.ok(self.drawer.isOpen);
d.resolve();
});
return d;
}
);
test('It should trigger core bus event for drawer open',
['web.core'], {asserts: 1},
function(assert, AppDrawer, core) {
self.initInterface(AppDrawer);
self.drawer.onDrawerOpen();
var d = $.Deferred();
core.bus.on('drawer.opened', this, function() {
assert.ok(true);
d.resolve();
});
self.drawer.$el.trigger({type: 'drawer.opened'});
return d;
}
);
test('It should choose link to right',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.linkGrid();
var $appLink = $('#a_1'),
$expect = $('#a_2'),
$res = self.drawer.findAdjacentAppLink(
$appLink, self.drawer.RIGHT
);
assert.equal($res[0].id, $expect[0].id);
}
);
test('It should choose link to left',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.linkGrid();
var $appLink = $('#a_2'),
$expect = $('#a_1'),
$res = self.drawer.findAdjacentAppLink(
$appLink, self.drawer.LEFT
);
assert.equal($res[0].id, $expect[0].id);
}
);
test('It should choose link above',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.linkGrid();
var $appLink = $('#a_1'),
$expect = $('#a_0'),
$res = self.drawer.findAdjacentAppLink(
$appLink, self.drawer.UP
);
assert.equal($res[0].id, $expect[0].id);
}
);
test('It should choose link below',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.linkGrid();
var $appLink = $('#a_1'),
$expect = $('#a_2'),
$res = self.drawer.findAdjacentAppLink(
$appLink, self.drawer.DOWN
);
assert.equal($res[0].id, $expect[0].id);
}
);
test('It should choose first link if next on last',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.linkGrid();
var $appLink = $('#b_2'),
$expect = $('#a_0'),
$res = self.drawer.findAdjacentAppLink(
$appLink, self.drawer.RIGHT
);
assert.equal($res[0].id, $expect[0].id);
}
);
test('It should choose bottom link if up on top',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.linkGrid();
var $appLink = $('#a_0'),
$expect = $('#a_2'),
$res = self.drawer.findAdjacentAppLink(
$appLink, self.drawer.UP
);
assert.equal($res[0].id, $expect[0].id);
}
);
test('It should choose top link if down on bottom',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.linkGrid();
var $appLink = $('#a_2'),
$expect = $('#a_0'),
$res = self.drawer.findAdjacentAppLink(
$appLink, self.drawer.DOWN
);
assert.equal($res[0].id, $expect[0].id);
}
);
test('It should move the panel header to negative iScroll.y',
function(assert, AppDrawer) {
self.initInterface(AppDrawer);
self.drawer.$el.iScroll.y = -200;
self.drawer.onIScroll();
var $header = $('#appDrawerAppPanelHead');
assert.equal(
$header.css('transform'),
'matrix(1, 0, 0, 1, 0, 200)'
);
}
);
});

16
web_app_drawer/tests/test_ui.py

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2016 LasLabs Inc.
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from openerp.tests import HttpCase
class TestUi(HttpCase):
def test_ui_web(self):
"""Test backend tests."""
self.phantom_js(
"/web/tests?module=web_app_drawer",
"",
login="admin",
)

46
web_app_drawer/views/assets.xml

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 LasLabs Inc.
@author Dave Lasley <dave@laslabs.com>
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<template id="assets_backend" name="Open Mobile Assets" inherit_id="web.assets_backend">
<xpath expr=".">
<link rel="stylesheet"
type="text/css"
href="/web_app_drawer/static/lib/css/drawer.3.2.0.css"
/>
<link rel="stylesheet"
href="/web_app_drawer/static/src/less/main.less"
/>
<link rel="stylesheet"
href="/web_app_drawer/static/src/less/navbar.less"
/>
<link rel="stylesheet"
href="/web_app_drawer/static/src/less/app_drawer.less"
/>
<link rel="stylesheet"
href="/web_app_drawer/static/src/less/variables.less"
/>
<script type="application/javascript"
src="/web_app_drawer/static/lib/js/iscroll-probe.5.2.0.js"
/>
<script type="application/javascript"
src="/web_app_drawer/static/lib/js/drawer.3.2.0.js"
/>
<script type="application/javascript"
src="/web_app_drawer/static/src/js/web_app_drawer.js"
/>
</xpath>
</template>
<template id="qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="//html/head" position="inside">
<script type="application/javascript"
src="/web_app_drawer/static/tests/js/web_app_drawer.js"
/>
</xpath>
</template>
</odoo>

236
web_app_drawer/views/web.xml

@ -0,0 +1,236 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<odoo>
<template id="webclient_bootstrap"
inherit_id="web.webclient_bootstrap"
name="App Drawer - Web Client"
>
<xpath expr="//div[@class='oe_leftbar']" position="replace" />
<xpath expr="//t[@t-set='head']" position="inside">
<meta charset="utf-8" />
<meta http-equiv="cleartype" content="on" />
<meta name="MobileOptimized" content="320" />
<meta name="HandheldFriendly" content="True" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
</xpath>
<xpath expr="//nav[@id='oe_main_menu_navbar']" position="replace">
<t t-set="body_classname" t-value="'drawer drawer--left'" />
<header role="banner">
<nav id="odooAppDrawer" class="app-drawer-nav drawer-nav" role="navigation">
<t t-call="web.menu" />
</nav>
<nav class="navbar navbar-default main-nav"
role="navigation"
groups="base.group_user,base.group_portal"
>
<div class="container-fluid">
<div class="navbar-header">
<a href="#"
class="drawer-toggle pull-left navbar-collapse collapse btn btn-default app-drawer-toggle"
accesskey="A"
>
<span class="sr-only">Toggle App Drawer</span>
<i class="fa fa-th fa-lg app-drawer-icon-open" />
</a>
<button type="button"
class="app-drawer-toggle drawer-toggle pull-left navbar-toggle collapsed"
>
<span class="sr-only">Toggle App Drawer</span>
<i class="fa fa-th fa-lg app-drawer-icon-open" />
</button>
<button type="button"
id="odooMenuBarToggle"
class="navbar-toggle collapsed pull-right"
data-toggle="collapse"
data-target="#odooMenuBarNav"
>
<span class="sr-only">Toggle Navigation</span>
<i class="fa fa-bars fa-lg" />
</button>
</div>
<div class="collapse navbar-collapse text-center"
id="odooMenuBarNav"
data-parent="#odooMenuBarToggle"
aria-expanded="false"
>
<div class="oe_leftbar"
groups="base.group_user,base.group_portal"
>
<t t-call="web.menu_secondary" />
</div>
<div class="nav navbar-nav navbar-right">
<ul class="nav navbar-nav navbar-right oe_user_menu_placeholder"
style="display: none;"
/>
<ul class="nav navbar-nav navbar-right oe_systray"
style="display: none;"
/>
</div>
</div>
</div>
</nav>
</header>
</xpath>
</template>
<template id="menu_secondary"
inherit_id="web.menu_secondary"
name="App Drawer - Secondary Menu"
>
<xpath expr="//a[@class='oe_logo']" position="replace" />
<xpath expr="//div[@class='oe_footer']" position="replace" />
<xpath expr="//div[@class='oe_secondary_menus_container']/t" position="replace">
<t t-foreach="menu_data['children']" t-as="menu">
<ul style="display: none" class="oe_secondary_menu nav navbar-nav" t-att-data-menu-parent="menu['id']">
<t t-call="web.menu_secondary_submenu" />
</ul>
</t>
</xpath>
</template>
<template id="menu_secondary_submenu"
inherit_id="web.menu_secondary_submenu"
name="App Drawer - Secondary Submenu"
>
<xpath expr="//ul" position="replace">
<t t-foreach="menu['children']" t-as="menu">
<t t-if="menu['children']">
<li t-attf-class="{{ 'dropdown-header' if submenu else '' }}">
<t t-if="submenu">
<t t-esc="menu['name']" />
<t t-call="web.menu_secondary_submenu">
<t t-set="submenu" t-value="True" />
</t>
</t>
<t t-if="not submenu">
<a href="#"
class="dropdown-toggle"
data-toggle="dropdown"
role="button"
aria-haspopup="true"
aria-expanded="false"
>
<t t-esc="menu['name']" />
<span class="caret" />
</a>
<ul t-if="menu['children']"
t-attf-class="dropdown-menu oe_secondary_submenu dropdown-scrollable"
>
<t t-call="web.menu_secondary_submenu">
<t t-set="submenu" t-value="True" />
</t>
</ul>
</t>
</li>
</t>
<t t-if="not menu['children']">
<li>
<t t-call="web.menu_link" />
</li>
</t>
</t>
</xpath>
</template>
<template id="menu_link"
inherit_id="web.menu_link"
name="App Drawer - Menu Link"
>
<xpath expr="//a" position="attributes">
<attribute name="t-att-data-menu-name">menu['name']</attribute>
</xpath>
<xpath expr="//span[@class='oe_menu_text']" position="replace">
<t t-if="display_images">
<img t-attf-src="/web/image/ir.ui.menu/{{ menu['id'] }}/web_icon_data"
class="app-drawer-icon-app img-rounded"
t-att-alt="menu['name']"
t-att-title="menu['name']"
/>
<p class="app-drawer-title text-center">
<t t-esc="menu['name']" />
</p>
</t>
<t t-if="not display_images">
<span class="oe_menu_text">
<t t-esc="menu['name']" />
</span>
</t>
</xpath>
</template>
<template id="menu"
inherit_id="web.menu"
name="App Drawer - Menu"
>
<xpath expr="//ul[contains(@class, 'oe_systray')]" position="replace" />
<xpath expr="//ul[contains(@class, 'oe_user_menu_placeholder')]" position="replace" />
<xpath expr="//ul[contains(@class, 'oe_application_menu_placeholder')]" position="replace">
<div class="panel-default app-drawer-app-panel" id="appDrawerAppMenu">
<div class="panel-heading" id="appDrawerAppPanelHead">
<h4 class="app-drawer-panel-title">
<a href="#" class="app-drawer-icon-close drawer-toggle">
<i class="fa fa-lg fa-chevron-left" />
Apps
</a>
</h4>
</div>
<div class="panel-body" id="appDrawerAppPanelBody">
<ul class="nav navbar-nav navbar-left oe_application_menu_placeholder" style="display: none;">
<li t-foreach="menu_data['children']" t-as="menu" class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center">
<t t-call="web.menu_link">
<t t-set="display_images" t-value="1" />
</t>
</li>
<li id="menu_more_container" class="dropdown" style="display: none;">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">More <b class="caret"></b></a>
<ul id="menu_more" class="dropdown-menu"></ul>
</li>
</ul>
</div>
</div>
</xpath>
</template>
</odoo>
Loading…
Cancel
Save