Browse Source

[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
parent
commit
69a7491f4a
No known key found for this signature in database GPG Key ID: 59564BF1E22F314F
  1. 108
      web_responsive/README.rst
  2. 3
      web_responsive/__init__.py
  3. 11
      web_responsive/__manifest__.py
  4. 73
      web_responsive/i18n/es.po
  5. 68
      web_responsive/i18n/web_responsive.pot
  6. 5
      web_responsive/models/__init__.py
  7. 0
      web_responsive/models/res_users.py
  8. 62
      web_responsive/readme/DESCRIPTION.rst
  9. 28
      web_responsive/readme/ROADMAP.rst
  10. 8
      web_responsive/readme/USAGE.rst
  11. 95
      web_responsive/static/description/index.html
  12. 525
      web_responsive/static/lib/css/drawer.3.2.2.css
  13. 765
      web_responsive/static/lib/js/bililiteRange.2.6.js
  14. 183
      web_responsive/static/lib/js/drawer.3.2.2.js
  15. 2197
      web_responsive/static/lib/js/iscroll-probe.5.2.0.js
  16. 57
      web_responsive/static/lib/js/jquery.sendkeys.4.js
  17. 427
      web_responsive/static/src/css/web_responsive.scss
  18. 799
      web_responsive/static/src/js/web_responsive.js
  19. 129
      web_responsive/static/src/less/app_drawer.less
  20. 212
      web_responsive/static/src/less/form_view.less
  21. 102
      web_responsive/static/src/less/main.less
  22. 195
      web_responsive/static/src/less/navbar.less
  23. 18
      web_responsive/static/src/less/variables.less
  24. 16
      web_responsive/static/src/xml/app_drawer_menu_search.xml
  25. 58
      web_responsive/static/src/xml/apps.xml
  26. 151
      web_responsive/static/src/xml/form_view.xml
  27. 13
      web_responsive/static/src/xml/navbar.xml
  28. 290
      web_responsive/static/tests/js/web_responsive.js
  29. 1
      web_responsive/tests/__init__.py
  30. 15
      web_responsive/tests/test_ui.py
  31. 37
      web_responsive/views/assets.xml
  32. 0
      web_responsive/views/res_users.xml
  33. 293
      web_responsive/views/web.xml

108
web_responsive/README.rst

@ -14,26 +14,74 @@ Web Responsive
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github
:target: https://github.com/OCA/web/tree/11.0/web_responsive
:target: https://github.com/OCA/web/tree/12.0/web_responsive
:alt: OCA/web
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/web-11-0/web-11-0-web_responsive
:target: https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_responsive
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/162/11.0
:target: https://runbot.odoo-community.org/runbot/162/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
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
**Table of contents**
@ -45,36 +93,20 @@ Usage
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
Known issues / Roadmap
======================
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?
Bug Tracker
===========
@ -82,7 +114,7 @@ Bug Tracker
Bugs are tracked on `GitHub Issues <https://github.com/OCA/web/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
`feedback <https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
@ -118,6 +150,6 @@ OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/11.0/web_responsive>`_ project on GitHub.
This module is part of the `OCA/web <https://github.com/OCA/web/tree/12.0/web_responsive>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

3
web_responsive/__init__.py

@ -1,4 +1 @@
# Copyright 2018 Alexandre Díaz
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
from . import models

11
web_responsive/__manifest__.py

@ -4,11 +4,10 @@
{
"name": "Web Responsive",
"summary": "It provides a mobile compliant interface for Odoo Community "
"web",
"version": "11.0.2.0.0",
"summary": "Responsive web client, community-supported",
"version": "12.0.1.0.0",
"category": "Website",
"website": "https://laslabs.com/",
"website": "https://github.com/OCA/web",
"author": "LasLabs, Tecnativa, Alexandre Díaz, "
"Odoo Community Association (OCA)",
"license": "LGPL-3",
@ -18,11 +17,11 @@
],
"data": [
'views/assets.xml',
'views/res_users.xml',
'views/web.xml',
'views/inherited_view_users_form_simple_modif.xml',
],
'qweb': [
'static/src/xml/app_drawer_menu_search.xml',
'static/src/xml/apps.xml',
'static/src/xml/form_view.xml',
'static/src/xml/navbar.xml',
],

73
web_responsive/i18n/es.po

@ -8,52 +8,71 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-12-23 02:13+0000\n"
"PO-Revision-Date: 2018-08-02 06:36+0000\n"
"Last-Translator: Pedro M. Baeza <pedro.baeza@gmail.com>\n"
"POT-Creation-Date: 2019-01-10 10:49+0000\n"
"PO-Revision-Date: 2019-01-10 10:50+0000\n"
"Last-Translator: Jairo Llopis <yajo.sk8@gmail.com>\n"
"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.1.1\n"
"X-Generator: Poedit 2.2\n"
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.webclient_bootstrap
msgid "<span class=\"sr-only\">Toggle App Drawer</span>"
msgstr ""
"<span class=\"sr-only\">Mostrar/ocultar selector de aplicaciones</span>"
#: model:ir.model.fields,field_description:web_responsive.field_res_users__chatter_position
msgid "Chatter Position"
msgstr "Posición del chatter"
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.webclient_bootstrap
msgid "<span class=\"sr-only\">Toggle Navigation</span>"
msgstr "<span class=\"sr-only\">Mostrar/Ocultar navegación</span>"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:57
#: code:addons/web_responsive/static/src/xml/form_view.xml:96
#, python-format
msgid "Create"
msgstr "Crear"
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.menu
msgid "Apps"
msgstr "Aplicaciones"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:71
#: code:addons/web_responsive/static/src/xml/form_view.xml:110
#, python-format
msgid "Discard"
msgstr "Descartar"
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users_chatter_position
msgid "Chatter Position"
msgstr "Posición del chatter"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:50
#, python-format
msgid "Edit"
msgstr "Editar"
#. module: web_responsive
#: model:ir.model,name:web_responsive.model_ir_http
msgid "HTTP routing"
msgstr "Enrutado HTTP"
#: selection:res.users,chatter_position:0
msgid "Normal"
msgstr "Normal"
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.menu
msgid "More <b class=\"caret\"/>"
msgstr "Más <b class=\"caret\"/>"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:27
#, python-format
msgid "Quick actions"
msgstr "Acciones rápidas"
#. module: web_responsive
#: selection:res.users,chatter_position:0
msgid "Normal"
msgstr "Normal"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:64
#: code:addons/web_responsive/static/src/xml/form_view.xml:103
#, python-format
msgid "Save"
msgstr "Guardar"
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/apps.xml:29
#, python-format
msgid "Search menus..."
msgstr "Buscar menús..."
#. module: web_responsive
#: selection:res.users,chatter_position:0

68
web_responsive/i18n/web_responsive.pot

@ -4,8 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Project-Id-Version: Odoo Server 12.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-01-10 10:49+0000\n"
"PO-Revision-Date: 2019-01-10 10:49+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -14,57 +16,58 @@ msgstr ""
"Plural-Forms: \n"
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.menu
msgid "<i>Searching:</i>"
msgstr ""
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.menu
msgid "<span class=\"hidden-xs\">\n"
" &amp;nbsp;|&amp;nbsp;\n"
" </span>"
#: model:ir.model.fields,field_description:web_responsive.field_res_users__chatter_position
msgid "Chatter Position"
msgstr ""
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.webclient_bootstrap
msgid "<span class=\"sr-only\">Toggle App Drawer</span>"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:57
#: code:addons/web_responsive/static/src/xml/form_view.xml:96
#, python-format
msgid "Create"
msgstr ""
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.webclient_bootstrap
msgid "<span class=\"sr-only\">Toggle Navigation</span>"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:71
#: code:addons/web_responsive/static/src/xml/form_view.xml:110
#, python-format
msgid "Discard"
msgstr ""
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.menu
msgid "Apps"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:50
#, python-format
msgid "Edit"
msgstr ""
#. module: web_responsive
#: model:ir.model.fields,field_description:web_responsive.field_res_users_chatter_position
msgid "Chatter Position"
#: selection:res.users,chatter_position:0
msgid "Normal"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:35
#: code:addons/web_responsive/static/src/xml/form_view.xml:27
#, python-format
msgid "More"
msgid "Quick actions"
msgstr ""
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.menu
msgid "More <b class=\"caret\"/>"
msgstr ""
#. module: web_responsive
#: model:ir.ui.view,arch_db:web_responsive.menu
msgid "No Search Supplied."
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:64
#: code:addons/web_responsive/static/src/xml/form_view.xml:103
#, python-format
msgid "Save"
msgstr ""
#. module: web_responsive
#: selection:res.users,chatter_position:0
msgid "Normal"
#. openerp-web
#: code:addons/web_responsive/static/src/xml/apps.xml:29
#, python-format
msgid "Search menus..."
msgstr ""
#. module: web_responsive
@ -72,13 +75,6 @@ msgstr ""
msgid "Sided"
msgstr ""
#. module: web_responsive
#. openerp-web
#: code:addons/web_responsive/static/src/xml/form_view.xml:54
#, python-format
msgid "Task"
msgstr ""
#. module: web_responsive
#: model:ir.model,name:web_responsive.model_res_users
msgid "Users"

5
web_responsive/models/__init__.py

@ -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

0
web_responsive/models/inherited_res_users.py → web_responsive/models/res_users.py

62
web_responsive/readme/DESCRIPTION.rst

@ -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

28
web_responsive/readme/ROADMAP.rst

@ -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?

8
web_responsive/readme/USAGE.rst

@ -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

95
web_responsive/static/description/index.html

@ -367,15 +367,54 @@ ul.auto-toc {
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/web/tree/11.0/web_responsive"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/web-11-0/web-11-0-web_responsive"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/162/11.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module provides a mobile compliant interface for Odoo Community web.</p>
<p>Features:</p>
<ul class="simple">
<li>New navigation with an App drawer</li>
<li>Keyboard shortcuts for easier navigation</li>
<li>Display kanban views for small screens if an action or field One2x</li>
<li>Set chatter side (Optional per user)</li>
<li>Quick search</li>
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/web/tree/12.0/web_responsive"><img alt="OCA/web" src="https://img.shields.io/badge/github-OCA%2Fweb-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/web-12-0/web-12-0-web_responsive"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/162/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module adds responsiveness to web backend.</p>
<p>Features for all devices:</p>
<ul>
<li><p class="first">New navigation with an app drawer</p>
<img alt="https://user-images.githubusercontent.com/973709/48417193-09a1e080-e74a-11e8-8a0c-e73eb689b2fb.gif" src="https://user-images.githubusercontent.com/973709/48417193-09a1e080-e74a-11e8-8a0c-e73eb689b2fb.gif" />
</li>
<li><p class="first">Quick menu search from the app drawer</p>
<img alt="https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif" src="https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif" />
</li>
</ul>
<p>Features for mobile:</p>
<ul>
<li><p class="first">App-specific submenus are shown on full screen when toggling them from the
“hamburger” menu</p>
<img alt="https://user-images.githubusercontent.com/973709/48417297-51286c80-e74a-11e8-9a47-22c810b18c43.gif" src="https://user-images.githubusercontent.com/973709/48417297-51286c80-e74a-11e8-9a47-22c810b18c43.gif" />
</li>
<li><p class="first">View type picker dropdown displays confortably</p>
<img alt="https://user-images.githubusercontent.com/973709/50964322-e3d55580-14c6-11e9-8249-48db9539600f.gif" src="https://user-images.githubusercontent.com/973709/50964322-e3d55580-14c6-11e9-8249-48db9539600f.gif" />
</li>
<li><p class="first">Top app bar is always visible, but the control panel is hidden when
scrolling down, to save some vaulable vertical space</p>
<img alt="https://user-images.githubusercontent.com/973709/50964496-5cd4ad00-14c7-11e9-9261-fd223a329d02.gif" src="https://user-images.githubusercontent.com/973709/50964496-5cd4ad00-14c7-11e9-9261-fd223a329d02.gif" />
</li>
<li><p class="first">Form status bar action and status buttons are collapsed in dropdowns.
Other control panel buttons use icons to save space.</p>
<img alt="https://user-images.githubusercontent.com/973709/50965446-e08f9900-14c9-11e9-92d6-dda472cb6557.gif" src="https://user-images.githubusercontent.com/973709/50965446-e08f9900-14c9-11e9-92d6-dda472cb6557.gif" />
</li>
<li><p class="first">Breadcrumbs navigation is collapsed with a “back arrow” button.</p>
<img alt="https://user-images.githubusercontent.com/973709/50965168-1d0ec500-14c9-11e9-82a0-dfee82ed0861.gif" src="https://user-images.githubusercontent.com/973709/50965168-1d0ec500-14c9-11e9-82a0-dfee82ed0861.gif" />
</li>
</ul>
<p>Features for computers:</p>
<ul>
<li><p class="first">Keyboard shortcuts for easier navigation, <strong>using ``Alt + Shift + [key]``</strong>
combination instead of just <tt class="docutils literal">Alt + [key]</tt>.
See <a class="reference external" href="https://github.com/odoo/odoo/issues/30068">https://github.com/odoo/odoo/issues/30068</a> to understand why.</p>
<img alt="https://user-images.githubusercontent.com/973709/48417578-ff341680-e74a-11e8-8881-017709e912bc.png" src="https://user-images.githubusercontent.com/973709/48417578-ff341680-e74a-11e8-8881-017709e912bc.png" />
</li>
<li><p class="first">Autofocus on search menu box when opening the drawer</p>
<img alt="https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif" src="https://user-images.githubusercontent.com/973709/48417213-17576600-e74a-11e8-846a-57691e82636b.gif" />
</li>
<li><p class="first">Set chatter on the side of the screen, optional per user</p>
<img alt="https://user-images.githubusercontent.com/973709/48417270-41108d00-e74a-11e8-9172-cba825d027ed.gif" src="https://user-images.githubusercontent.com/973709/48417270-41108d00-e74a-11e8-9172-cba825d027ed.gif" />
</li>
<li><p class="first">Full width form sheets</p>
<img alt="https://user-images.githubusercontent.com/973709/48417428-ac5a5f00-e74a-11e8-8839-5bc538c54c1d.png" src="https://user-images.githubusercontent.com/973709/48417428-ac5a5f00-e74a-11e8-8839-5bc538c54c1d.png" />
</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
@ -395,35 +434,21 @@ ul.auto-toc {
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>The following keyboard shortcuts are implemented:</p>
<ul class="simple">
<li>Toggle App Drawer - <cite>ActionKey &lt;https://en.wikipedia.org/wiki/Access_key#Access_in_different_browsers&gt;</cite> + <tt class="docutils literal">A</tt></li>
<li>Navigate Apps Drawer - Arrow Keys</li>
<li>Type to select App Links</li>
<li><tt class="docutils literal">esc</tt> to close App Drawer</li>
<li>Toggle app drawer - <tt class="docutils literal">Alt + Shift + H</tt></li>
<li>Navigate app search results - Arrow keys</li>
<li>Choose app result - <tt class="docutils literal">Enter</tt></li>
<li><tt class="docutils literal">Esc</tt> to close app drawer</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id2">Known issues / Roadmap</a></h1>
<p>Note: Data added to the footer <tt class="docutils literal">support_branding</tt> is not shown while using
this module.</p>
<ul class="simple">
<li>Provide full menu search feature instead of just App search</li>
<li>Drag drawer from left to open in mobile</li>
<li>Figure out how to test focus on hidden elements for keyboard nav tests</li>
<li>If you resize the window, body gets a wrong <tt class="docutils literal">overflow: auto</tt> css property
and you need to refresh your view or open/close the app drawer to fix that.</li>
<li>Override LESS styling to allow for responsive widget layouts</li>
<li>Adding <tt class="docutils literal">oe_main_menu_navbar</tt> ID to the top navigation bar triggers some
great styles, but also <a class="reference external" href="https://github.com/OCA/web/pull/446#issuecomment-254827880">JavaScript that causes issues on mobile</a></li>
<li>Sticky header and footer in list view only works on certain browsers:
<a class="reference external" href="https://caniuse.com/#search=sticky">https://caniuse.com/#search=sticky</a> (note that the used feature is in
<cite>thead</cite>).</li>
<li>On Android (FireFox) - clicking the search icon does not
focus the search input.</li>
<li>On Android (FireFox &amp; 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.</li>
<li>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.</li>
<li>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.</li>
<li>App navigation with keyboard.</li>
<li>Make it more beautiful. Maybe OCA-branded?</li>
</ul>
</div>
<div class="section" id="bug-tracker">
@ -431,7 +456,7 @@ users to filter on complete paths of menus and not only on the last item.</li>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/web/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2011.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<a class="reference external" href="https://github.com/OCA/web/issues/new?body=module:%20web_responsive%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
@ -461,7 +486,7 @@ If you spotted it first, help us smashing it by providing a detailed and welcome
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/11.0/web_responsive">OCA/web</a> project on GitHub.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/web/tree/12.0/web_responsive">OCA/web</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>

525
web_responsive/static/lib/css/drawer.3.2.2.css

@ -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;
}
}

765
web_responsive/static/lib/js/bililiteRange.2.6.js

@ -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);
}
};
}

183
web_responsive/static/lib/js/drawer.3.2.2.js

@ -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

57
web_responsive/static/lib/js/jquery.sendkeys.4.js

@ -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)

427
web_responsive/static/src/css/web_responsive.scss

@ -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;
}
}
}
}
}

799
web_responsive/static/src/js/web_responsive.js

@ -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);
});

129
web_responsive/static/src/less/app_drawer.less

@ -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();
}

212
web_responsive/static/src/less/form_view.less

@ -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;
}
}

102
web_responsive/static/src/less/main.less

@ -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;
}

195
web_responsive/static/src/less/navbar.less

@ -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;
}

18
web_responsive/static/src/less/variables.less

@ -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;

16
web_responsive/static/src/xml/app_drawer_menu_search.xml

@ -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 ''}}&amp;menu_id={{ menu.id }}">
<h2 class="text-center">
<t t-esc="menu.display_name" />
</h2>
</a>
</li>
</t>
</templates>

58
web_responsive/static/src/xml/apps.xml

@ -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 &gt; 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>

151
web_responsive/static/src/xml/form_view.xml

@ -2,63 +2,140 @@
<!--
Copyright 2017 LasLabs Inc.
Copyright 2018 Alexandre Díaz
Copyright 2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="form_view" xml:space="preserve">
<!-- Template for buttons that display only the icon in xs -->
<t t-name="web_responsive.icon_button">
<i t-attf-class="fa fa-#{icon}"
t-att-title="label"/>
<span class="d-none d-sm-inline" t-esc="label"/>
</t>
<t t-name="web_responsive.MenuStatusbarButtons">
<div class="dropdown">
<button class="o_statusbar_buttons_dropdown btn btn-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'cogs'"/>
<t t-set="label">Quick actions</t>
</t>
</button>
<!-- A div.o_statusbar_buttons.dropdown-menu
is appended here from JS -->
</div>
</t>
<t t-extend="FormView.buttons">
<t t-jquery="button[accesskey='a']" t-operation="attributes">
<!-- Change "Edit" button hotkey to "E" -->
<t t-jquery=".o_form_button_edit" t-operation="attributes">
<attribute name="accesskey">e</attribute>
</t>
<!-- Change "Discard" button hotkey to "D" -->
<t t-jquery=".o_form_button_cancel" t-operation="attributes">
<attribute name="accesskey">d</attribute>
</t>
<!-- Add responsive icons to buttons -->
<t t-jquery=".o_form_button_edit" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'pencil'"/>
<t t-set="label">Edit</t>
</t>
</t>
<t t-jquery=".o_form_button_create" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'plus'"/>
<t t-set="label">Create</t>
</t>
</t>
<t t-jquery=".o_form_button_save" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'check'"/>
<t t-set="label">Save</t>
</t>
</t>
<t t-jquery=".o_form_button_cancel" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'times'"/>
<t t-set="label">Discard</t>
</t>
</t>
</t>
<t t-extend="FieldStatus.content.button">
<t t-jquery="button" t-operation="replace">
<button type="button" t-att-data-value="i.id" t-att-disabled="disabled ? 'disabled' : undefined"
t-attf-class="btn btn-sm o_arrow_button btn-#{i.selected ? 'primary' : 'default'}#{disabled ? ' disabled' : ''} #{button_css or ''}">
<t t-esc="i.display_name"/>
</button>
<t t-extend="KanbanView.buttons">
<!-- Add responsive icons to buttons -->
<t t-jquery="button" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'plus'"/>
<t t-set="label" t-value="create_text || _t('Create')"/>
</t>
</t>
</t>
<t t-extend="ListView.buttons">
<!-- Change "Discard" button hotkey to "D" -->
<t t-jquery=".o_list_button_discard" t-operation="attributes">
<attribute name="accesskey">d</attribute>
</t>
<!-- Add responsive icons to buttons -->
<t t-jquery=".o_list_button_add" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'plus'"/>
<t t-set="label">Create</t>
</t>
</t>
<t t-extend="FieldStatus.content">
<t t-jquery="[t-if='selection_folded.length']" t-operation="replace">
<t t-if="selections &amp;&amp; has_folded">
<ul class="dropdown-menu o-status-more hidden-lg hidden-md" role="menu">
<t t-set="button_css" t-value="'hidden-lg hidden-md'" />
<li t-foreach="selections" t-as="i">
<t t-if="!i.selected" t-call="FieldStatus.content.button"/>
</li>
</ul>
<button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle hidden-lg hidden-md" data-toggle="dropdown" aria-expanded="false">More <span class="caret"/></button>
<t t-jquery=".o_list_button_save" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'check'"/>
<t t-set="label">Save</t>
</t>
</t>
<t t-jquery="[t-foreach='selection_unfolded.reverse()']" t-operation="replace">
<t t-if="selections">
<t t-foreach="selections.reverse()" t-as="i">
<t t-set="button_css" t-value="i.selected?'':'hidden-xs hidden-sm'" />
<t t-call="FieldStatus.content.button"/>
</t>
<t t-jquery=".o_list_button_discard" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'times'"/>
<t t-set="label">Discard</t>
</t>
</t>
</t>
<t t-name="web_responsive.MenuStatusbarButtons">
<div class="o_statusbar_buttons_container">
<div class="o_statusbar_buttons_base hidden-xs hidden-sm">
<!-- Normal Buttons Zone -->
</div>
<div class="dropdown o_statusbar_buttons_dropdown hidden-lg hidden-md">
<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenuHeader" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Task
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuHeader">
</ul>
</div>
</div>
<t t-extend="Sidebar">
<!-- Replace some common sections by icons in mobile -->
<t t-jquery=".o_dropdown_toggler_btn t[t-esc='section.label']"
t-operation="replace">
<t t-set="label" t-value="section.label"/>
<t t-if="section.name == 'files'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'paperclip'"/>
</t>
</t>
<t t-elif="section.name == 'print'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'print'"/>
</t>
</t>
<t t-elif="section.name == 'other'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'wrench'"/>
</t>
</t>
<t t-else="">
<span t-esc="label"/>
</t>
</t>
</t>
</templates>

13
web_responsive/static/src/xml/navbar.xml

@ -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>

290
web_responsive/static/tests/js/web_responsive.js

@ -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
web_responsive/tests/__init__.py

@ -1,2 +1 @@
from . import test_ui
from . import test_res_users

15
web_responsive/tests/test_ui.py

@ -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",
)

37
web_responsive/views/assets.xml

@ -10,46 +10,11 @@
<template id="assets_backend" name="Open Mobile Assets" inherit_id="web.assets_backend">
<xpath expr=".">
<link rel="stylesheet"
type="text/css"
href="/web_responsive/static/lib/css/drawer.3.2.2.css"
href="/web_responsive/static/src/css/web_responsive.scss"
/>
<link rel="stylesheet"
href="/web_responsive/static/src/less/main.less"
/>
<link rel="stylesheet"
href="/web_responsive/static/src/less/navbar.less"
/>
<link rel="stylesheet"
href="/web_responsive/static/src/less/app_drawer.less"
/>
<link rel="stylesheet"
href="/web_responsive/static/src/less/form_view.less"
/>
<link rel="stylesheet"
href="/web_responsive/static/src/less/variables.less"
/>
<script type="application/javascript"
src="/web_responsive/static/lib/js/bililiteRange.2.6.js"
/>
<script type="application/javascript"
src="/web_responsive/static/lib/js/jquery.sendkeys.4.js"
/>
<script type="application/javascript"
src="/web_responsive/static/lib/js/iscroll-probe.5.2.0.js"
/>
<script type="application/javascript"
src="/web_responsive/static/lib/js/drawer.3.2.2.js"
/>
<script type="application/javascript"
src="/web_responsive/static/src/js/web_responsive.js"
/>
</xpath>
</template>
<template id="qunit_suite" inherit_id="web.qunit_suite">
<xpath expr="//t[@t-set='head']" position="inside">
<script type="application/javascript"
src="/web_responsive/static/tests/js/web_responsive.js"
/>
</xpath>
</template>
</odoo>

0
web_responsive/views/inherited_view_users_form_simple_modif.xml → web_responsive/views/res_users.xml

293
web_responsive/views/web.xml

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2016 LasLabs Inc.
Copyright 2018 Alexandre Díaz
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
@ -12,299 +11,9 @@
inherit_id="web.webclient_bootstrap"
name="App Drawer - Web Client"
>
<xpath expr="//div[hasclass('o_sub_menu')]" position="replace" />
<xpath expr="//t[@t-set='head']" position="inside">
<meta charset="utf-8" />
<meta http-equiv="cleartype" content="on" />
<meta name="MobileOptimized" content="320" />
<meta name="HandheldFriendly" content="True" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
</xpath>
<xpath expr="//nav[@id='oe_main_menu_navbar']" position="replace">
<t t-set="body_classname" t-value="'drawer drawer--left o_web_client'" />
<header role="banner">
<nav id="odooAppDrawer" class="app-drawer-nav drawer-nav" role="navigation">
<t t-call="web.menu" />
</nav>
<nav class="navbar navbar-default main-nav"
role="navigation"
groups="base.group_user,base.group_portal"
>
<div class="container-fluid">
<div class="navbar-header">
<a class="drawer-toggle navbar-collapse collapse btn btn-default app-drawer-toggle"
accesskey="A"
>
<span class="sr-only">Toggle App Drawer</span>
<i class="fa fa-th fa-lg app-drawer-icon-open"
t-translation="off"
aria-hidden="true"
/>
</a>
<button type="button"
class="app-drawer-toggle drawer-toggle pull-left navbar-toggle collapsed"
>
<span class="sr-only">Toggle App Drawer</span>
<div class="fa fa-th fa-lg app-drawer-icon-open" />
</button>
<button type="button"
id="odooMenuBarToggle"
class="navbar-toggle collapsed pull-right"
data-toggle="collapse"
data-target="#odooMenuBarNav"
>
<span class="sr-only">Toggle Navigation</span>
<i class="fa fa-bars fa-lg"
t-translation="off"
aria-hidden="true"
/>
</button>
</div>
<div class="collapse navbar-collapse"
id="odooMenuBarNav"
data-parent="#odooMenuBarToggle"
aria-expanded="false"
>
<div class="o_sub_menu"
groups="base.group_user,base.group_portal"
>
<t t-call="web.menu_secondary" />
</div>
</div>
</div>
<div class="nav navbar-nav navbar-right navbar-systray o_menu_systray">
<ul class="nav navbar-nav navbar-right navbar-systray-item oe_user_menu_placeholder"/>
<ul class="nav navbar-nav navbar-right navbar-systray-item oe_systray"/>
</div>
</nav>
</header>
</xpath>
<xpath expr="//div[hasclass('o_main')]" position="attributes">
<xpath expr="//*[hasclass('o_main')]" position="attributes">
<attribute name="t-attf-class">o_main o_chatter_position_{{ request.env.user.chatter_position or 'normal' }}</attribute>
</xpath>
</template>
<template id="menu_secondary"
inherit_id="web.menu_secondary"
name="App Drawer - Secondary Menu"
>
<xpath expr="//div[hasclass('o_sub_menu_content')]/t" position="replace">
<t t-foreach="menu_data['children']" t-as="menu">
<ul style="display: none"
class="oe_secondary_menu nav navbar-nav"
t-att-data-menu-parent="menu['id']">
<li class="app-name">
<span class="oe_menu_text">
<t t-esc="menu['name']"/>
</span>
</li>
<li>
<t t-call="web.menu_secondary_submenu" />
</li>
</ul>
</t>
</xpath>
</template>
<template id="menu_secondary_submenu"
inherit_id="web.menu_secondary_submenu"
name="App Drawer - Secondary Submenu"
>
<xpath expr="//ul" position="replace">
<t t-foreach="menu['children']" t-as="menu">
<t t-if="menu['children']">
<li t-attf-class="{{ 'dropdown-header' if submenu else '' }}">
<t t-if="submenu">
<t t-esc="menu['name']" />
<t t-call="web.menu_secondary_submenu">
<t t-set="submenu" t-value="True" />
</t>
</t>
<t t-if="not submenu">
<a class="dropdown-toggle"
data-toggle="dropdown"
role="button"
aria-haspopup="true"
aria-expanded="false"
>
<t t-esc="menu['name']" />
<span class="caret" />
</a>
<ul t-if="menu['children']"
t-attf-class="dropdown-menu oe_secondary_submenu dropdown-scrollable"
>
<t t-call="web.menu_secondary_submenu">
<t t-set="submenu" t-value="True" />
</t>
</ul>
</t>
</li>
</t>
<t t-if="not menu['children']">
<li>
<t t-call="web.menu_link" />
</li>
</t>
</t>
</xpath>
</template>
<template id="menu_link"
inherit_id="web.menu_link"
name="App Drawer - Menu Link"
>
<xpath expr="//a" position="attributes">
<attribute name="t-att-data-menu-name">menu['name']</attribute>
</xpath>
<xpath expr="//span[hasclass('oe_menu_text')]" position="replace">
<t t-if="display_images">
<img t-attf-src="/web/image/ir.ui.menu/{{ menu['id'] }}/web_icon_data"
class="app-drawer-icon-app img-rounded"
t-att-alt="menu['name']"
t-att-title="menu['name']"
/>
<p class="app-drawer-title text-center">
<t t-esc="menu['name']" />
</p>
</t>
<t t-if="not display_images">
<span class="oe_menu_text">
<t t-esc="menu['name']" />
</span>
</t>
</xpath>
</template>
<template id="menu"
inherit_id="web.menu"
name="App Drawer - Menu"
>
<xpath expr="//ul[hasclass('oe_systray')]" position="replace" />
<xpath expr="//ul[hasclass('oe_user_menu_placeholder')]" position="replace" />
<xpath expr="//ul[hasclass('oe_application_menu_placeholder')]" position="replace">
<div class="panel-default app-drawer-app-panel" id="appDrawerAppMenu">
<div class="panel-heading" id="appDrawerAppPanelHead">
<div class="col-xs-6">
<h4 class="app-drawer-panel-title pull-left">
<a class="app-drawer-icon-close drawer-toggle hidden-xs">
<i class="fa fa-lg fa-chevron-left"
t-translation="off"
aria-hidden="true"
/>
Apps
</a>
<span class="hidden-xs">
&amp;nbsp;|&amp;nbsp;
</span>
<a class="app-drawer-icon-search drawer-search-open">
<i class="fa fa-lg fa-search"
t-translation="off"
aria-hidden="true"
/>
</a>
<a class="app-drawer-icon-search drawer-search-close">
<i class="fa fa-lg fa-close"
t-translation="off"
aria-hidden="true"
/>
</a>
</h4>
</div>
<div class="col-xs-6">
<a class="oe_logo pull-right" t-attf-href="/web?{{ keep_query() }}">
<i class="fa fa-pencil-square-o oe_logo_edit"
aria-hidden="true"
t-translation="off"
/>
<img src='/web/binary/company_logo'/>
</a>
</div>
</div>
<div class="panel-body" id="appDrawerAppPanelBody">
<div id="appDrawerApps"
class="row oe_application_menu_placeholder"
style="display: none;">
<t t-foreach="menu_data['children']" t-as="menu">
<div class="col-xs-6 col-sm-4 col-md-3 col-lg-2 text-center mt16">
<t t-call="web.menu_link">
<t t-set="display_images" t-value="1" />
</t>
</div>
<!-- Provide breakpoints on necessary viewports for proper row heights -->
<t t-if="(menu_index + 1) % 6 == 0">
<div class="clearfix visible-lg-block" />
</t>
<t t-if="(menu_index + 1) % 4 == 0">
<div class="clearfix visible-md-block" />
</t>
<t t-if="(menu_index + 1) % 3 == 0">
<div class="clearfix visible-sm-block" />
</t>
<t t-if="(menu_index + 1) % 2 == 0">
<div class="clearfix visible-xs-block" />
</t>
</t>
<div id="menu_more_container" class="dropdown" style="display: none;">
<a class="dropdown-toggle" data-toggle="dropdown">More <b class="caret"></b></a>
<ul id="menu_more" class="dropdown-menu"></ul>
</div>
</div>
<ul id="appDrawerMenuSearch"
class="row list-unstyled"
style="display: none;">
<div class="panel panel-info app-drawer-search-panel">
<div class="panel-heading">
<h2>
<i>Searching:</i>
<input id="appDrawerSearchInput" class="menu-search-query"/>
</h2>
</div>
<div class="panel-body">
<ul id="appDrawerSearchResults"
class="row list-unstyled oe_application_menu_placeholder">
No Search Supplied.
</ul>
</div>
</div>
</ul>
</div>
</div>
<div class="app-drawer-search-action" />
</xpath>
</template>
</odoo>
Loading…
Cancel
Save