Browse Source
[MIG] web_responsive: Migrate to v12 and refactor
[MIG] web_responsive: Migrate to v12 and refactor
This migration includes a full refactoring to make this module more maintainable. Some things that have changed: - Removed external libraries. - Change Less for Scss. - Reduce ES and XML to the minimal required needs. - Implement as much features as possible with just Scss. - Remove copyright from `__init__.py` files. - Trigger the new hotkeys system from Odoo v12 with `Shift+Alt` instead of just `Alt`, and restore some good old hotkeys (`E` for "Edit", `D` for "Discard", and `A` for "Apps menu"). See https://github.com/odoo/odoo/issues/30068 on the matter. - Control panel breadcrumbs are collapsed into a single backwards icon. - Add FA icons to most common buttons in control panel. - Hide text in XS for those buttons, to have a slicker phone experience. - Lots of gifs in the README!pull/1066/head
Jairo Llopis
6 years ago
No known key found for this signature in database
GPG Key ID: 59564BF1E22F314F
33 changed files with 1227 additions and 5717 deletions
-
108web_responsive/README.rst
-
3web_responsive/__init__.py
-
11web_responsive/__manifest__.py
-
73web_responsive/i18n/es.po
-
68web_responsive/i18n/web_responsive.pot
-
5web_responsive/models/__init__.py
-
0web_responsive/models/res_users.py
-
62web_responsive/readme/DESCRIPTION.rst
-
28web_responsive/readme/ROADMAP.rst
-
8web_responsive/readme/USAGE.rst
-
95web_responsive/static/description/index.html
-
525web_responsive/static/lib/css/drawer.3.2.2.css
-
765web_responsive/static/lib/js/bililiteRange.2.6.js
-
183web_responsive/static/lib/js/drawer.3.2.2.js
-
2197web_responsive/static/lib/js/iscroll-probe.5.2.0.js
-
57web_responsive/static/lib/js/jquery.sendkeys.4.js
-
427web_responsive/static/src/css/web_responsive.scss
-
799web_responsive/static/src/js/web_responsive.js
-
129web_responsive/static/src/less/app_drawer.less
-
212web_responsive/static/src/less/form_view.less
-
102web_responsive/static/src/less/main.less
-
195web_responsive/static/src/less/navbar.less
-
18web_responsive/static/src/less/variables.less
-
16web_responsive/static/src/xml/app_drawer_menu_search.xml
-
58web_responsive/static/src/xml/apps.xml
-
151web_responsive/static/src/xml/form_view.xml
-
13web_responsive/static/src/xml/navbar.xml
-
290web_responsive/static/tests/js/web_responsive.js
-
1web_responsive/tests/__init__.py
-
15web_responsive/tests/test_ui.py
-
37web_responsive/views/assets.xml
-
0web_responsive/views/res_users.xml
-
293web_responsive/views/web.xml
@ -1,4 +1 @@ |
|||
# Copyright 2018 Alexandre Díaz |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from . import models |
@ -1,4 +1 @@ |
|||
# Copyright 2018 Alexandre Díaz |
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). |
|||
|
|||
from . import inherited_res_users |
|||
from . import res_users |
@ -1,9 +1,57 @@ |
|||
This module provides a mobile compliant interface for Odoo Community web. |
|||
This module adds responsiveness to web backend. |
|||
|
|||
Features: |
|||
Features for all devices: |
|||
|
|||
* New navigation with an App drawer |
|||
* Keyboard shortcuts for easier navigation |
|||
* Display kanban views for small screens if an action or field One2x |
|||
* Set chatter side (Optional per user) |
|||
* Quick search |
|||
* New navigation with an app drawer |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/48417193-09a1e080-e74a-11e8-8a0c-e73eb689b2fb.gif |
|||
|
|||
* Quick menu search from the app drawer |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif |
|||
|
|||
Features for mobile: |
|||
|
|||
* App-specific submenus are shown on full screen when toggling them from the |
|||
"hamburger" menu |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/48417297-51286c80-e74a-11e8-9a47-22c810b18c43.gif |
|||
|
|||
* View type picker dropdown displays confortably |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/50964322-e3d55580-14c6-11e9-8249-48db9539600f.gif |
|||
|
|||
* Top app bar is always visible, but the control panel is hidden when |
|||
scrolling down, to save some vaulable vertical space |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/50964496-5cd4ad00-14c7-11e9-9261-fd223a329d02.gif |
|||
|
|||
* Form status bar action and status buttons are collapsed in dropdowns. |
|||
Other control panel buttons use icons to save space. |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/50965446-e08f9900-14c9-11e9-92d6-dda472cb6557.gif |
|||
|
|||
* Breadcrumbs navigation is collapsed with a "back arrow" button. |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/50965168-1d0ec500-14c9-11e9-82a0-dfee82ed0861.gif |
|||
|
|||
Features for computers: |
|||
|
|||
* Keyboard shortcuts for easier navigation, **using ``Alt + Shift + [key]``** |
|||
combination instead of just ``Alt + [key]``. |
|||
See https://github.com/odoo/odoo/issues/30068 to understand why. |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/48417578-ff341680-e74a-11e8-8881-017709e912bc.png |
|||
|
|||
|
|||
* Autofocus on search menu box when opening the drawer |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif |
|||
|
|||
* Set chatter on the side of the screen, optional per user |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/48417270-41108d00-e74a-11e8-9172-cba825d027ed.gif |
|||
|
|||
* Full width form sheets |
|||
|
|||
.. image:: https://user-images.githubusercontent.com/973709/48417428-ac5a5f00-e74a-11e8-8839-5bc538c54c1d.png |
@ -1,22 +1,6 @@ |
|||
Note: Data added to the footer ``support_branding`` is not shown while using |
|||
this module. |
|||
|
|||
* Provide full menu search feature instead of just App search |
|||
* Drag drawer from left to open in mobile |
|||
* Figure out how to test focus on hidden elements for keyboard nav tests |
|||
* If you resize the window, body gets a wrong ``overflow: auto`` css property |
|||
and you need to refresh your view or open/close the app drawer to fix that. |
|||
* Override LESS styling to allow for responsive widget layouts |
|||
* Adding ``oe_main_menu_navbar`` ID to the top navigation bar triggers some |
|||
great styles, but also `JavaScript that causes issues on mobile |
|||
<https://github.com/OCA/web/pull/446#issuecomment-254827880>`_ |
|||
* Sticky header and footer in list view only works on certain browsers: |
|||
https://caniuse.com/#search=sticky (note that the used feature is in |
|||
`thead`). |
|||
* On Android (FireFox) - clicking the search icon does not |
|||
focus the search input. |
|||
* On Android (FireFox & Chrome) - clicking the search query input will |
|||
show the on screen keyboard for a split second, but the App Drawer |
|||
immediately closes and the keyboard closes with it. |
|||
* Filter menu items completely on client-side, to make it smoother and allow |
|||
users to filter on complete paths of menus and not only on the last item. |
|||
* To view the full experience in a device, the page must be loaded with the |
|||
device screen size. This means that, if you change the size of your browser, |
|||
you should reload the web client to get the full experience for that |
|||
new size. This is Odoo's own limitation. |
|||
* App navigation with keyboard. |
|||
* Make it more beautiful. Maybe OCA-branded? |
@ -1,6 +1,6 @@ |
|||
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 |
|||
* Toggle app drawer - ``Alt + Shift + H`` |
|||
* Navigate app search results - Arrow keys |
|||
* Choose app result - ``Enter`` |
|||
* ``Esc`` to close app drawer |
@ -1,525 +0,0 @@ |
|||
/*! |
|||
* jquery-drawer v3.2.2 |
|||
* 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-open { |
|||
overflow: hidden !important; |
|||
} |
|||
|
|||
.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); |
|||
transform: rotate(45deg); |
|||
} |
|||
|
|||
.drawer-open .drawer-hamburger-icon:after { |
|||
-webkit-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; |
|||
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); |
|||
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); |
|||
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; |
|||
} |
|||
} |
@ -1,765 +0,0 @@ |
|||
// Cross-broswer implementation of text ranges and selections
|
|||
// documentation: http://bililite.com/blog/2011/01/17/cross-browser-text-ranges-and-selections/
|
|||
// Version: 2.6
|
|||
// Copyright (c) 2013 Daniel Wachsstock
|
|||
// MIT license:
|
|||
// Permission is hereby granted, free of charge, to any person
|
|||
// obtaining a copy of this software and associated documentation
|
|||
// files (the "Software"), to deal in the Software without
|
|||
// restriction, including without limitation the rights to use,
|
|||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|||
// copies of the Software, and to permit persons to whom the
|
|||
// Software is furnished to do so, subject to the following
|
|||
// conditions:
|
|||
|
|||
// The above copyright notice and this permission notice shall be
|
|||
// included in all copies or substantial portions of the Software.
|
|||
|
|||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|||
// OTHER DEALINGS IN THE SOFTWARE.
|
|||
|
|||
(function(){ |
|||
|
|||
// a bit of weirdness with IE11: using 'focus' is flaky, even if I'm not bubbling, as far as I can tell.
|
|||
var focusEvent = 'onfocusin' in document.createElement('input') ? 'focusin' : 'focus'; |
|||
|
|||
// IE11 normalize is buggy (http://connect.microsoft.com/IE/feedback/details/809424/node-normalize-removes-text-if-dashes-are-present)
|
|||
var n = document.createElement('div'); |
|||
n.appendChild(document.createTextNode('x-')); |
|||
n.appendChild(document.createTextNode('x')); |
|||
n.normalize(); |
|||
var canNormalize = n.firstChild.length == 3; |
|||
|
|||
|
|||
bililiteRange = function(el, debug){ |
|||
var ret; |
|||
if (debug){ |
|||
ret = new NothingRange(); // Easier to force it to use the no-selection type than to try to find an old browser
|
|||
}else if (window.getSelection && el.setSelectionRange){ |
|||
// Standards. Element is an input or textarea
|
|||
// note that some input elements do not allow selections
|
|||
try{ |
|||
el.selectionStart; // even getting the selection in such an element will throw
|
|||
ret = new InputRange(); |
|||
}catch(e){ |
|||
ret = new NothingRange(); |
|||
} |
|||
}else if (window.getSelection){ |
|||
// Standards, with any other kind of element
|
|||
ret = new W3CRange(); |
|||
}else if (document.selection){ |
|||
// Internet Explorer
|
|||
ret = new IERange(); |
|||
}else{ |
|||
// doesn't support selection
|
|||
ret = new NothingRange(); |
|||
} |
|||
ret._el = el; |
|||
// determine parent document, as implemented by John McLear <john@mclear.co.uk>
|
|||
ret._doc = el.ownerDocument; |
|||
ret._win = 'defaultView' in ret._doc ? ret._doc.defaultView : ret._doc.parentWindow; |
|||
ret._textProp = textProp(el); |
|||
ret._bounds = [0, ret.length()]; |
|||
// There's no way to detect whether a focus event happened as a result of a click (which should change the selection)
|
|||
// or as a result of a keyboard event (a tab in) or a script action (el.focus()). So we track it globally, which is a hack, and is likely to fail
|
|||
// in edge cases (right-clicks, drag-n-drop), and is vulnerable to a lower-down handler preventing bubbling.
|
|||
// I just don't know a better way.
|
|||
// I'll hack my event-listening code below, rather than create an entire new bilililiteRange, potentially before the DOM has loaded
|
|||
if (!('bililiteRangeMouseDown' in ret._doc)){ |
|||
var _doc = {_el: ret._doc}; |
|||
ret._doc.bililiteRangeMouseDown = false; |
|||
bililiteRange.fn.listen.call(_doc, 'mousedown', function() { |
|||
ret._doc.bililiteRangeMouseDown = true; |
|||
}); |
|||
bililiteRange.fn.listen.call(_doc, 'mouseup', function() { |
|||
ret._doc.bililiteRangeMouseDown = false; |
|||
}); |
|||
} |
|||
// note that bililiteRangeSelection is an array, which means that copying it only copies the address, which points to the original.
|
|||
// make sure that we never let it (always do return [bililiteRangeSelection[0], bililiteRangeSelection[1]]), which means never returning
|
|||
// this._bounds directly
|
|||
if (!('bililiteRangeSelection' in el)){ |
|||
// start tracking the selection
|
|||
function trackSelection(evt){ |
|||
if (evt && evt.which == 9){ |
|||
// do tabs my way, by restoring the selection
|
|||
// there's a flash of the browser's selection, but I don't see a way of avoiding that
|
|||
ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); |
|||
}else{ |
|||
el.bililiteRangeSelection = ret._nativeSelection(); |
|||
} |
|||
} |
|||
trackSelection(); |
|||
// only IE does this right and allows us to grab the selection before blurring
|
|||
if ('onbeforedeactivate' in el){ |
|||
ret.listen('beforedeactivate', trackSelection); |
|||
}else{ |
|||
// with standards-based browsers, have to listen for every user interaction
|
|||
ret.listen('mouseup', trackSelection).listen('keyup', trackSelection); |
|||
} |
|||
ret.listen(focusEvent, function(){ |
|||
// restore the correct selection when the element comes into focus (mouse clicks change the position of the selection)
|
|||
// Note that Firefox will not fire the focus event until the window/tab is active even if el.focus() is called
|
|||
// https://bugzilla.mozilla.org/show_bug.cgi?id=566671
|
|||
if (!ret._doc.bililiteRangeMouseDown){ |
|||
ret._nativeSelect(ret._nativeRange(el.bililiteRangeSelection)); |
|||
} |
|||
}); |
|||
} |
|||
if (!('oninput' in el)){ |
|||
// give IE8 a chance. Note that this still fails in IE11, which has has oninput on contenteditable elements but does not
|
|||
// dispatch input events. See http://connect.microsoft.com/IE/feedback/details/794285/ie10-11-input-event-does-not-fire-on-div-with-contenteditable-set
|
|||
// TODO: revisit this when I have IE11 running on my development machine
|
|||
var inputhack = function() {ret.dispatch({type: 'input', bubbles: true}) }; |
|||
ret.listen('keyup', inputhack); |
|||
ret.listen('cut', inputhack); |
|||
ret.listen('paste', inputhack); |
|||
ret.listen('drop', inputhack); |
|||
el.oninput = 'patched'; |
|||
} |
|||
return ret; |
|||
} |
|||
|
|||
function textProp(el){ |
|||
// returns the property that contains the text of the element
|
|||
// note that for <body> elements the text attribute represents the obsolete text color, not the textContent.
|
|||
// we document that these routines do not work for <body> elements so that should not be relevant
|
|||
|
|||
// Bugfix for https://github.com/dwachss/bililiteRange/issues/18
|
|||
// Adding typeof check of string for el.value in case for li elements
|
|||
if (typeof el.value === 'string') return 'value'; |
|||
if (typeof el.text != 'undefined') return 'text'; |
|||
if (typeof el.textContent != 'undefined') return 'textContent'; |
|||
return 'innerText'; |
|||
} |
|||
|
|||
// base class
|
|||
function Range(){} |
|||
Range.prototype = { |
|||
length: function() { |
|||
return this._el[this._textProp].replace(/\r/g, '').length; // need to correct for IE's CrLf weirdness
|
|||
}, |
|||
bounds: function(s){ |
|||
if (bililiteRange.bounds[s]){ |
|||
this._bounds = bililiteRange.bounds[s].apply(this); |
|||
}else if (s){ |
|||
this._bounds = s; // don't do error checking now; things may change at a moment's notice
|
|||
}else{ |
|||
var b = [ |
|||
Math.max(0, Math.min (this.length(), this._bounds[0])), |
|||
Math.max(0, Math.min (this.length(), this._bounds[1])) |
|||
]; |
|||
b[1] = Math.max(b[0], b[1]); |
|||
return b; // need to constrain it to fit
|
|||
} |
|||
return this; // allow for chaining
|
|||
}, |
|||
select: function(){ |
|||
var b = this._el.bililiteRangeSelection = this.bounds(); |
|||
if (this._el === this._doc.activeElement){ |
|||
// only actually select if this element is active!
|
|||
this._nativeSelect(this._nativeRange(b)); |
|||
} |
|||
this.dispatch({type: 'select', bubbles: true}); |
|||
return this; // allow for chaining
|
|||
}, |
|||
text: function(text, select){ |
|||
if (arguments.length){ |
|||
var bounds = this.bounds(), el = this._el; |
|||
// signal the input per DOM 3 input events, http://www.w3.org/TR/DOM-Level-3-Events/#h4_events-inputevents
|
|||
// we add another field, bounds, which are the bounds of the original text before being changed.
|
|||
this.dispatch({type: 'beforeinput', bubbles: true, |
|||
data: text, bounds: bounds}); |
|||
this._nativeSetText(text, this._nativeRange(bounds)); |
|||
if (select == 'start'){ |
|||
this.bounds ([bounds[0], bounds[0]]); |
|||
}else if (select == 'end'){ |
|||
this.bounds ([bounds[0]+text.length, bounds[0]+text.length]); |
|||
}else if (select == 'all'){ |
|||
this.bounds ([bounds[0], bounds[0]+text.length]); |
|||
} |
|||
this.dispatch({type: 'input', bubbles: true, |
|||
data: text, bounds: bounds}); |
|||
return this; // allow for chaining
|
|||
}else{ |
|||
return this._nativeGetText(this._nativeRange(this.bounds())).replace(/\r/g, ''); // need to correct for IE's CrLf weirdness
|
|||
} |
|||
}, |
|||
insertEOL: function (){ |
|||
this._nativeEOL(); |
|||
this._bounds = [this._bounds[0]+1, this._bounds[0]+1]; // move past the EOL marker
|
|||
return this; |
|||
}, |
|||
sendkeys: function (text){ |
|||
var self = this; |
|||
this.data().sendkeysOriginalText = this.text(); |
|||
this.data().sendkeysBounds = undefined; |
|||
function simplechar (rng, c){ |
|||
if (/^{[^}]*}$/.test(c)) c = c.slice(1,-1); // deal with unknown {key}s
|
|||
for (var i =0; i < c.length; ++i){ |
|||
var x = c.charCodeAt(i); |
|||
rng.dispatch({type: 'keypress', bubbles: true, keyCode: x, which: x, charCode: x}); |
|||
} |
|||
rng.text(c, 'end'); |
|||
} |
|||
text.replace(/{[^}]*}|[^{]+|{/g, function(part){ |
|||
(bililiteRange.sendkeys[part] || simplechar)(self, part, simplechar); |
|||
}); |
|||
this.bounds(this.data().sendkeysBounds); |
|||
this.dispatch({type: 'sendkeys', which: text}); |
|||
return this; |
|||
}, |
|||
top: function(){ |
|||
return this._nativeTop(this._nativeRange(this.bounds())); |
|||
}, |
|||
scrollIntoView: function(scroller){ |
|||
var top = this.top(); |
|||
// scroll into position if necessary
|
|||
if (this._el.scrollTop > top || this._el.scrollTop+this._el.clientHeight < top){ |
|||
if (scroller){ |
|||
scroller.call(this._el, top); |
|||
}else{ |
|||
this._el.scrollTop = top; |
|||
} |
|||
} |
|||
return this; |
|||
}, |
|||
wrap: function (n){ |
|||
this._nativeWrap(n, this._nativeRange(this.bounds())); |
|||
return this; |
|||
}, |
|||
selection: function(text){ |
|||
if (arguments.length){ |
|||
return this.bounds('selection').text(text, 'end').select(); |
|||
}else{ |
|||
return this.bounds('selection').text(); |
|||
} |
|||
}, |
|||
clone: function(){ |
|||
return bililiteRange(this._el).bounds(this.bounds()); |
|||
}, |
|||
all: function(text){ |
|||
if (arguments.length){ |
|||
this.dispatch ({type: 'beforeinput', bubbles: true, data: text}); |
|||
this._el[this._textProp] = text; |
|||
this.dispatch ({type: 'input', bubbles: true, data: text}); |
|||
return this; |
|||
}else{ |
|||
return this._el[this._textProp].replace(/\r/g, ''); // need to correct for IE's CrLf weirdness
|
|||
} |
|||
}, |
|||
element: function() { return this._el }, |
|||
// includes a quickie polyfill for CustomEvent for IE that isn't perfect but works for me
|
|||
// IE10 allows custom events but not "new CustomEvent"; have to do it the old-fashioned way
|
|||
dispatch: function(opts){ |
|||
opts = opts || {}; |
|||
var event = document.createEvent ? document.createEvent('CustomEvent') : this._doc.createEventObject(); |
|||
event.initCustomEvent && event.initCustomEvent(opts.type, !!opts.bubbles, !!opts.cancelable, opts.detail); |
|||
for (var key in opts) event[key] = opts[key]; |
|||
// dispatch event asynchronously (in the sense of on the next turn of the event loop; still should be fired in order of dispatch
|
|||
var el = this._el; |
|||
setTimeout(function(){ |
|||
try { |
|||
el.dispatchEvent ? el.dispatchEvent(event) : el.fireEvent("on" + opts.type, document.createEventObject()); |
|||
}catch(e){ |
|||
// IE8 will not let me fire custom events at all. Call them directly
|
|||
var listeners = el['listen'+opts.type]; |
|||
if (listeners) for (var i = 0; i < listeners.length; ++i){ |
|||
listeners[i].call(el, event); |
|||
} |
|||
} |
|||
}, 0); |
|||
return this; |
|||
}, |
|||
listen: function (type, func){ |
|||
var el = this._el; |
|||
if (el.addEventListener){ |
|||
el.addEventListener(type, func); |
|||
}else{ |
|||
el.attachEvent("on" + type, func); |
|||
// IE8 can't even handle custom events created with createEventObject (though it permits attachEvent), so we have to make our own
|
|||
var listeners = el['listen'+type] = el['listen'+type] || []; |
|||
listeners.push(func); |
|||
} |
|||
return this; |
|||
}, |
|||
dontlisten: function (type, func){ |
|||
var el = this._el; |
|||
if (el.removeEventListener){ |
|||
el.removeEventListener(type, func); |
|||
}else try{ |
|||
el.detachEvent("on" + type, func); |
|||
}catch(e){ |
|||
var listeners = el['listen'+type]; |
|||
if (listeners) for (var i = 0; i < listeners.length; ++i){ |
|||
if (listeners[i] === func) listeners[i] = function(){}; // replace with a noop
|
|||
} |
|||
} |
|||
return this; |
|||
} |
|||
}; |
|||
|
|||
// allow extensions ala jQuery
|
|||
bililiteRange.fn = Range.prototype; // to allow monkey patching
|
|||
bililiteRange.extend = function(fns){ |
|||
for (fn in fns) Range.prototype[fn] = fns[fn]; |
|||
}; |
|||
|
|||
//bounds functions
|
|||
bililiteRange.bounds = { |
|||
all: function() { return [0, this.length()] }, |
|||
start: function () { return [0,0] }, |
|||
end: function () { return [this.length(), this.length()] }, |
|||
selection: function(){ |
|||
if (this._el === this._doc.activeElement){ |
|||
this.bounds ('all'); // first select the whole thing for constraining
|
|||
return this._nativeSelection(); |
|||
}else{ |
|||
return this._el.bililiteRangeSelection; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
// sendkeys functions
|
|||
bililiteRange.sendkeys = { |
|||
'{enter}': function (rng){ |
|||
rng.dispatch({type: 'keypress', bubbles: true, keyCode: '\n', which: '\n', charCode: '\n'}); |
|||
rng.insertEOL(); |
|||
}, |
|||
'{tab}': function (rng, c, simplechar){ |
|||
simplechar(rng, '\t'); // useful for inserting what would be whitespace
|
|||
}, |
|||
'{newline}': function (rng, c, simplechar){ |
|||
simplechar(rng, '\n'); // useful for inserting what would be whitespace (and if I don't want to use insertEOL, which does some fancy things)
|
|||
}, |
|||
'{backspace}': function (rng){ |
|||
var b = rng.bounds(); |
|||
if (b[0] == b[1]) rng.bounds([b[0]-1, b[0]]); // no characters selected; it's just an insertion point. Remove the previous character
|
|||
rng.text('', 'end'); // delete the characters and update the selection
|
|||
}, |
|||
'{del}': function (rng){ |
|||
var b = rng.bounds(); |
|||
if (b[0] == b[1]) rng.bounds([b[0], b[0]+1]); // no characters selected; it's just an insertion point. Remove the next character
|
|||
rng.text('', 'end'); // delete the characters and update the selection
|
|||
}, |
|||
'{rightarrow}': function (rng){ |
|||
var b = rng.bounds(); |
|||
if (b[0] == b[1]) ++b[1]; // no characters selected; it's just an insertion point. Move to the right
|
|||
rng.bounds([b[1], b[1]]); |
|||
}, |
|||
'{leftarrow}': function (rng){ |
|||
var b = rng.bounds(); |
|||
if (b[0] == b[1]) --b[0]; // no characters selected; it's just an insertion point. Move to the left
|
|||
rng.bounds([b[0], b[0]]); |
|||
}, |
|||
'{selectall}' : function (rng){ |
|||
rng.bounds('all'); |
|||
}, |
|||
'{selection}': function (rng){ |
|||
// insert the characters without the sendkeys processing
|
|||
var s = rng.data().sendkeysOriginalText; |
|||
for (var i =0; i < s.length; ++i){ |
|||
var x = s.charCodeAt(i); |
|||
rng.dispatch({type: 'keypress', bubbles: true, keyCode: x, which: x, charCode: x}); |
|||
} |
|||
rng.text(s, 'end'); |
|||
}, |
|||
'{mark}' : function (rng){ |
|||
rng.data().sendkeysBounds = rng.bounds(); |
|||
} |
|||
}; |
|||
// Synonyms from the proposed DOM standard (http://www.w3.org/TR/DOM-Level-3-Events-key/)
|
|||
bililiteRange.sendkeys['{Enter}'] = bililiteRange.sendkeys['{enter}']; |
|||
bililiteRange.sendkeys['{Backspace}'] = bililiteRange.sendkeys['{backspace}']; |
|||
bililiteRange.sendkeys['{Delete}'] = bililiteRange.sendkeys['{del}']; |
|||
bililiteRange.sendkeys['{ArrowRight}'] = bililiteRange.sendkeys['{rightarrow}']; |
|||
bililiteRange.sendkeys['{ArrowLeft}'] = bililiteRange.sendkeys['{leftarrow}']; |
|||
|
|||
function IERange(){} |
|||
IERange.prototype = new Range(); |
|||
IERange.prototype._nativeRange = function (bounds){ |
|||
var rng; |
|||
if (this._el.tagName == 'INPUT'){ |
|||
// IE 8 is very inconsistent; textareas have createTextRange but it doesn't work
|
|||
rng = this._el.createTextRange(); |
|||
}else{ |
|||
rng = this._doc.body.createTextRange (); |
|||
rng.moveToElementText(this._el); |
|||
} |
|||
if (bounds){ |
|||
if (bounds[1] < 0) bounds[1] = 0; // IE tends to run elements out of bounds
|
|||
if (bounds[0] > this.length()) bounds[0] = this.length(); |
|||
if (bounds[1] < rng.text.replace(/\r/g, '').length){ // correct for IE's CrLf weirdness
|
|||
// block-display elements have an invisible, uncounted end of element marker, so we move an extra one and use the current length of the range
|
|||
rng.moveEnd ('character', -1); |
|||
rng.moveEnd ('character', bounds[1]-rng.text.replace(/\r/g, '').length); |
|||
} |
|||
if (bounds[0] > 0) rng.moveStart('character', bounds[0]); |
|||
} |
|||
return rng; |
|||
}; |
|||
IERange.prototype._nativeSelect = function (rng){ |
|||
rng.select(); |
|||
}; |
|||
IERange.prototype._nativeSelection = function (){ |
|||
// returns [start, end] for the selection constrained to be in element
|
|||
var rng = this._nativeRange(); // range of the element to constrain to
|
|||
var len = this.length(); |
|||
var sel = this._doc.selection.createRange(); |
|||
try{ |
|||
return [ |
|||
iestart(sel, rng), |
|||
ieend (sel, rng) |
|||
]; |
|||
}catch (e){ |
|||
// TODO: determine if this is still necessary, since we only call _nativeSelection if _el is active
|
|||
// IE gets upset sometimes about comparing text to input elements, but the selections cannot overlap, so make a best guess
|
|||
return (sel.parentElement().sourceIndex < this._el.sourceIndex) ? [0,0] : [len, len]; |
|||
} |
|||
}; |
|||
IERange.prototype._nativeGetText = function (rng){ |
|||
return rng.text; |
|||
}; |
|||
IERange.prototype._nativeSetText = function (text, rng){ |
|||
rng.text = text; |
|||
}; |
|||
IERange.prototype._nativeEOL = function(){ |
|||
if ('value' in this._el){ |
|||
this.text('\n'); // for input and textarea, insert it straight
|
|||
}else{ |
|||
this._nativeRange(this.bounds()).pasteHTML('\n<br/>'); |
|||
} |
|||
}; |
|||
IERange.prototype._nativeTop = function(rng){ |
|||
var startrng = this._nativeRange([0,0]); |
|||
return rng.boundingTop - startrng.boundingTop; |
|||
} |
|||
IERange.prototype._nativeWrap = function(n, rng) { |
|||
// hacky to use string manipulation but I don't see another way to do it.
|
|||
var div = document.createElement('div'); |
|||
div.appendChild(n); |
|||
// insert the existing range HTML after the first tag
|
|||
var html = div.innerHTML.replace('><', '>'+rng.htmlText+'<'); |
|||
rng.pasteHTML(html); |
|||
}; |
|||
|
|||
// IE internals
|
|||
function iestart(rng, constraint){ |
|||
// returns the position (in character) of the start of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
|||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness
|
|||
if (rng.compareEndPoints ('StartToStart', constraint) <= 0) return 0; // at or before the beginning
|
|||
if (rng.compareEndPoints ('StartToEnd', constraint) >= 0) return len; |
|||
for (var i = 0; rng.compareEndPoints ('StartToStart', constraint) > 0; ++i, rng.moveStart('character', -1)); |
|||
return i; |
|||
} |
|||
function ieend (rng, constraint){ |
|||
// returns the position (in character) of the end of rng within constraint. If it's not in constraint, returns 0 if it's before, length if it's after
|
|||
var len = constraint.text.replace(/\r/g, '').length; // correct for IE's CrLf weirdness
|
|||
if (rng.compareEndPoints ('EndToEnd', constraint) >= 0) return len; // at or after the end
|
|||
if (rng.compareEndPoints ('EndToStart', constraint) <= 0) return 0; |
|||
for (var i = 0; rng.compareEndPoints ('EndToStart', constraint) > 0; ++i, rng.moveEnd('character', -1)); |
|||
return i; |
|||
} |
|||
|
|||
// an input element in a standards document. "Native Range" is just the bounds array
|
|||
function InputRange(){} |
|||
InputRange.prototype = new Range(); |
|||
InputRange.prototype._nativeRange = function(bounds) { |
|||
return bounds || [0, this.length()]; |
|||
}; |
|||
InputRange.prototype._nativeSelect = function (rng){ |
|||
this._el.setSelectionRange(rng[0], rng[1]); |
|||
}; |
|||
InputRange.prototype._nativeSelection = function(){ |
|||
return [this._el.selectionStart, this._el.selectionEnd]; |
|||
}; |
|||
InputRange.prototype._nativeGetText = function(rng){ |
|||
return this._el.value.substring(rng[0], rng[1]); |
|||
}; |
|||
InputRange.prototype._nativeSetText = function(text, rng){ |
|||
var val = this._el.value; |
|||
this._el.value = val.substring(0, rng[0]) + text + val.substring(rng[1]); |
|||
}; |
|||
InputRange.prototype._nativeEOL = function(){ |
|||
this.text('\n'); |
|||
}; |
|||
InputRange.prototype._nativeTop = function(rng){ |
|||
// I can't remember where I found this clever hack to find the location of text in a text area
|
|||
var clone = this._el.cloneNode(true); |
|||
clone.style.visibility = 'hidden'; |
|||
clone.style.position = 'absolute'; |
|||
this._el.parentNode.insertBefore(clone, this._el); |
|||
clone.style.height = '1px'; |
|||
clone.value = this._el.value.slice(0, rng[0]); |
|||
var top = clone.scrollHeight; |
|||
// this gives the bottom of the text, so we have to subtract the height of a single line
|
|||
clone.value = 'X'; |
|||
top -= clone.scrollHeight; |
|||
clone.parentNode.removeChild(clone); |
|||
return top; |
|||
} |
|||
InputRange.prototype._nativeWrap = function() {throw new Error("Cannot wrap in a text element")}; |
|||
|
|||
function W3CRange(){} |
|||
W3CRange.prototype = new Range(); |
|||
W3CRange.prototype._nativeRange = function (bounds){ |
|||
var rng = this._doc.createRange(); |
|||
rng.selectNodeContents(this._el); |
|||
if (bounds){ |
|||
w3cmoveBoundary (rng, bounds[0], true, this._el); |
|||
rng.collapse (true); |
|||
w3cmoveBoundary (rng, bounds[1]-bounds[0], false, this._el); |
|||
} |
|||
return rng; |
|||
}; |
|||
W3CRange.prototype._nativeSelect = function (rng){ |
|||
this._win.getSelection().removeAllRanges(); |
|||
this._win.getSelection().addRange (rng); |
|||
}; |
|||
W3CRange.prototype._nativeSelection = function (){ |
|||
// returns [start, end] for the selection constrained to be in element
|
|||
var rng = this._nativeRange(); // range of the element to constrain to
|
|||
if (this._win.getSelection().rangeCount == 0) return [this.length(), this.length()]; // append to the end
|
|||
var sel = this._win.getSelection().getRangeAt(0); |
|||
return [ |
|||
w3cstart(sel, rng), |
|||
w3cend (sel, rng) |
|||
]; |
|||
} |
|||
W3CRange.prototype._nativeGetText = function (rng){ |
|||
return String.prototype.slice.apply(this._el.textContent, this.bounds()); |
|||
// return rng.toString(); // this fails in IE11 since it insists on inserting \r's before \n's in Ranges. node.textContent works as expected
|
|||
}; |
|||
W3CRange.prototype._nativeSetText = function (text, rng){ |
|||
rng.deleteContents(); |
|||
rng.insertNode (this._doc.createTextNode(text)); |
|||
if (canNormalize) this._el.normalize(); // merge the text with the surrounding text
|
|||
}; |
|||
W3CRange.prototype._nativeEOL = function(){ |
|||
var rng = this._nativeRange(this.bounds()); |
|||
rng.deleteContents(); |
|||
var br = this._doc.createElement('br'); |
|||
br.setAttribute ('_moz_dirty', ''); // for Firefox
|
|||
rng.insertNode (br); |
|||
rng.insertNode (this._doc.createTextNode('\n')); |
|||
rng.collapse (false); |
|||
}; |
|||
W3CRange.prototype._nativeTop = function(rng){ |
|||
if (this.length == 0) return 0; // no text, no scrolling
|
|||
if (rng.toString() == ''){ |
|||
var textnode = this._doc.createTextNode('X'); |
|||
rng.insertNode (textnode); |
|||
} |
|||
var startrng = this._nativeRange([0,1]); |
|||
var top = rng.getBoundingClientRect().top - startrng.getBoundingClientRect().top; |
|||
if (textnode) textnode.parentNode.removeChild(textnode); |
|||
return top; |
|||
} |
|||
W3CRange.prototype._nativeWrap = function(n, rng) { |
|||
rng.surroundContents(n); |
|||
}; |
|||
|
|||
// W3C internals
|
|||
function nextnode (node, root){ |
|||
// in-order traversal
|
|||
// we've already visited node, so get kids then siblings
|
|||
if (node.firstChild) return node.firstChild; |
|||
if (node.nextSibling) return node.nextSibling; |
|||
if (node===root) return null; |
|||
while (node.parentNode){ |
|||
// get uncles
|
|||
node = node.parentNode; |
|||
if (node == root) return null; |
|||
if (node.nextSibling) return node.nextSibling; |
|||
} |
|||
return null; |
|||
} |
|||
function w3cmoveBoundary (rng, n, bStart, el){ |
|||
// move the boundary (bStart == true ? start : end) n characters forward, up to the end of element el. Forward only!
|
|||
// if the start is moved after the end, then an exception is raised
|
|||
if (n <= 0) return; |
|||
var node = rng[bStart ? 'startContainer' : 'endContainer']; |
|||
if (node.nodeType == 3){ |
|||
// we may be starting somewhere into the text
|
|||
n += rng[bStart ? 'startOffset' : 'endOffset']; |
|||
} |
|||
while (node){ |
|||
if (node.nodeType == 3){ |
|||
var length = node.nodeValue.length; |
|||
if (n <= length){ |
|||
rng[bStart ? 'setStart' : 'setEnd'](node, n); |
|||
// special case: if we end next to a <br>, include that node.
|
|||
if (n == length){ |
|||
// skip past zero-length text nodes
|
|||
for (var next = nextnode (node, el); next && next.nodeType==3 && next.nodeValue.length == 0; next = nextnode(next, el)){ |
|||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); |
|||
} |
|||
if (next && next.nodeType == 1 && next.nodeName == "BR") rng[bStart ? 'setStartAfter' : 'setEndAfter'](next); |
|||
} |
|||
return; |
|||
}else{ |
|||
rng[bStart ? 'setStartAfter' : 'setEndAfter'](node); // skip past this one
|
|||
n -= length; // and eat these characters
|
|||
} |
|||
} |
|||
node = nextnode (node, el); |
|||
} |
|||
} |
|||
var START_TO_START = 0; // from the w3c definitions
|
|||
var START_TO_END = 1; |
|||
var END_TO_END = 2; |
|||
var END_TO_START = 3; |
|||
// from the Mozilla documentation, for range.compareBoundaryPoints(how, sourceRange)
|
|||
// -1, 0, or 1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange.
|
|||
// * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
|
|||
// * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
|
|||
// * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
|
|||
// * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range.
|
|||
function w3cstart(rng, constraint){ |
|||
if (rng.compareBoundaryPoints (START_TO_START, constraint) <= 0) return 0; // at or before the beginning
|
|||
if (rng.compareBoundaryPoints (END_TO_START, constraint) >= 0) return constraint.toString().length; |
|||
rng = rng.cloneRange(); // don't change the original
|
|||
rng.setEnd (constraint.endContainer, constraint.endOffset); // they now end at the same place
|
|||
return constraint.toString().replace(/\r/g, '').length - rng.toString().replace(/\r/g, '').length; |
|||
} |
|||
function w3cend (rng, constraint){ |
|||
if (rng.compareBoundaryPoints (END_TO_END, constraint) >= 0) return constraint.toString().length; // at or after the end
|
|||
if (rng.compareBoundaryPoints (START_TO_END, constraint) <= 0) return 0; |
|||
rng = rng.cloneRange(); // don't change the original
|
|||
rng.setStart (constraint.startContainer, constraint.startOffset); // they now start at the same place
|
|||
return rng.toString().replace(/\r/g, '').length; |
|||
} |
|||
|
|||
function NothingRange(){} |
|||
NothingRange.prototype = new Range(); |
|||
NothingRange.prototype._nativeRange = function(bounds) { |
|||
return bounds || [0,this.length()]; |
|||
}; |
|||
NothingRange.prototype._nativeSelect = function (rng){ // do nothing
|
|||
}; |
|||
NothingRange.prototype._nativeSelection = function(){ |
|||
return [0,0]; |
|||
}; |
|||
NothingRange.prototype._nativeGetText = function (rng){ |
|||
return this._el[this._textProp].substring(rng[0], rng[1]); |
|||
}; |
|||
NothingRange.prototype._nativeSetText = function (text, rng){ |
|||
var val = this._el[this._textProp]; |
|||
this._el[this._textProp] = val.substring(0, rng[0]) + text + val.substring(rng[1]); |
|||
}; |
|||
NothingRange.prototype._nativeEOL = function(){ |
|||
this.text('\n'); |
|||
}; |
|||
NothingRange.prototype._nativeTop = function(){ |
|||
return 0; |
|||
}; |
|||
NothingRange.prototype._nativeWrap = function() {throw new Error("Wrapping not implemented")}; |
|||
|
|||
|
|||
// data for elements, similar to jQuery data, but allows for monitoring with custom events
|
|||
var data = []; // to avoid attaching javascript objects to DOM elements, to avoid memory leaks
|
|||
bililiteRange.fn.data = function(){ |
|||
var index = this.element().bililiteRangeData; |
|||
if (index == undefined){ |
|||
index = this.element().bililiteRangeData = data.length; |
|||
data[index] = new Data(this); |
|||
} |
|||
return data[index]; |
|||
} |
|||
try { |
|||
Object.defineProperty({},'foo',{}); // IE8 will throw an error
|
|||
var Data = function(rng) { |
|||
// we use JSON.stringify to display the data values. To make some of those non-enumerable, we have to use properties
|
|||
Object.defineProperty(this, 'values', { |
|||
value: {} |
|||
}); |
|||
Object.defineProperty(this, 'sourceRange', { |
|||
value: rng |
|||
}); |
|||
Object.defineProperty(this, 'toJSON', { |
|||
value: function(){ |
|||
var ret = {}; |
|||
for (var i in Data.prototype) if (i in this.values) ret[i] = this.values[i]; |
|||
return ret; |
|||
} |
|||
}); |
|||
// to display all the properties (not just those changed), use JSON.stringify(state.all)
|
|||
Object.defineProperty(this, 'all', { |
|||
get: function(){ |
|||
var ret = {}; |
|||
for (var i in Data.prototype) ret[i] = this[i]; |
|||
return ret; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
Data.prototype = {}; |
|||
Object.defineProperty(Data.prototype, 'values', { |
|||
value: {} |
|||
}); |
|||
Object.defineProperty(Data.prototype, 'monitored', { |
|||
value: {} |
|||
}); |
|||
|
|||
bililiteRange.data = function (name, newdesc){ |
|||
newdesc = newdesc || {}; |
|||
var desc = Object.getOwnPropertyDescriptor(Data.prototype, name) || {}; |
|||
if ('enumerable' in newdesc) desc.enumerable = !!newdesc.enumerable; |
|||
if (!('enumerable' in desc)) desc.enumerable = true; // default
|
|||
if ('value' in newdesc) Data.prototype.values[name] = newdesc.value; |
|||
if ('monitored' in newdesc) Data.prototype.monitored[name] = newdesc.monitored; |
|||
desc.configurable = true; |
|||
desc.get = function (){ |
|||
if (name in this.values) return this.values[name]; |
|||
return Data.prototype.values[name]; |
|||
}; |
|||
desc.set = function (value){ |
|||
this.values[name] = value; |
|||
if (Data.prototype.monitored[name]) this.sourceRange.dispatch({ |
|||
type: 'bililiteRangeData', |
|||
bubbles: true, |
|||
detail: {name: name, value: value} |
|||
}); |
|||
} |
|||
Object.defineProperty(Data.prototype, name, desc); |
|||
} |
|||
}catch(err){ |
|||
// if we can't set object property properties, just use old-fashioned properties
|
|||
Data = function(rng){ this.sourceRange = rng }; |
|||
Data.prototype = {}; |
|||
bililiteRange.data = function(name, newdesc){ |
|||
if ('value' in newdesc) Data.prototype[name] = newdesc.value; |
|||
} |
|||
} |
|||
|
|||
})(); |
|||
|
|||
// Polyfill for forEach, per Mozilla documentation. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach#Polyfill
|
|||
if (!Array.prototype.forEach) |
|||
{ |
|||
Array.prototype.forEach = function(fun /*, thisArg */) |
|||
{ |
|||
"use strict"; |
|||
|
|||
if (this === void 0 || this === null) |
|||
throw new TypeError(); |
|||
|
|||
var t = Object(this); |
|||
var len = t.length >>> 0; |
|||
if (typeof fun !== "function") |
|||
throw new TypeError(); |
|||
|
|||
var thisArg = arguments.length >= 2 ? arguments[1] : void 0; |
|||
for (var i = 0; i < len; i++) |
|||
{ |
|||
if (i in t) |
|||
fun.call(thisArg, t[i], i, t); |
|||
} |
|||
}; |
|||
} |
@ -1,183 +0,0 @@ |
|||
/*! |
|||
* jquery-drawer v3.2.2 |
|||
* 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) |
|||
// XXX: local patch waiting for:
|
|||
// https://github.com/blivesta/drawer/pull/36
|
|||
//.css({ 'overflow': 'hidden' })
|
|||
// end local patch
|
|||
.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) |
|||
// XXX: local patch waiting for:
|
|||
// https://github.com/blivesta/drawer/pull/36
|
|||
//.css("overflow", "auto")
|
|||
// end local patch
|
|||
.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); |
|||
} |
|||
}; |
|||
|
|||
})); |
2197
web_responsive/static/lib/js/iscroll-probe.5.2.0.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,57 +0,0 @@ |
|||
// insert characters in a textarea or text input field
|
|||
// special characters are enclosed in {}; use {{} for the { character itself
|
|||
// documentation: http://bililite.com/blog/2008/08/20/the-fnsendkeys-plugin/
|
|||
// source: https://github.com/dwachss/bililiteRange/blob/master/jquery.sendkeys.js
|
|||
// Version: 4
|
|||
// Copyright (c) 2013 Daniel Wachsstock
|
|||
// MIT license:
|
|||
// Permission is hereby granted, free of charge, to any person
|
|||
// obtaining a copy of this software and associated documentation
|
|||
// files (the "Software"), to deal in the Software without
|
|||
// restriction, including without limitation the rights to use,
|
|||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|||
// copies of the Software, and to permit persons to whom the
|
|||
// Software is furnished to do so, subject to the following
|
|||
// conditions:
|
|||
|
|||
// The above copyright notice and this permission notice shall be
|
|||
// included in all copies or substantial portions of the Software.
|
|||
|
|||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|||
// OTHER DEALINGS IN THE SOFTWARE.
|
|||
|
|||
(function($){ |
|||
|
|||
$.fn.sendkeys = function (x){ |
|||
x = x.replace(/([^{])\n/g, '$1{enter}'); // turn line feeds into explicit break insertions, but not if escaped
|
|||
return this.each( function(){ |
|||
bililiteRange(this).bounds('selection').sendkeys(x).select(); |
|||
this.focus(); |
|||
}); |
|||
}; // sendkeys
|
|||
|
|||
// add a default handler for keydowns so that we can send keystrokes, even though code-generated events
|
|||
// are untrusted (http://www.w3.org/TR/DOM-Level-3-Events/#trusted-events)
|
|||
// documentation of special event handlers is at http://learn.jquery.com/events/event-extensions/
|
|||
$.event.special.keydown = $.event.special.keydown || {}; |
|||
$.event.special.keydown._default = function (evt){ |
|||
if (evt.isTrusted) return false; |
|||
if (evt.ctrlKey || evt.altKey || evt.metaKey) return false; // only deal with printable characters. This may be a false assumption
|
|||
if (evt.key == null) return false; // nothing to print. Use the keymap plugin to set this
|
|||
var target = evt.target; |
|||
if (target.isContentEditable || target.nodeName == 'INPUT' || target.nodeName == 'TEXTAREA') { |
|||
// only insert into editable elements
|
|||
var key = evt.key; |
|||
if (key.length > 1 && key.charAt(0) != '{') key = '{'+key+'}'; // sendkeys notation
|
|||
$(target).sendkeys(key); |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
})(jQuery) |
@ -0,0 +1,427 @@ |
|||
/* Copyright 2018 Tecnativa - Jairo Llopis |
|||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ |
|||
|
|||
@mixin full-screen-dropdown { |
|||
border: none; |
|||
box-shadow: none; |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: calc(100vh - #{$o-navbar-height}); |
|||
max-height: calc(100vh - #{$o-navbar-height}); |
|||
position: fixed; |
|||
width: 100vw; |
|||
z-index: 100; |
|||
// Inline style will override our `top`, so we need !important here |
|||
top: $o-navbar-height !important; |
|||
transform: none !important; |
|||
} |
|||
|
|||
// Main navbar (with apps menu, user menu, debug menu...) |
|||
@include media-breakpoint-down(sm) { |
|||
.o_main_navbar { |
|||
// This allows to have a sane layout for mobiles |
|||
display: flex; |
|||
|
|||
// Remove clutter to only have small icons that fit in a small screen |
|||
> .dropdown { |
|||
display: flex; |
|||
|
|||
.navbar-toggler { |
|||
color: white; |
|||
} |
|||
|
|||
.o_menu_sections, |
|||
.o_menu_systray, |
|||
{ |
|||
padding: 0; |
|||
} |
|||
} |
|||
|
|||
// Whitespace before systray icons |
|||
.o_menu_systray { |
|||
margin-left: auto; |
|||
} |
|||
|
|||
// Hide big things |
|||
.o_menu_brand, |
|||
.o_menu_sections, |
|||
.oe_topbar_name, |
|||
{ |
|||
display: none; |
|||
} |
|||
|
|||
// Fix toggler button padding |
|||
.o-menu-toggle { |
|||
cursor: pointer; |
|||
padding: 0 $o-horizontal-padding; |
|||
} |
|||
|
|||
// Custom fullscreen layout when showing submenus |
|||
.o_menu_sections.show { |
|||
@include full-screen-dropdown(); |
|||
background-color: $dropdown-bg; |
|||
|
|||
.show { |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
.dropdown-menu { |
|||
margin-left: 1rem; |
|||
min-width: auto; |
|||
width: calc(100vw - 2rem); |
|||
} |
|||
} |
|||
|
|||
> li, |
|||
.o_menu_entry_lvl_1, |
|||
.o_menu_header_lvl_1, |
|||
{ |
|||
// Homogeneous background color |
|||
background-color: $dropdown-bg; |
|||
color: $dropdown-link-color; |
|||
|
|||
&:hover { |
|||
background-color: $dropdown-link-hover-bg; |
|||
color: $dropdown-link-hover-color; |
|||
} |
|||
|
|||
// Disable the .o-no-caret class effect, to display the caret |
|||
&.o-no-caret::after { |
|||
content: ""; |
|||
} |
|||
|
|||
// Fix a strange glitch leaving headers invisible |
|||
.dropdown-header { |
|||
color: $dropdown-header-color; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Custom fullscreen layout for systray items |
|||
.o_mail_systray_dropdown.show { |
|||
@include full-screen-dropdown(); |
|||
|
|||
// Fix stretchy images |
|||
.o_mail_preview_image { |
|||
align-items: center; |
|||
display: flex; |
|||
flex-direction: row; |
|||
|
|||
img { |
|||
display: block; |
|||
height: auto; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Higher height for dropdown items, for those with sausage fingers |
|||
.dropdown-menu .dropdown-item { |
|||
padding: { |
|||
bottom: 0.5rem; |
|||
top: 0.5rem; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Iconized full screen apps menu |
|||
.o_menu_apps { |
|||
|
|||
.search-input:focus { |
|||
border-color: $o-brand-primary; |
|||
} |
|||
|
|||
.dropdown-menu.show { |
|||
@include full-screen-dropdown(); |
|||
|
|||
// Display apps in a grid |
|||
align-content: flex-start; |
|||
display: flex; |
|||
flex-direction: row; |
|||
flex-wrap: wrap; |
|||
justify-content: flex-start; |
|||
|
|||
@include media-breakpoint-up(lg) { |
|||
padding: { |
|||
left: 20vw; |
|||
right: 20vw; |
|||
} |
|||
} |
|||
|
|||
.o_app { |
|||
align-items: center; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
|
|||
// Size depends on screen |
|||
width: 33.33333333%; |
|||
@include media-breakpoint-up(sm) { |
|||
width: 25%; |
|||
} |
|||
@include media-breakpoint-up(md) { |
|||
width: 16.6666666%; |
|||
} |
|||
} |
|||
|
|||
// Hide app icons when searching |
|||
.has-results ~ .o_app { |
|||
display: none; |
|||
} |
|||
|
|||
.o-app-icon { |
|||
height: auto; |
|||
max-width: 7rem; |
|||
width: 100%; |
|||
} |
|||
|
|||
// Search input for menus |
|||
.form-row { |
|||
width: 100%; |
|||
} |
|||
|
|||
.o-menu-search-result { |
|||
align-items: center; |
|||
background-position: left; |
|||
background-repeat: no-repeat; |
|||
background-size: contain; |
|||
cursor: pointer; |
|||
display: flex; |
|||
padding-left: 3rem; |
|||
white-space: normal; |
|||
} |
|||
|
|||
// Allow to scroll only on results, keeping static search box above |
|||
.search-container.has-results { |
|||
height: 100%; |
|||
|
|||
.search-input { |
|||
height: 3em; |
|||
} |
|||
|
|||
.search-results { |
|||
height: calc(100% - 3em); |
|||
overflow: auto; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Scroll all but top bar |
|||
html .o_web_client .o_main .o_main_content { |
|||
@include media-breakpoint-down(sm) { |
|||
overflow: auto; |
|||
|
|||
.o_content { |
|||
overflow: initial; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Control panel (breadcrumbs, search box, buttons...) |
|||
@include media-breakpoint-down(sm) { |
|||
.o_control_panel { |
|||
// Arrange buttons to use space better |
|||
.breadcrumb, |
|||
.o_cp_buttons, |
|||
.o_cp_left, |
|||
.o_cp_right, |
|||
.o_cp_searchview, |
|||
{ |
|||
flex: 1 1 100%; |
|||
@include media-breakpoint-up(md) { |
|||
flex-basis: 50%; |
|||
} |
|||
} |
|||
|
|||
.breadcrumb { |
|||
flex-basis: 80%; |
|||
} |
|||
|
|||
.o_cp_searchview, |
|||
.o_cp_right, |
|||
{ |
|||
flex-basis: 10%; |
|||
} |
|||
|
|||
.o_cp_left { |
|||
flex-basis: 50%; |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
.o_cp_pager { |
|||
white-space: nowrap; |
|||
} |
|||
|
|||
// Hide all but 2 last breadcrumbs, and render 2nd-to-last as arrow |
|||
.breadcrumb-item { |
|||
&:not(.active) { |
|||
padding-left: 0; |
|||
} |
|||
|
|||
&::before { |
|||
content: none; |
|||
padding-right: 0; |
|||
} |
|||
|
|||
&:nth-last-of-type(1n+3) { |
|||
display: none; |
|||
} |
|||
|
|||
&:nth-last-of-type(2) { |
|||
&::before { |
|||
color: var(--primary); |
|||
content: "\f048"; // .fa-step-backward |
|||
cursor: pointer; |
|||
font-family: FontAwesome; |
|||
} |
|||
|
|||
a { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Ellipsize long breadcrumbs |
|||
.breadcrumb { |
|||
max-width: 100%; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
// Empty sidebar should not break layout |
|||
.o_cp_sidebar:blank { |
|||
display: none; |
|||
} |
|||
|
|||
// In case you install `mail`, there is a mess on BS vs inline styles |
|||
// we need to fix |
|||
.o_cp_buttons .btn.d-block:not(.d-none) { |
|||
display: inline-block !important; |
|||
} |
|||
|
|||
// Dropdown with buttons to switch the view type |
|||
.o_cp_switch_buttons.show { |
|||
.dropdown-menu { |
|||
align-content: center; |
|||
display: flex; |
|||
flex-direction: row; |
|||
justify-content: space-around; |
|||
padding: 0; |
|||
|
|||
.btn { |
|||
border: { |
|||
bottom: 0; |
|||
radius: 0; |
|||
top: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Normal views |
|||
.o_content, .modal-content { |
|||
max-width: 100%; |
|||
|
|||
// Form views |
|||
.o_form_view { |
|||
.o_form_sheet { |
|||
max-width: calc(100% - 32px); |
|||
} |
|||
|
|||
@include media-breakpoint-down(sm) { |
|||
min-width: auto; |
|||
|
|||
// Avoid overflow on forms with title and/or button box |
|||
.oe_button_box, |
|||
.oe_title, |
|||
{ |
|||
max-width: 100%; |
|||
} |
|||
|
|||
// Avoid overflow on modals |
|||
.o_form_sheet { |
|||
min-width: auto; |
|||
} |
|||
|
|||
// Render website inputs properly in phones |
|||
.o_group .o_field_widget.o_text_overflow { |
|||
// Overrides another !important |
|||
width: auto !important; |
|||
} |
|||
|
|||
// Make all input groups vertical |
|||
.o_group_col_6 { |
|||
width: 100%; |
|||
} |
|||
|
|||
// Statusbar buttons dropdown for mobiles |
|||
.o_statusbar_buttons_dropdown { |
|||
border: { |
|||
bottom: 0; |
|||
radius: 0; |
|||
top: 0; |
|||
} |
|||
height: 100%; |
|||
} |
|||
.o_statusbar_buttons > .btn { |
|||
border-radius: 0; |
|||
border: 0; |
|||
width: 100%; |
|||
margin-bottom: 0.2rem; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.o_statusbar_status { |
|||
// Arrow from rightmost button exceeds allowed width |
|||
.o_arrow_button:first-child::before { |
|||
content: none; |
|||
display: none; |
|||
} |
|||
} |
|||
|
|||
// Full width in form sheets |
|||
.o_form_sheet, |
|||
.oe_chatter, |
|||
{ |
|||
min-width: auto; |
|||
max-width: 98%; |
|||
} |
|||
|
|||
// Settings pages |
|||
.app_settings_block { |
|||
.row { |
|||
margin: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Sided chatter, if user wants |
|||
.o_chatter_position_sided & { |
|||
@include media-breakpoint-up(md) { |
|||
.o_form_view:not(.o_form_nosheet) { |
|||
display: flex; |
|||
flex-flow: row nowrap; |
|||
height: 100%; |
|||
|
|||
.o_form_sheet_bg { |
|||
flex: 1 1 60%; |
|||
overflow: auto; |
|||
} |
|||
|
|||
.o_chatter { |
|||
border-left: 1px solid gray('400'); |
|||
flex: 1 1 40%; |
|||
max-width: 50%; |
|||
min-width: 30%; |
|||
overflow: auto; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -1,532 +1,409 @@ |
|||
/* Copyright 2016 LasLabs Inc. |
|||
/* Copyright 2018 Tecnativa - Jairo Llopis |
|||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
|||
|
|||
odoo.define('web_responsive', function(require) { |
|||
odoo.define('web_responsive', function (require) { |
|||
'use strict'; |
|||
|
|||
var Menu = require('web.Menu'); |
|||
var rpc = require('web.rpc'); |
|||
var SearchView = require('web.SearchView'); |
|||
var core = require('web.core'); |
|||
var config = require('web.config'); |
|||
var session = require('web.session'); |
|||
var ViewManager = require('web.ViewManager'); |
|||
var RelationalFields = require('web.relational_fields'); |
|||
var AbstractWebClient = require("web.AbstractWebClient"); |
|||
var AppsMenu = require("web.AppsMenu"); |
|||
var config = require("web.config"); |
|||
var core = require("web.core"); |
|||
var FormRenderer = require('web.FormRenderer'); |
|||
var Widget = require('web.Widget'); |
|||
|
|||
var qweb = core.qweb; |
|||
|
|||
Menu.include({ |
|||
|
|||
// Force all_outside to prevent app icons from going into more menu
|
|||
reflow: function() { |
|||
this._super('all_outside'); |
|||
}, |
|||
var Menu = require("web.Menu"); |
|||
var RelationalFields = require('web.relational_fields'); |
|||
|
|||
/* 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', ''); |
|||
/** |
|||
* Reduce menu data to a searchable format understandable by fuzzy.js |
|||
* |
|||
* `AppsMenu.init()` gets `menuData` in a format similar to this (only |
|||
* relevant data is shown): |
|||
* |
|||
* ```js
|
|||
* { |
|||
* [...], |
|||
* children: [ |
|||
* // This is a menu entry:
|
|||
* { |
|||
* action: "ir.actions.client,94", // Or `false`
|
|||
* children: [... similar to above "children" key], |
|||
* name: "Actions", |
|||
* parent_id: [146, "Settings/Technical/Actions"], // Or `false`
|
|||
* }, |
|||
* ... |
|||
* ] |
|||
* } |
|||
* ```
|
|||
* |
|||
* This format is very hard to process to search matches, and it would |
|||
* slow down the search algorithm, so we reduce it with this method to be |
|||
* able to later implement a simpler search. |
|||
* |
|||
* @param {Object} memo |
|||
* Reference to current result object, passed on recursive calls. |
|||
* |
|||
* @param {Object} menu |
|||
* A menu entry, as described above. |
|||
* |
|||
* @returns {Object} |
|||
* Reduced object, without entries that have no action, and with a |
|||
* format like this: |
|||
* |
|||
* ```js
|
|||
* { |
|||
* "Discuss": {Menu entry Object}, |
|||
* "Settings": {Menu entry Object}, |
|||
* "Settings/Technical/Actions/Actions": {Menu entry Object}, |
|||
* ... |
|||
* } |
|||
* ```
|
|||
*/ |
|||
function findNames (memo, menu) { |
|||
if (menu.action) { |
|||
var key = menu.parent_id ? menu.parent_id[1] + "/" : ""; |
|||
memo[key + menu.name] = menu; |
|||
} |
|||
|
|||
}); |
|||
|
|||
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; |
|||
} |
|||
if (menu.children.length) { |
|||
_.reduce(menu.children, findNames, memo); |
|||
} |
|||
}); |
|||
return memo; |
|||
} |
|||
|
|||
var AppDrawer = Widget.extend({ |
|||
AppsMenu.include({ |
|||
events: _.extend({ |
|||
"keydown .search-input input": "_searchResultsNavigate", |
|||
"click .o-menu-search-result": "_searchResultChosen", |
|||
"shown.bs.dropdown": "_searchFocus", |
|||
"hidden.bs.dropdown": "_searchReset", |
|||
}, AppsMenu.prototype.events), |
|||
|
|||
/* Provides all features inside of the application drawer navigation. |
|||
|
|||
Attributes: |
|||
directionCodes (str): Canonical key name to direction mappings. |
|||
deleteCodes |
|||
/** |
|||
* Rescue some menu data stripped out in original method. |
|||
* |
|||
* @override |
|||
*/ |
|||
|
|||
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() { |
|||
init: function (parent, menuData) { |
|||
this._super.apply(this, arguments); |
|||
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) |
|||
); |
|||
this.filter_timeout = $.Deferred(); |
|||
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 |
|||
// Keep base64 icon for main menus
|
|||
for (var n in this._apps) { |
|||
this._apps[n].web_icon_data = |
|||
menuData.children[n].web_icon_data; |
|||
} |
|||
// Store menu data in a format searchable by fuzzy.js
|
|||
this._searchableMenus = _.reduce( |
|||
menuData.children, |
|||
findNames, |
|||
{} |
|||
); |
|||
// Search only after timeout, for fast typers
|
|||
this._search_def = $.Deferred(); |
|||
}, |
|||
|
|||
/* 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``. |
|||
/** |
|||
* @override |
|||
*/ |
|||
handleKeyDown: function(e) { |
|||
if (!this.isOpen){ |
|||
return; |
|||
} |
|||
var directionCode = $.hotkeys.specialKeys[e.keyCode.toString()]; |
|||
if (Object.keys(this.directionCodes).indexOf(directionCode) !== -1) { |
|||
var $link = false; |
|||
if (this.searching) { |
|||
var $collection = this.$el.find('#appDrawerMenuSearch a'); |
|||
$link = this.findAdjacentLink( |
|||
this.$el.find('#appDrawerMenuSearch a:first, #appDrawerMenuSearch a.web-responsive-focus').last(), |
|||
this.directionCodes[directionCode], |
|||
$collection, |
|||
true |
|||
); |
|||
} else { |
|||
$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); |
|||
} |
|||
start: function () { |
|||
this.$search_container = this.$(".search-container"); |
|||
this.$search_input = this.$(".search-input input"); |
|||
this.$search_results = this.$(".search-results"); |
|||
return this._super.apply(this, arguments); |
|||
}, |
|||
|
|||
/* Provide centralized key event redirects for the App Drawer. |
|||
/** |
|||
* Get all info for a given menu. |
|||
* |
|||
* This method is for all key events not related to arrow navigation. |
|||
* @param {String} key |
|||
* Full path to requested menu. |
|||
* |
|||
* @param e The key event that was triggered by ``core.bus``. |
|||
* @returns {Object} |
|||
* Menu definition, plus extra needed keys. |
|||
*/ |
|||
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') { |
|||
var href = $('.web-responsive-focus').attr('href'); |
|||
if (!_.isUndefined(href)) { |
|||
window.location.href = 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(); |
|||
|
|||
_menuInfo: function (key) { |
|||
var original = this._searchableMenus[key]; |
|||
return _.extend({ |
|||
action_id: parseInt(original.action.split(',')[1], 10), |
|||
}, original); |
|||
}, |
|||
|
|||
/* Performs close actions |
|||
* @fires ``drawer.closed`` to the ``core.bus`` |
|||
* @listens ``drawer.opened`` and sends to onDrawerOpen |
|||
/** |
|||
* Autofocus on search field on big screens. |
|||
*/ |
|||
onDrawerClose: function() { |
|||
core.bus.trigger('drawer.closed'); |
|||
this.closeSearchMenus(); |
|||
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.closeSearchMenus(); |
|||
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; |
|||
this.$searchInput.val(""); |
|||
}, |
|||
|
|||
// Selects a link visibly & deselects others.
|
|||
selectLink: function($link) { |
|||
$('.web-responsive-focus').removeClass('web-responsive-focus'); |
|||
if ($link) { |
|||
$link.addClass('web-responsive-focus'); |
|||
_searchFocus: function () { |
|||
if (!config.device.isMobile) { |
|||
this.$search_input.focus(); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* Search matching menus immediately |
|||
* Reset search input and results |
|||
*/ |
|||
_searchMenus: function () { |
|||
rpc.query({ |
|||
model: 'ir.ui.menu', |
|||
method: 'search_read', |
|||
kwargs: { |
|||
fields: ['action', 'display_name', 'id'], |
|||
domain: [ |
|||
['name', 'ilike', this.$searchInput.val()], |
|||
['action', '!=', false], |
|||
], |
|||
context: session.user_context, |
|||
}, |
|||
}).then(this.showFoundMenus.bind(this)); |
|||
_searchReset: function () { |
|||
this.$search_container.removeClass("has-results"); |
|||
this.$search_results.empty(); |
|||
this.$search_input.val(""); |
|||
}, |
|||
|
|||
/** |
|||
* Queue the next menu search for the search input |
|||
* Schedule a search on current menu items. |
|||
*/ |
|||
searchMenus: function() { |
|||
// Stop current search, if any
|
|||
this.filter_timeout.reject(); |
|||
this.filter_timeout = $.Deferred(); |
|||
// Schedule a new search
|
|||
this.filter_timeout.done(this._searchMenus.bind(this)); |
|||
setTimeout( |
|||
this.filter_timeout.resolve.bind(this.filter_timeout), |
|||
200 |
|||
); |
|||
// Focus search input
|
|||
this.$searchInput = $('#appDrawerSearchInput').focus(); |
|||
_searchMenusSchedule: function () { |
|||
this._search_def.reject(); |
|||
this._search_def = $.Deferred(); |
|||
setTimeout(this._search_def.resolve.bind(this._search_def), 50); |
|||
this._search_def.done(this._searchMenus.bind(this)); |
|||
}, |
|||
|
|||
/* Display the menus that are provided as input. |
|||
/** |
|||
* Search among available menu items, and render that search. |
|||
*/ |
|||
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} |
|||
) |
|||
_searchMenus: function () { |
|||
var query = this.$search_input.val(); |
|||
if (query === "") { |
|||
this.$search_container.removeClass("has-results"); |
|||
this.$search_results.empty(); |
|||
return; |
|||
} |
|||
var results = fuzzy.filter( |
|||
query, |
|||
_.keys(this._searchableMenus), |
|||
{ |
|||
pre: "<b>", |
|||
post: "</b>", |
|||
} |
|||
); |
|||
this.$search_container.toggleClass( |
|||
"has-results", |
|||
Boolean(results.length) |
|||
); |
|||
this.$search_results.html( |
|||
core.qweb.render( |
|||
"web_responsive.MenuSearchResults", |
|||
{ |
|||
results: results, |
|||
widget: this, |
|||
} |
|||
) |
|||
// 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. |
|||
/** |
|||
* Use chooses a search result, so we navigate to that menu |
|||
* |
|||
* @param {jQuery.Event} event |
|||
*/ |
|||
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(); |
|||
_searchResultChosen: function (event) { |
|||
event.preventDefault(); |
|||
var $result = $(event.currentTarget), |
|||
text = $result.text().trim(), |
|||
data = $result.data(), |
|||
suffix = ~text.indexOf("/") ? "/" : ""; |
|||
// Load the menu view
|
|||
this.trigger_up("menu_clicked", { |
|||
action_id: data.actionId, |
|||
id: data.menuId, |
|||
previous_menu_id: data.parentId, |
|||
}); |
|||
// Find app that owns the chosen menu
|
|||
var app = _.find(this._apps, function (_app) { |
|||
return text.indexOf(_app.name + suffix) === 0; |
|||
}); |
|||
// Update navbar menus
|
|||
core.bus.trigger("change_menu_section", app.menuID); |
|||
}, |
|||
|
|||
/* 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 |
|||
/** |
|||
* Navigate among search results |
|||
* |
|||
* @param {jQuery.Event} event |
|||
*/ |
|||
findAdjacentLink: function($link, direction, $objs, restrictHorizontal) { |
|||
|
|||
if (_.isUndefined($objs)) { |
|||
$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; |
|||
_searchResultsNavigate: function (event) { |
|||
// Exit soon when not navigating results
|
|||
if (this.$search_results.is(":empty")) { |
|||
// Just in case it is the 1st search
|
|||
this._searchMenusSchedule(); |
|||
return; |
|||
} |
|||
|
|||
if (obj.length) { |
|||
// Find current results and active element (1st by default)
|
|||
var all = this.$search_results.find(".o-menu-search-result"), |
|||
pre_focused = all.filter(".active") || $(all[0]), |
|||
offset = all.index(pre_focused), |
|||
key = event.key; |
|||
// Transform tab presses in arrow presses
|
|||
if (key === "Tab") { |
|||
event.preventDefault(); |
|||
key = event.shiftKey ? "ArrowUp" : "ArrowDown"; |
|||
} |
|||
switch (key) { |
|||
// Pressing enter is the same as clicking on the active element
|
|||
case "Enter": |
|||
pre_focused.click(); |
|||
break; |
|||
// Navigate up or down
|
|||
case "ArrowUp": |
|||
offset--; |
|||
break; |
|||
case "ArrowDown": |
|||
offset++; |
|||
break; |
|||
// Other keys trigger a search
|
|||
default: |
|||
this._searchMenusSchedule(); |
|||
return; |
|||
} |
|||
// Allow looping on results
|
|||
if (offset < 0) { |
|||
offset = all.length + offset; |
|||
} else if (offset >= all.length) { |
|||
offset -= all.length; |
|||
} |
|||
// Switch active element
|
|||
var new_focused = $(all[offset]); |
|||
pre_focused.removeClass("active"); |
|||
new_focused.addClass("active"); |
|||
this.$search_results.scrollTo(new_focused, { |
|||
offset: { |
|||
top: this.$search_results.height() * -0.5, |
|||
}, |
|||
}); |
|||
}, |
|||
}); |
|||
|
|||
return $(obj); |
|||
|
|||
Menu.include({ |
|||
events: _.extend({ |
|||
// Clicking a hamburger menu item should close the hamburger
|
|||
"click .o_menu_sections [role=menuitem]": "_hideMobileSubmenus", |
|||
// Opening any dropdown in the navbar should hide the hamburger
|
|||
"show.bs.dropdown .o_menu_systray, .o_menu_apps": |
|||
"_hideMobileSubmenus", |
|||
}, Menu.prototype.events), |
|||
|
|||
start: function () { |
|||
this.$menu_toggle = this.$(".o-menu-toggle"); |
|||
return this._super.apply(this, arguments); |
|||
}, |
|||
|
|||
/* 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 |
|||
/** |
|||
* Hide menus for current app if you're in mobile |
|||
*/ |
|||
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; |
|||
}; |
|||
_hideMobileSubmenus: function () { |
|||
if ( |
|||
this.$menu_toggle.is(":visible") && |
|||
this.$section_placeholder.is(":visible") |
|||
) { |
|||
this.$section_placeholder.collapse("hide"); |
|||
} |
|||
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'; |
|||
/** |
|||
* No menu brand in mobiles |
|||
* |
|||
* @override |
|||
*/ |
|||
_updateMenuBrand: function () { |
|||
if (!config.device.isMobile) { |
|||
return this._super.apply(this, arguments); |
|||
} |
|||
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.
|
|||
/** |
|||
* Fold all on mobiles. |
|||
* |
|||
* @override |
|||
*/ |
|||
_setState: function () { |
|||
this._super.apply(this, arguments); |
|||
this.$el.html(qweb.render("FieldStatus.content", this._renderQWebValues())); |
|||
} |
|||
if (config.device.isMobile) { |
|||
_.map(this.status_information, function (value) { |
|||
value.fold = true; |
|||
}); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
// 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))); |
|||
} |
|||
}); |
|||
/** |
|||
* In mobiles, put all statusbar buttons in a dropdown. |
|||
* |
|||
* @override |
|||
*/ |
|||
_renderHeaderButtons: function () { |
|||
var $buttons = this._super.apply(this, arguments); |
|||
if ( |
|||
!config.device.isMobile || |
|||
!$buttons.is(":has(>:not(.o_invisible_modifier))") |
|||
) { |
|||
return $buttons; |
|||
} |
|||
|
|||
return $container; |
|||
} |
|||
// $buttons must be appended by JS because all events are bound
|
|||
$buttons.addClass("dropdown-menu"); |
|||
var $dropdown = $(core.qweb.render( |
|||
'web_responsive.MenuStatusbarButtons' |
|||
)); |
|||
$buttons.addClass("dropdown-menu").appendTo($dropdown); |
|||
return $dropdown; |
|||
}, |
|||
}); |
|||
|
|||
/** |
|||
* Use ALT+SHIFT instead of ALT as hotkey triggerer. |
|||
* |
|||
* HACK https://github.com/odoo/odoo/issues/30068 - See it to know why.
|
|||
* |
|||
* Cannot patch in `KeyboardNavigationMixin` directly because it's a mixin, |
|||
* not a `Class`, and altering a mixin's `prototype` doesn't alter it where |
|||
* it has already been used. |
|||
* |
|||
* Instead, we provide an additional mixin to be used wherever you need to |
|||
* enable this behavior. |
|||
*/ |
|||
var KeyboardNavigationShiftAltMixin = { |
|||
|
|||
/** |
|||
* Alter the key event to require pressing Shift. |
|||
* |
|||
* This will produce a mocked event object where it will seem that |
|||
* `Alt` is not pressed if `Shift` is not pressed. |
|||
* |
|||
* The reason for this is that original upstream code, found in |
|||
* `KeyboardNavigationMixin` is very hardcoded against the `Alt` key, |
|||
* so it is more maintainable to mock its input than to rewrite it |
|||
* completely. |
|||
* |
|||
* @param {keyEvent} keyEvent |
|||
* Original event object |
|||
* |
|||
* @returns {keyEvent} |
|||
* Altered event object |
|||
*/ |
|||
_shiftPressed: function (keyEvent) { |
|||
var alt = keyEvent.altKey || keyEvent.key === "Alt", |
|||
newEvent = _.extend({}, keyEvent), |
|||
shift = keyEvent.shiftKey || keyEvent.key === "Shift"; |
|||
// Mock event to make it seem like Alt is not pressed
|
|||
if (alt && !shift) { |
|||
newEvent.altKey = false; |
|||
if (newEvent.key === "Alt") { |
|||
newEvent.key = "Shift"; |
|||
} |
|||
} |
|||
return newEvent; |
|||
}, |
|||
|
|||
_onKeyDown: function (keyDownEvent) { |
|||
return this._super(this._shiftPressed(keyDownEvent)); |
|||
}, |
|||
|
|||
return { |
|||
'AppDrawer': AppDrawer, |
|||
_onKeyUp: function (keyUpEvent) { |
|||
return this._super(this._shiftPressed(keyUpEvent)); |
|||
}, |
|||
}; |
|||
|
|||
// Include the SHIFT+ALT mixin wherever
|
|||
// `KeyboardNavigationMixin` is used upstream
|
|||
AbstractWebClient.include(KeyboardNavigationShiftAltMixin); |
|||
}); |
@ -1,129 +0,0 @@ |
|||
/* 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; |
|||
z-index: 10000; |
|||
|
|||
.o_tooltip { |
|||
z-index: 1051; |
|||
} |
|||
|
|||
.oe_logo { |
|||
margin-top: -11px; |
|||
position: relative; |
|||
img { |
|||
height: @app-drawer-title-height; |
|||
} |
|||
.oe_logo_edit { |
|||
position: absolute; |
|||
bottom: 0px; |
|||
width: 100%; |
|||
padding: 4px; |
|||
display: none; |
|||
color: @odoo-list-footer-bg-color; |
|||
background: rgba(37,37,37,0.9); |
|||
} |
|||
&:hover .oe_logo_edit_admin { |
|||
display: block; |
|||
} |
|||
} |
|||
|
|||
.navbar-left { |
|||
width: 100%; |
|||
|
|||
li { |
|||
padding: 0; |
|||
} |
|||
} |
|||
|
|||
.app-drawer-title { |
|||
float: none; |
|||
font-weight: bold; // Bold titles for apps in the app-drawer |
|||
} |
|||
|
|||
.app-drawer-panel-title { |
|||
line-height: 16px; |
|||
> .drawer-toggle { |
|||
padding-top: 17px; |
|||
padding-bottom: 17px; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
|
|||
.app-drawer-icon-app { |
|||
height: 100%; |
|||
width: 100%; |
|||
max-width: @app-drawer-icon-size; |
|||
max-height: @app-drawer-icon-size; |
|||
object-fit: contain; |
|||
object-position: center; |
|||
} |
|||
|
|||
.panel-body { |
|||
padding-top: @app-drawer-title-height; |
|||
} |
|||
|
|||
#appDrawerAppPanelHead { |
|||
position: absolute; |
|||
height: @app-drawer-title-height; |
|||
width: 100%; |
|||
} |
|||
|
|||
.app-drawer-search-panel { |
|||
|
|||
.panel-body { |
|||
padding-top: @padding-base-vertical; |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
|||
|
|||
.drawer-nav { |
|||
width: @app-drawer-width; |
|||
} |
|||
|
|||
.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; |
|||
} |
|||
|
|||
/* Icon Focusing */ |
|||
|
|||
.web-responsive-focus { |
|||
.tab-focus(); |
|||
} |
@ -1,212 +0,0 @@ |
|||
/* Copyright 2016 Ponto Suprimentos Ltda. |
|||
Copyright 2018 Alexandre Díaz |
|||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ |
|||
|
|||
@sheet-margin: @sheet-padding; |
|||
@chatter-side-width: 30%; |
|||
|
|||
// Sided Chatter |
|||
@media (min-width: @screen-md) { |
|||
.o_chatter_position_sided { |
|||
.o_form_view:not(.o_form_nosheet) { |
|||
display: flex; |
|||
height: 100%; |
|||
|
|||
.o_form_sheet_bg { |
|||
border-right: 1px solid @table-border-color; |
|||
overflow: auto; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.oe_chatter { |
|||
overflow: auto; |
|||
flex: 0 0 @chatter-side-width; |
|||
|
|||
.o_chatter_topbar { |
|||
height: auto; |
|||
flex-wrap: wrap; |
|||
|
|||
button:last-of-type { |
|||
flex: 1 0 auto; |
|||
text-align: left; |
|||
} |
|||
|
|||
.o_followers { |
|||
order: -10; |
|||
flex: 0 1 100%; |
|||
} |
|||
} |
|||
|
|||
&:empty { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Normal Chatter |
|||
.o_chatter_position_normal { |
|||
.oe_chatter { |
|||
max-width: initial; |
|||
} |
|||
} |
|||
|
|||
// Sticky Header & Footer in List View |
|||
.o_view_manager_content { |
|||
>div { |
|||
>.table-responsive { |
|||
>.o_list_view { |
|||
thead { |
|||
position: sticky; |
|||
top: 0; |
|||
} |
|||
tfoot { |
|||
position: sticky; |
|||
bottom: 0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.o_form_view { |
|||
// Form must fill 100% width in any size |
|||
.o_form_sheet_bg { |
|||
|
|||
.o_form_sheet { |
|||
min-width: auto; |
|||
max-width: 100%; |
|||
margin: @sheet-margin; |
|||
} |
|||
|
|||
@media (max-width: @screen-sm-max) { |
|||
padding: 0; |
|||
|
|||
.o_form_sheet { |
|||
border: none; |
|||
} |
|||
} |
|||
|
|||
.o_form_statusbar { |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 1; |
|||
|
|||
.o-status-more > li > button { |
|||
border: 0; |
|||
} |
|||
|
|||
.o_statusbar_buttons_container { |
|||
.o_statusbar_buttons_dropdown { |
|||
height: 100%; |
|||
|
|||
>#dropdownMenuHeader { |
|||
height: 100%; |
|||
border-top: 0; |
|||
border-bottom: 0; |
|||
border-radius: 0; |
|||
} |
|||
>.dropdown-menu > li > button { |
|||
width: 100%; |
|||
border-radius: 0; |
|||
border: 0; |
|||
} |
|||
} |
|||
|
|||
.o_statusbar_buttons_base > .o_statusbar_buttons { |
|||
.o-flex-flow(row, wrap); |
|||
>.btn { |
|||
@o-statusbar-buttons-vmargin: 4px; |
|||
min-height: @odoo-statusbar-height - 2 * @o-statusbar-buttons-vmargin; |
|||
margin: @o-statusbar-buttons-vmargin 3px @o-statusbar-buttons-vmargin 0; |
|||
padding-top: 2px; |
|||
padding-bottom: 2px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// No overflowing buttons or titles |
|||
.oe_button_box, .oe_title { |
|||
max-width: 100%; |
|||
} |
|||
|
|||
@media (max-width: @screen-xs) { |
|||
.o_form_field > .o_form_input_dropdown { |
|||
width: 80%; |
|||
} |
|||
} |
|||
.o_group { |
|||
&.o_inner_group > tbody > tr > td { |
|||
.note-editor > .note-toolbar { |
|||
// prevent wysiwyg editor buttons from expanding the screen |
|||
white-space: initial; |
|||
} |
|||
} |
|||
// reduce form maximum columns for smaller screens |
|||
@media (max-width: @screen-xs-max) { |
|||
.o-generate-groups(12); |
|||
.o-generate-groups(@n, @i: 1) when (@i =< @n) { |
|||
.o_group_col_@{i} { |
|||
width: 100%; |
|||
} |
|||
.o-generate-groups(@n, @i + 1); |
|||
} |
|||
} |
|||
// break field label into a separate line from field on small screens |
|||
@media (max-width: @screen-xs) { |
|||
&.o_inner_group { |
|||
display: block; |
|||
> tbody { |
|||
display: block; |
|||
> tr { |
|||
margin-top: 8px; |
|||
.o-flex-display(); |
|||
.o-flex-flow(row, wrap); |
|||
> td { |
|||
.o-flex(1, 0, auto); |
|||
padding: 0; |
|||
display: block; |
|||
padding: 0; |
|||
// odoo adds a `style="width: 100%"` by javascript |
|||
// directly on the tag so we need `!important` here: |
|||
width: auto!important; |
|||
max-width: 100%; |
|||
&.o_td_label { |
|||
border-right: 0; |
|||
// keep 6% space on line to fit checkboxes |
|||
// see above about `!important` |
|||
width: 94%!important; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Make image editing controls always available, instead of depending on resolution or hover |
|||
.o_form_field_image > .o_form_image_controls { |
|||
position: initial; |
|||
opacity: 1; |
|||
> .fa { |
|||
width: 50%; |
|||
padding: 6px; |
|||
margin: 0px; |
|||
text-align: center; |
|||
} |
|||
> .fa.o_select_file_button { |
|||
background: @odoo-brand-primary; |
|||
} |
|||
> .fa.o_clear_file_button { |
|||
background: @brand-danger; |
|||
} |
|||
} |
|||
|
|||
// Adapt chatter widget to small viewports |
|||
.oe_chatter { |
|||
min-width: inherit; |
|||
} |
|||
} |
@ -1,102 +0,0 @@ |
|||
/* Copyright 2016 LasLabs Inc. |
|||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ |
|||
|
|||
body { |
|||
width: 100%; |
|||
height: 100%; |
|||
|
|||
// Do not fix the search part, it's too big for small screens |
|||
@media (max-width: @screen-sm-max) { |
|||
overflow: inherit; |
|||
.odoo { |
|||
.oe-view-manager { |
|||
overflow: inherit; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
main { |
|||
width: 100%; |
|||
height: 100%; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.navbar { |
|||
z-index: 10; |
|||
} |
|||
|
|||
.o_cp_switch_buttons { |
|||
.active { |
|||
z-index: 10; |
|||
} |
|||
} |
|||
|
|||
.o_sub_menu { |
|||
.o_sub_menu_logo { |
|||
display: none; |
|||
} |
|||
.o_sub_menu_footer { |
|||
display: none; |
|||
} |
|||
} |
|||
|
|||
.o_tooltip.active { |
|||
z-index: 1051; |
|||
} |
|||
|
|||
.o_web_client { |
|||
>.o_main { |
|||
overflow: auto; |
|||
> .o_main_content { |
|||
overflow: initial; |
|||
> .o_content { |
|||
@media (max-width: @screen-xs-max) { |
|||
overflow: initial; |
|||
} |
|||
|
|||
@media (min-width: @screen-sm-min) { |
|||
// .o_content is the one to display horizontal scrolling in |
|||
// case of wide tables |
|||
.table-responsive { |
|||
overflow-x: visible; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
@media (max-width: @screen-sm-max) { |
|||
.o_control_panel { |
|||
// Remove z-index from CP buttons so it doesn't overlap the menu |
|||
.btn-group > .btn.active { |
|||
z-index: initial; |
|||
} |
|||
|
|||
// Better horizontal space usage for buttons |
|||
justify-content: space-between; |
|||
.o_cp_left, .o_cp_right { |
|||
width: inherit; |
|||
} |
|||
|
|||
.o_search_options > .o_dropdown { |
|||
&.hidden-xs { |
|||
// No other way to display "Group By" button :( |
|||
display: inline-block !important; |
|||
} |
|||
|
|||
// Hack to hide text and display larger icons |
|||
> .btn { |
|||
font-size: 0; |
|||
> .fa { |
|||
font-size: @odoo-font-size-base * 1.4; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.o_chat_window { |
|||
z-index: 1000; |
|||
} |
@ -1,195 +0,0 @@ |
|||
/* Copyright 2016 LasLabs Inc. |
|||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ |
|||
|
|||
header { |
|||
margin: 0; |
|||
padding: 0; |
|||
|
|||
@media print { |
|||
display: none; |
|||
} |
|||
|
|||
> .main-nav { |
|||
display: block; |
|||
white-space: nowrap; |
|||
|
|||
.navbar-systray { |
|||
white-space: nowrap; |
|||
@media (max-width: @screen-xs-max) { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 56px; |
|||
} |
|||
> .oe_user_menu_placeholder > li > a { |
|||
> .oe_topbar_avatar { |
|||
border-radius: 50%; |
|||
margin-top: -8px; |
|||
max-height: 36px; |
|||
height: 36px; |
|||
width: 36px; |
|||
} |
|||
.oe_topbar_name { |
|||
position: relative; |
|||
top: -3px; |
|||
@media (max-width: @screen-xs-max) { |
|||
display: none; |
|||
} |
|||
} |
|||
.caret { |
|||
position: relative; |
|||
top: -3.5px; |
|||
} |
|||
} |
|||
.o_switch_company_menu { |
|||
.oe_topbar_name { |
|||
@media (max-width: @screen-xs-max) { |
|||
display: none; |
|||
} |
|||
} |
|||
} |
|||
> .oe_systray > li > a { |
|||
.fa { |
|||
position: relative; |
|||
top: 3px; |
|||
font-size: 16px; |
|||
} |
|||
.caret { |
|||
position: relative; |
|||
top: 0.5px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.navbar-right { |
|||
float: right; |
|||
> li { |
|||
float: left; |
|||
} |
|||
@media (max-width: @screen-xs-max) { |
|||
.navbar-nav .open .dropdown-menu { |
|||
position: fixed; |
|||
top: 46px; |
|||
bottom: 0; |
|||
left: 0; |
|||
right: 0; |
|||
overflow: auto; |
|||
float: left; |
|||
background-color: @odoo-view-background-color; |
|||
border: 1px solid rgba(0, 0, 0, 0.15); |
|||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.container-fluid:before, .container-fluid:after, .navbar-collapse:before, .navbar-collapse:after { |
|||
display: inline; |
|||
} |
|||
|
|||
> .container-fluid { |
|||
margin: 0; |
|||
padding: 0; |
|||
|
|||
@media (max-width: @screen-xs-max) { |
|||
> .navbar-collapse { |
|||
margin: 0; |
|||
padding: 0; |
|||
overflow: auto; |
|||
&.collapsing { |
|||
overflow: hidden; |
|||
} |
|||
} |
|||
} |
|||
> .navbar-header { |
|||
margin: 0; |
|||
padding: 0; |
|||
> .drawer-toggle, .navbar-toggle { |
|||
margin: 0; |
|||
padding: 0; |
|||
border: 0px; |
|||
border-radius: 0px; |
|||
> i.fa, div.fa { |
|||
padding: 17px 14px 16px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.o_sub_menu > .o_sub_menu_content > .oe_secondary_menu { |
|||
ul.dropdown-menu > li.dropdown-header { |
|||
color: @odoo-view-background-color; |
|||
text-decoration: none; |
|||
background-color: @odoo-main-color-muted; |
|||
font-weight: bold; |
|||
} |
|||
@media (min-width: @screen-sm-min) { |
|||
height: @navbar-height; |
|||
} |
|||
margin: 0; |
|||
padding: 0; |
|||
> li { |
|||
@media (min-width: @screen-sm-min) { |
|||
height: @navbar-height; |
|||
} |
|||
margin: 0; |
|||
padding: 0; |
|||
&.app-name { |
|||
display: block; |
|||
padding: 7px 8px; |
|||
> .oe_menu_text { |
|||
font-size: 20px; |
|||
} |
|||
@media (min-width: @screen-sm-min) { |
|||
padding: 8.5px 12px; |
|||
} |
|||
} |
|||
> a { |
|||
margin: 0; |
|||
@media (min-width: @screen-sm-min) { |
|||
height: @navbar-height; |
|||
padding: 14px 8px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
> .navbar-right.o_menu_systray { |
|||
display: inline; |
|||
margin: 0; |
|||
padding: 0; |
|||
> ul { |
|||
margin: 0; |
|||
padding: 0; |
|||
> li > a { |
|||
margin: 0; |
|||
padding: 13px 8px; |
|||
height: @navbar-height; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.badge { |
|||
position: absolute; |
|||
top: 3px; |
|||
right: @navbar-padding-horizontal / 2; |
|||
} |
|||
|
|||
ul.nav > li > a { |
|||
padding: @app-drawer-navbar-padding-vertical @app-drawer-padding-horizontal; |
|||
} |
|||
|
|||
.o_planner_systray > .progress { |
|||
margin-top: 15px; |
|||
} |
|||
} |
|||
} |
|||
|
|||
a.navbar-collapse.collapse { |
|||
@media (min-width: @screen-sm-min) { |
|||
padding-bottom: @app-drawer-navbar-padding-vertical; |
|||
padding-top: @app-drawer-navbar-padding-vertical; |
|||
} |
|||
} |
|||
|
|||
.dropdown-scrollable { |
|||
overflow-x: hidden; |
|||
} |
@ -1,18 +0,0 @@ |
|||
/* Copyright 2016 LasLabs Inc. |
|||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */ |
|||
|
|||
// App Drawer / Icons |
|||
@app-drawer-icon-size: 6em; |
|||
@app-drawer-icon-margin: 1em; |
|||
@app-drawer-width: 80%; |
|||
@app-drawer-title-height: 54px; |
|||
|
|||
// Navbar |
|||
@navbar-height: 46px; |
|||
@app-drawer-navbar-height: @navbar-height / 2; |
|||
@app-drawer-navbar-padding-vertical: @navbar-padding-vertical / 2; |
|||
@app-drawer-padding-horizontal: @navbar-padding-horizontal / 2; |
|||
|
|||
// Drawer Toggle |
|||
@drawer-toggle-height: @navbar-height; |
|||
@drawer-toggle-width: @navbar-height; |
@ -1,16 +0,0 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!-- Copyright 2017 LasLabs Inc. |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
|
|||
<templates xml:space="preserve"> |
|||
<t t-name="AppDrawerMenuSearchResults"> |
|||
<li class="menu-search-element" t-foreach="menus" t-as="menu"> |
|||
<a t-att-id="menu.id" |
|||
t-attf-href="#action={{ menu.action and menu.action.split(',')[1] or ''}}&menu_id={{ menu.id }}"> |
|||
<h2 class="text-center"> |
|||
<t t-esc="menu.display_name" /> |
|||
</h2> |
|||
</a> |
|||
</li> |
|||
</t> |
|||
</templates> |
@ -0,0 +1,58 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!-- Copyright 2018 Tecnativa - Jairo Llopis |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
|
|||
<template> |
|||
<t t-extend="AppsMenu"> |
|||
<!-- App icons should be more than a text --> |
|||
<t t-jquery=".o_app > t" t-operation="replace"> |
|||
<t t-call="web_responsive.AppIcon"/> |
|||
</t> |
|||
|
|||
<!-- Same hotkey as in EE --> |
|||
<t t-jquery=".full" t-operation="attributes"> |
|||
<attribute name="accesskey">a</attribute> |
|||
</t> |
|||
|
|||
<!-- Search bar --> |
|||
<t t-jquery="[t-as=app]" t-operation="before"> |
|||
<div class="search-container form-row align-items-center mb-4 col-12"> |
|||
<div class="search-input col-md-10 ml-auto mr-auto mb-2"> |
|||
<div class="input-group"> |
|||
<div class="input-group-prepend"> |
|||
<div class="input-group-text"> |
|||
<i class="fa fa-search"/> |
|||
</div> |
|||
</div> |
|||
<input type="search" |
|||
placeholder="Search menus..." |
|||
class="form-control"/> |
|||
</div> |
|||
</div> |
|||
<div class="search-results col-md-10 ml-auto mr-auto"/> |
|||
</div> |
|||
</t> |
|||
</t> |
|||
|
|||
<!-- Separate app icon template, for easier inheritance --> |
|||
<t t-name="web_responsive.AppIcon"> |
|||
<img class="o-app-icon" |
|||
t-attf-src="data:image/png;base64,#{app.web_icon_data}"/> |
|||
<span class="o-app-name"> |
|||
<t t-esc="app.name"/> |
|||
</span> |
|||
</t> |
|||
|
|||
<!-- A search result --> |
|||
<t t-name="web_responsive.MenuSearchResults"> |
|||
<t t-foreach="results" t-as="result"> |
|||
<t t-set="menu" t-value="widget._menuInfo(result.original)"/> |
|||
<div t-attf-class="o-menu-search-result dropdown-item col-12 ml-auto mr-auto #{result_first ? 'active' : ''}" |
|||
t-attf-style="background-image:url('data:image/png;base64,#{menu.web_icon_data}')" |
|||
t-att-data-menu-id="menu.id" |
|||
t-att-data-action-id="menu.action_id" |
|||
t-att-data-parent-id="menu.parent_id[0]" |
|||
t-raw="result.string"/> |
|||
</t> |
|||
</t> |
|||
</template> |
@ -1,11 +1,16 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com> |
|||
<!-- Copyright 2017-2018 Tecnativa - Jairo Llopis |
|||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). --> |
|||
|
|||
<template> |
|||
<t t-extend="SwitchCompanyMenu"> |
|||
<t t-jquery=".oe_topbar_name" t-operation="before"> |
|||
<i class="fa fa-building visible-xs-inline-block"/> |
|||
<t t-extend="Menu"> |
|||
<t t-jquery=".o_menu_apps" t-operation="after"> |
|||
<!-- Hamburger button to show submenus in sm screens --> |
|||
<button class="o-menu-toggle d-md-none" |
|||
data-toggle="collapse" |
|||
data-target=".o_main_navbar .o_menu_sections"> |
|||
<i class="fa fa-bars"/> |
|||
</button> |
|||
</t> |
|||
</t> |
|||
</template> |
@ -1,290 +0,0 @@ |
|||
/* global QUnit */ |
|||
/* Copyright 2016 LasLabs Inc. |
|||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
|||
|
|||
odoo.define('web_responsive.test', function(require) { |
|||
"use strict"; |
|||
|
|||
var core = require('web.core'); |
|||
var responsive = require('web_responsive'); |
|||
|
|||
QUnit.module('web_responsive', { |
|||
beforeEach: function() { |
|||
var $el = $( |
|||
'<div class="drawer drawer--left">' + |
|||
'<header role="banner">' + |
|||
'<button class="drawer-toggle">' + |
|||
'<span class="drawer-hamburger-icon"/>' + |
|||
'</button>' + |
|||
'<nav class="drawer-nav">' + |
|||
'<ul class="drawer-menu">' + |
|||
'<li class="drawer-menu-item"/>' + |
|||
'</ul>' + |
|||
'</nav>' + |
|||
'<div class="panel-title" id="appDrawerAppPanelHead"></div>' + |
|||
'</header>' + |
|||
'<main role="main"></main>' + |
|||
'<a class="oe_menu_leaf"/>' + |
|||
'<div>' + |
|||
'<div class="o_sub_menu_content"></div>' + |
|||
'</div>' + |
|||
'<div class="dropdown-scrollable"></div>' + |
|||
'</div>' |
|||
); |
|||
|
|||
this.$clickZone = $el.find('a.oe_menu_leaf'); |
|||
this.$secondaryMenu = $el.find('div.o_sub_menu_content').parent(); |
|||
this.$dropdown = $el.find('div.dropdown-scrollable'); |
|||
|
|||
this.document = $("#qunit-fixture"); |
|||
this.document.append($el); |
|||
|
|||
this.drawer = new responsive.AppDrawer(); |
|||
}, |
|||
|
|||
linkGrid: function() { |
|||
for(var i = 0; i < 3; i++){ |
|||
this.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>' |
|||
) |
|||
) |
|||
); |
|||
this.drawer.$appLinks = this.drawer.$el.find('a.col-md-6'); |
|||
} |
|||
} |
|||
}); |
|||
|
|||
QUnit.test('It should set initialized after success init', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
assert.ok(this.drawer.initialized); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should close drawer after click on clickZone', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.$clickZone.click(); |
|||
|
|||
var self = this; |
|||
var d = $.Deferred(); |
|||
setTimeout(function() { |
|||
assert.ok(self.drawer.$el.hasClass('drawer-close')); |
|||
d.resolve(); |
|||
}, 100); |
|||
|
|||
return d; |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should collapse open secondary menus during handleClickZones', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.$clickZone.click(); |
|||
|
|||
var self = this; |
|||
var d = $.Deferred(); |
|||
setTimeout(function() { |
|||
assert.equal(self.$secondaryMenu.attr('aria-expanded'), 'false'); |
|||
d.resolve(); |
|||
}, 200); |
|||
|
|||
return d; |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should update max-height on scrollable dropdowns', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.drawer.handleWindowResize(); |
|||
|
|||
var height = $(window).height() * this.drawer.dropdownHeightFactor; |
|||
var actual = parseFloat(this.$dropdown.css('max-height')); |
|||
|
|||
var pass = Math.abs(actual - height) < 0.001; |
|||
|
|||
assert.pushResult({ |
|||
result: pass, |
|||
actual: actual, |
|||
expect: height, |
|||
message: '' |
|||
}); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should trigger core bus event for drawer close', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.drawer.onDrawerOpen(); |
|||
var d = $.Deferred(); |
|||
core.bus.on('drawer.closed', this, function() { |
|||
assert.ok(true); |
|||
d.resolve(); |
|||
}); |
|||
|
|||
this.drawer.$el.trigger({type: 'drawer.closed'}); |
|||
return d; |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should set isOpen to false when closing', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.drawer.onDrawerOpen(); |
|||
|
|||
var self = this; |
|||
var d = $.Deferred(); |
|||
setTimeout(function() { |
|||
assert.equal(self.drawer.isOpen, false); |
|||
d.resolve(); |
|||
}, 100); |
|||
|
|||
this.drawer.$el.trigger({type: 'drawer.closed'}); |
|||
|
|||
return d; |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should set isOpen to true when opening', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.drawer.$el.trigger({type: 'drawer.opened'}); |
|||
|
|||
var self = this; |
|||
var d = $.Deferred(); |
|||
setTimeout(function() { |
|||
assert.ok(self.drawer.isOpen); |
|||
d.resolve(); |
|||
}, 100); |
|||
|
|||
return d; |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should trigger core bus event for drawer open', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.drawer.onDrawerOpen(); |
|||
var d = $.Deferred(); |
|||
|
|||
core.bus.on('drawer.opened', this, function() { |
|||
assert.ok(true); |
|||
d.resolve(); |
|||
}); |
|||
|
|||
this.drawer.$el.trigger({type: 'drawer.opened'}); |
|||
return d; |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should choose link to right', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.linkGrid(); |
|||
|
|||
var $appLink = $('#a_1'), |
|||
$expect = $('#a_2'), |
|||
$res = this.drawer.findAdjacentLink( |
|||
$appLink, this.drawer.RIGHT |
|||
); |
|||
|
|||
assert.equal($res[0].id, $expect[0].id); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should choose link to left', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.linkGrid(); |
|||
var $appLink = $('#a_2'), |
|||
$expect = $('#a_1'), |
|||
$res = this.drawer.findAdjacentLink( |
|||
$appLink, this.drawer.LEFT |
|||
); |
|||
assert.equal($res[0].id, $expect[0].id); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should choose link above', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.linkGrid(); |
|||
var $appLink = $('#a_1'), |
|||
$expect = $('#a_0'), |
|||
$res = this.drawer.findAdjacentLink( |
|||
$appLink, this.drawer.UP |
|||
); |
|||
assert.equal($res[0].id, $expect[0].id); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should choose link below', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.linkGrid(); |
|||
var $appLink = $('#a_1'), |
|||
$expect = $('#a_2'), |
|||
$res = this.drawer.findAdjacentLink( |
|||
$appLink, this.drawer.DOWN |
|||
); |
|||
assert.equal($res[0].id, $expect[0].id); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should choose first link if next on last', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.linkGrid(); |
|||
var $appLink = $('#b_2'), |
|||
$expect = $('#a_0'), |
|||
$res = this.drawer.findAdjacentLink( |
|||
$appLink, this.drawer.RIGHT |
|||
); |
|||
assert.equal($res[0].id, $expect[0].id); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should choose bottom link if up on top', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.linkGrid(); |
|||
var $appLink = $('#a_0'), |
|||
$expect = $('#a_2'), |
|||
$res = this.drawer.findAdjacentLink( |
|||
$appLink, this.drawer.UP |
|||
); |
|||
assert.equal($res[0].id, $expect[0].id); |
|||
} |
|||
); |
|||
|
|||
QUnit.test('It should choose top link if down on bottom', |
|||
function(assert) { |
|||
assert.expect(1); |
|||
|
|||
this.linkGrid(); |
|||
var $appLink = $('#a_2'), |
|||
$expect = $('#a_0'), |
|||
$res = this.drawer.findAdjacentLink( |
|||
$appLink, this.drawer.DOWN |
|||
); |
|||
assert.equal($res[0].id, $expect[0].id); |
|||
} |
|||
); |
|||
|
|||
}); |
@ -1,2 +1 @@ |
|||
from . import test_ui |
|||
from . import test_res_users |
@ -1,15 +0,0 @@ |
|||
# Copyright 2016 LasLabs Inc. |
|||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html). |
|||
|
|||
from odoo.tests import HttpCase |
|||
|
|||
|
|||
class TestUi(HttpCase): |
|||
|
|||
def test_ui_web(self): |
|||
"""Test backend tests.""" |
|||
self.phantom_js( |
|||
"/web/tests?module=web_responsive", |
|||
"", |
|||
login="admin", |
|||
) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue