Edgar Ortigoza преди 8 години
ревизия
f17fd049c0
променени са 39 файла, в които са добавени 6845 реда и са изтрити 0 реда
  1. 4 0
      __init__.py
  2. 22 0
      __openerp__.py
  3. 1 0
      controllers/__init__.py
  4. 149 0
      controllers/main.py
  5. 34 0
      doc/index.rst
  6. 13 0
      ir_attachment.py
  7. 344 0
      ir_qweb.py
  8. 653 0
      static/src/css/editor.css
  9. 565 0
      static/src/css/editor.sass
  10. 727 0
      static/src/css/snippets.css
  11. 590 0
      static/src/css/snippets.sass
  12. BIN
      static/src/img/theme/layout-boxed.gif
  13. BIN
      static/src/img/theme/layout-full.gif
  14. BIN
      static/src/img/theme/variant-amethyst.gif
  15. BIN
      static/src/img/theme/variant-cobalt.gif
  16. BIN
      static/src/img/theme/variant-emerald.gif
  17. BIN
      static/src/img/theme/variant-gold.gif
  18. BIN
      static/src/img/theme/variant-ruby.gif
  19. BIN
      static/src/img/theme/variant-stone.gif
  20. 118 0
      static/src/js/website.editor.js
  21. 80 0
      static/src/js/website.snippets.animation.js
  22. 1931 0
      static/src/js/website.snippets.editor.js
  23. 289 0
      static/src/js/website.theme.js
  24. 95 0
      static/src/less/colors.less
  25. 1 0
      static/src/less/import_bootstrap.less
  26. 77 0
      static/src/less/option_color_amethyst.less
  27. 74 0
      static/src/less/option_color_cobalt.less
  28. 73 0
      static/src/less/option_color_emerald.less
  29. 77 0
      static/src/less/option_color_gold.less
  30. 78 0
      static/src/less/option_color_ruby.less
  31. 76 0
      static/src/less/option_color_stone.less
  32. 2 0
      static/src/less/option_font.less
  33. 4 0
      static/src/less/option_layout_boxed.less
  34. 13 0
      static/src/less/website.less
  35. 294 0
      views/snippets.xml
  36. 268 0
      views/themes.xml
  37. 16 0
      views/website_backend_navbar.xml
  38. 45 0
      views/website_templates.xml
  39. 132 0
      website.py

+ 4 - 0
__init__.py

@@ -0,0 +1,4 @@
+import ir_qweb
+import ir_attachment
+import controllers
+import website

+ 22 - 0
__openerp__.py

@@ -0,0 +1,22 @@
+{
+    'name': 'Theme Support Engine',
+    'category': 'Website',
+    'summary': 'Support layer for Themes in 8.0',
+    'version': '1.1',
+    'description': """
+Support layer for themes in 8.0.
+
+This module requires `lessc` and its `clean-css` plugin to be installed on your system. Please refer to the `Less CSS via nodejs` section of https://www.odoo.com/documentation/8.0/setup/install.html#setup-install-source for installation instructions.
+        """,
+    'author': 'Odoo SA',
+    'depends': ['website'],
+    'data': [
+        'views/snippets.xml',
+        'views/themes.xml',
+        'views/website_templates.xml',
+        'views/website_backend_navbar.xml',
+    ],
+    'qweb': ['static/src/xml/website.backend.xml'],
+    'installable': True,
+    'application': False,
+}

+ 1 - 0
controllers/__init__.py

@@ -0,0 +1 @@
+import main

+ 149 - 0
controllers/main.py

@@ -0,0 +1,149 @@
+import werkzeug
+from openerp.http import request
+from openerp.addons.web import http
+from openerp.addons.website.controllers.main import Website
+from openerp.addons.base.ir.ir_qweb import QWebTemplateNotFound
+from openerp.addons.website_less.ir_qweb import AssetsBundle
+from openerp.addons.web.controllers.main import Home, make_conditional, BUNDLE_MAXAGE
+
+
+class Home_less(Home):
+
+    @http.route([
+        '/web/js/<xmlid>',
+        '/web/js/<xmlid>/<version>',
+    ], type='http', auth='public')
+    def js_bundle(self, xmlid, version=None, **kw):
+        try:
+            bundle = AssetsBundle(xmlid)
+        except QWebTemplateNotFound:
+            return request.not_found()
+
+        response = request.make_response(bundle.js(), [('Content-Type', 'application/javascript')])
+        return make_conditional(response, bundle.last_modified, max_age=BUNDLE_MAXAGE)
+
+    @http.route([
+        '/web/css/<xmlid>',
+        '/web/css/<xmlid>/<version>',
+    ], type='http', auth='public')
+    def css_bundle(self, xmlid, version=None, **kw):
+        try:
+            bundle = AssetsBundle(xmlid)
+        except QWebTemplateNotFound:
+            return request.not_found()
+
+        response = request.make_response(bundle.css(), [('Content-Type', 'text/css')])
+        return make_conditional(response, bundle.last_modified, max_age=BUNDLE_MAXAGE)
+
+
+class Website_less(Website):
+    #------------------------------------------------------
+    # Themes
+    #------------------------------------------------------
+
+    def get_view_ids(self, xml_ids):
+        ids = []
+        imd = request.registry['ir.model.data']
+        for xml_id in xml_ids:
+            if "." in xml_id:
+                xml = xml_id.split(".")
+                view_model, id = imd.get_object_reference(request.cr, request.uid, xml[0], xml[1])
+            else:
+                id = int(xml_id)
+            ids.append(id)
+        return ids
+
+    @http.route(['/website/theme_customize_get'], type='json', auth="public", website=True)
+    def theme_customize_get(self, xml_ids):
+        view = request.registry["ir.ui.view"]
+        enable = []
+        disable = []
+        ids = self.get_view_ids(xml_ids)
+        context = dict(request.context or {}, active_test=True)
+        for v in view.browse(request.cr, request.uid, ids, context=context):
+            if v.active:
+                enable.append(v.xml_id)
+            else:
+                disable.append(v.xml_id)
+        return [enable, disable]
+
+    @http.route(['/website/theme_customize'], type='json', auth="public", website=True)
+    def theme_customize(self, enable, disable):
+        """ enable or Disable lists of ``xml_id`` of the inherit templates
+        """
+        cr, uid, context, pool = request.cr, request.uid, request.context, request.registry
+        view = pool["ir.ui.view"]
+        context = dict(request.context or {}, active_test=True)
+
+        def set_active(ids, active):
+            if ids:
+                view.write(cr, uid, self.get_view_ids(ids), {'active': active}, context=context)
+
+        set_active(disable, False)
+        set_active(enable, True)
+
+        return True
+
+    @http.route(['/website/theme_customize_reload'], type='http', auth="public", website=True)
+    def theme_customize_reload(self, href, enable, disable):
+        self.theme_customize(enable and enable.split(",") or [], disable and disable.split(",") or [])
+        return request.redirect(href + ("&theme=true" if "#" in href else "#theme=true"))
+
+    @http.route(['/website/multi_render'], type='json', auth="public", website=True)
+    def multi_render(self, ids_or_xml_ids, values=None):
+        res = {}
+        for id_or_xml_id in ids_or_xml_ids:
+            res[id_or_xml_id] = request.registry["ir.ui.view"].render(request.cr, request.uid,
+                id_or_xml_id, values=values, engine='ir.qweb', context=request.context)
+        return res
+
+    @http.route([
+        '/website/image',
+        '/website/image/<xmlid>',
+        '/website/image/<xmlid>/<field>',
+        '/website/image/<xmlid>/<int:max_width>x<int:max_height>',
+        '/website/image/<xmlid>/<field>/<int:max_width>x<int:max_height>',
+        '/website/image/<model>/<id>/<field>',
+        '/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>',
+    ], auth="public", website=True)
+    def website_image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None):
+        """ Fetches the requested field and ensures it does not go above
+        (max_width, max_height), resizing it if necessary.
+
+        If the record is not found or does not have the requested field,
+        returns a placeholder image via :meth:`~.placeholder`.
+
+        Sets and checks conditional response parameters:
+        * :mailheader:`ETag` is always set (and checked)
+        * :mailheader:`Last-Modified is set iif the record has a concurrency
+          field (``__last_update``)
+
+        The requested field is assumed to be base64-encoded image data in
+        all cases.
+
+        xmlid can be used to load the image. But the field image must by base64-encoded
+        """
+        if xmlid and "." in xmlid:
+            try:
+                record = request.env['ir.model.data'].xmlid_to_object(xmlid)
+                model, id = record._name, record.id
+            except:
+                raise werkzeug.exceptions.NotFound()
+                env.ref
+            if model == 'ir.attachment' and not field:
+                if record.sudo().type == 'url':
+                    field = "url"
+                else:
+                    field = "datas"
+        elif model and id and field:
+            idsha = id.split('_')
+            try:
+                id = idsha[0]
+            except IndexError:
+                raise werkzeug.exceptions.NotFound()
+        else:
+            raise werkzeug.exceptions.NotFound()
+
+        response = werkzeug.wrappers.Response()
+        return request.registry['website']._image(
+                    request.cr, request.uid, model, id, field, response, max_width, max_height)

+ 34 - 0
doc/index.rst

@@ -0,0 +1,34 @@
+You need to install the *Less CSS* compiler to run this module
+
+* on Linux, use your distribution's package manager to install nodejs and npm.
+   * In debian wheezy and Ubuntu 13.10 and before you need to install nodejs manually:
+
+       .. code-block:: console
+
+           $ wget -qO- https://deb.nodesource.com/setup | bash -
+           $ apt-get install -y nodejs
+
+   * In later debian (>jessie) and ubuntu (>14.04) you may need to add a symlink as npm packages call ``node`` but debian calls the binary ``nodejs``
+
+       .. code-block:: console
+
+           $ apt-get install -y npm
+           $ sudo ln -s /usr/bin/nodejs /usr/bin/node
+
+   * Once npm is installed, use it to install less and less-plugin-clean-css:
+
+       .. code-block:: console
+
+           $ sudo npm install -g less less-plugin-clean-css
+
+* on OS X, install nodejs via your preferred package manager (`homebrew <http://brew.sh/>`_, `macports <https://www.macports.org/>`_) then install less and less-plugin-clean-css:
+
+   .. code-block:: console
+
+       $ sudo npm install -g less less-plugin-clean-css
+
+* on Windows, `install nodejs <http://nodejs.org/download/>`_, reboot (to update the `PATH`) and install less and less-plugin-clean-css:
+
+   .. code-block:: ps1
+
+       C:\> npm install -g less less-plugin-clean-css

+ 13 - 0
ir_attachment.py

@@ -0,0 +1,13 @@
+from openerp.osv import fields, osv
+
+
+class ir_attachment(osv.osv):
+    _inherit = "ir.attachment"
+
+    def invalidate_bundle(self, cr, uid, type='%', xmlid=None, context=None):
+        assert type in ('%', 'css', 'js'), "Unhandled bundle type"
+        xmlid = '%' if xmlid is None else xmlid + '%'
+        domain = [('url', '=like', '/web/%s/%s/%%' % (type, xmlid))]
+        ids = self.search(cr, uid, domain, context=context)
+        if ids:
+            self.unlink(cr, uid, ids, context=context)

+ 344 - 0
ir_qweb.py

@@ -0,0 +1,344 @@
+import os
+import re
+import logging
+from lxml import etree, html
+from subprocess import Popen, PIPE
+
+import openerp
+from openerp.osv import orm
+from openerp.tools import which
+from openerp.addons.base.ir.ir_qweb import AssetError, JavascriptAsset, QWebException
+
+_logger = logging.getLogger(__name__)
+
+
+class QWeb_less(orm.AbstractModel):
+    _name = "website.qweb"
+    _inherit = "website.qweb"
+
+    def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
+        """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
+        if len(element):
+            # An asset bundle is rendered in two differents contexts (when genereting html and
+            # when generating the bundle itself) so they must be qwebcontext free
+            # even '0' variable is forbidden
+            template = qwebcontext.get('__template__')
+            raise QWebException("t-call-assets cannot contain children nodes", template=template)
+        xmlid = template_attributes['call-assets']
+        cr, uid, context = [getattr(qwebcontext, attr) for attr in ('cr', 'uid', 'context')]
+        bundle = AssetsBundle(xmlid, cr=cr, uid=uid, context=context, registry=self.pool)
+        css = self.get_attr_bool(template_attributes.get('css'), default=True)
+        js = self.get_attr_bool(template_attributes.get('js'), default=True)
+        return bundle.to_html(css=css, js=js, debug=bool(qwebcontext.get('debug')))
+
+    def render_tag_snippet(self, element, template_attributes, generated_attributes, qwebcontext):
+        d = qwebcontext.copy()
+        d[0] = self.render_element(element, template_attributes, generated_attributes, d)
+        cr = d.get('request') and d['request'].cr or None
+        uid = d.get('request') and d['request'].uid or None
+
+        template = self.eval_format(template_attributes["snippet"], d)
+        document = self.render(cr, uid, template, d)
+        node = etree.fromstring(document)
+
+        id = int(node.get('data-oe-id', self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, template, raise_if_not_found=True)))
+        node.set('data-oe-name', self.pool['ir.ui.view'].browse(cr, uid, id, context=qwebcontext.context).name)
+        node.set('data-oe-type', "snippet")
+        node.set('data-oe-thumbnail', template_attributes.get('thumbnail', "oe-thumbnail"))
+
+        return html.tostring(node, encoding='UTF-8')
+
+
+class AssetsBundle(openerp.addons.base.ir.ir_qweb.AssetsBundle):
+    rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
+
+    def parse(self):
+        fragments = html.fragments_fromstring(self.html)
+        for el in fragments:
+            if isinstance(el, basestring):
+                self.remains.append(el)
+            elif isinstance(el, html.HtmlElement):
+                src = el.get('src', '')
+                href = el.get('href', '')
+                atype = el.get('type')
+                media = el.get('media')
+                if el.tag == 'style':
+                    if atype == 'text/sass' or src.endswith('.sass'):
+                        self.stylesheets.append(SassAsset(self, inline=el.text, media=media))
+                    elif atype == 'text/less' or src.endswith('.less'):
+                        self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
+                    else:
+                        self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
+                elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
+                    if href.endswith('.sass') or atype == 'text/sass':
+                        self.stylesheets.append(SassAsset(self, url=href, media=media))
+                    elif href.endswith('.less') or atype == 'text/less':
+                        self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
+                    else:
+                        self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
+                elif el.tag == 'script' and not src:
+                    self.javascripts.append(JavascriptAsset(self, inline=el.text))
+                elif el.tag == 'script' and self.can_aggregate(src):
+                    self.javascripts.append(JavascriptAsset(self, url=src))
+                else:
+                    self.remains.append(html.tostring(el))
+            else:
+                try:
+                    self.remains.append(html.tostring(el))
+                except Exception:
+                    # notYETimplementederror
+                    raise NotImplementedError
+
+    def to_html(self, sep=None, css=True, js=True, debug=False):
+        if sep is None:
+            sep = '\n            '
+        response = []
+        if debug:
+            if css and self.stylesheets:
+                self.preprocess_css()
+                if self.css_errors:
+                    msg = '\n'.join(self.css_errors)
+                    self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
+                for style in self.stylesheets:
+                    response.append(style.to_html())
+            if js:
+                for jscript in self.javascripts:
+                    response.append(jscript.to_html())
+        else:
+            url_for = self.context.get('url_for', lambda url: url)
+            if css and self.stylesheets:
+                href = '/web/css/%s/%s' % (self.xmlid, self.version)
+                response.append('<link href="%s" rel="stylesheet"/>' % url_for(href))
+            if js:
+                src = '/web/js/%s/%s' % (self.xmlid, self.version)
+                response.append('<script type="text/javascript" src="%s"></script>' % url_for(src))
+        response.extend(self.remains)
+        return sep + sep.join(response)
+
+    def css(self):
+        content = self.get_cache('css')
+        if content is None:
+            content = self.preprocess_css()
+
+            if self.css_errors:
+                msg = '\n'.join(self.css_errors)
+                content += self.css_message(msg)
+
+            # move up all @import rules to the top
+            matches = []
+
+            def push(matchobj):
+                matches.append(matchobj.group(0))
+                return ''
+
+            content = re.sub(self.rx_css_import, push, content)
+
+            matches.append(content)
+            content = u'\n'.join(matches)
+            if self.css_errors:
+                return content
+            self.set_cache('css', content)
+
+        return content
+
+    def set_cache(self, type, content):
+        ira = self.registry['ir.attachment']
+        ira.invalidate_bundle(self.cr, openerp.SUPERUSER_ID, type=type, xmlid=self.xmlid)
+        url = '/web/%s/%s/%s' % (type, self.xmlid, self.version)
+        ira.create(self.cr, openerp.SUPERUSER_ID, dict(
+                    datas=content.encode('utf8').encode('base64'),
+                    type='binary',
+                    name=url,
+                    url=url,
+                ), context=self.context)
+
+    def css_message(self, message):
+        # '\A' == css content carriage return
+        message = message.replace('\n', '\\A ').replace('"', '\\"')
+        return """
+            body:before {
+                background: #ffc;
+                width: 100%%;
+                font-size: 14px;
+                font-family: monospace;
+                white-space: pre;
+                content: "%s";
+            }
+        """ % message
+
+    def preprocess_css(self):
+        """
+            Checks if the bundle contains any sass/less content, then compiles it to css.
+            Returns the bundle's flat css.
+        """
+        for atype in (SassAsset, LessStylesheetAsset):
+            assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
+            if assets:
+                cmd = assets[0].get_command()
+                source = '\n'.join([asset.get_source() for asset in assets])
+                compiled = self.compile_css(cmd, source)
+
+                fragments = self.rx_css_split.split(compiled)
+                at_rules = fragments.pop(0)
+                if at_rules:
+                    # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
+                    self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
+                while fragments:
+                    asset_id = fragments.pop(0)
+                    asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
+                    asset._content = fragments.pop(0)
+
+        return '\n'.join(asset.minify() for asset in self.stylesheets)
+
+    def compile_sass(self):
+        self.preprocess_css()
+
+    def compile_css(self, cmd, source):
+        """Sanitizes @import rules, remove duplicates @import rules, then compile"""
+        imports = []
+
+        def sanitize(matchobj):
+            ref = matchobj.group(2)
+            line = '@import "%s"%s' % (ref, matchobj.group(3))
+            if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
+                imports.append(line)
+                return line
+            msg = "Local import '%s' is forbidden for security reasons." % ref
+            _logger.warning(msg)
+            self.css_errors.append(msg)
+            return ''
+        source = re.sub(self.rx_preprocess_imports, sanitize, source)
+
+        try:
+            compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+        except Exception:
+            msg = "Could not execute command %r" % cmd[0]
+            _logger.error(msg)
+            self.css_errors.append(msg)
+            return ''
+        result = compiler.communicate(input=source.encode('utf-8'))
+        if compiler.returncode:
+            error = self.get_preprocessor_error(''.join(result), source=source)
+            _logger.warning(error)
+            self.css_errors.append(error)
+            return ''
+        compiled = result[0].strip().decode('utf8')
+        return compiled
+
+    def get_preprocessor_error(self, stderr, source=None):
+        """Improve and remove sensitive information from sass/less compilator error messages"""
+        error = stderr.split('Load paths')[0].replace('  Use --trace for backtrace.', '')
+        if 'Cannot load compass' in error:
+            error += "Maybe you should install the compass gem using this extra argument:\n\n" \
+                     "    $ sudo gem install compass --pre\n"
+        error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
+        for asset in self.stylesheets:
+            if isinstance(asset, (SassAsset, LessStylesheetAsset)):
+                error += '\n    - %s' % (asset.url if asset.url else '<inline sass>')
+        return error
+
+
+class StylesheetAsset(openerp.addons.base.ir.ir_qweb.StylesheetAsset):
+
+    @property
+    def content(self):
+        if self._content is None:
+            self._content = self.inline or self._fetch_content()
+        return self._content
+
+    def _fetch_content(self):
+        try:
+            content = openerp.addons.base.ir.ir_qweb.WebAsset._fetch_content(self)
+            web_dir = os.path.dirname(self.url)
+
+            if self.rx_import:
+                content = self.rx_import.sub(
+                    r"""@import \1%s/""" % (web_dir,),
+                    content,
+                )
+
+            if self.rx_url:
+                content = self.rx_url.sub(
+                    r"url(\1%s/" % (web_dir,),
+                    content,
+                )
+
+            if self.rx_charset:
+                # remove charset declarations, we only support utf-8
+                content = self.rx_charset.sub('', content)
+
+            return content
+        except AssetError, e:
+            self.bundle.css_errors.append(e.message)
+            return ''
+
+
+class SassAsset(StylesheetAsset, openerp.addons.base.ir.ir_qweb.SassAsset):
+
+    def to_html(self):
+        if self.url:
+            ira = self.registry['ir.attachment']
+            url = self.html_url % self.url
+            domain = [('type', '=', 'binary'), ('url', '=', url)]
+            ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
+            datas = self.content.encode('utf8').encode('base64')
+            if ira_id:
+                # TODO: update only if needed
+                ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
+            else:
+                ira.create(self.cr, openerp.SUPERUSER_ID, dict(
+                    datas=datas,
+                    mimetype='text/css',
+                    type='binary',
+                    name=url,
+                    url=url,
+                ), context=self.context)
+        return super(SassAsset, self).to_html()
+
+    def get_command(self):
+        defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
+        sass = which('sass', path=os.pathsep.join(defpath))
+        return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
+                '-r', 'bootstrap-sass']
+
+
+class LessStylesheetAsset(StylesheetAsset):
+    html_url = '%s.css'
+    rx_import = None
+
+    def minify(self):
+        return self.with_header()
+
+    def to_html(self):
+        if self.url:
+            ira = self.registry['ir.attachment']
+            url = self.html_url % self.url
+            domain = [('type', '=', 'binary'), ('url', '=', url)]
+            ira_id = ira.search(self.cr, openerp.SUPERUSER_ID, domain, context=self.context)
+            datas = self.content.encode('utf8').encode('base64')
+            if ira_id:
+                # TODO: update only if needed
+                ira.write(self.cr, openerp.SUPERUSER_ID, ira_id, {'datas': datas}, context=self.context)
+            else:
+                ira.create(self.cr, openerp.SUPERUSER_ID, dict(
+                    datas=datas,
+                    mimetype='text/css',
+                    type='binary',
+                    name=url,
+                    url=url,
+                ), context=self.context)
+        return super(LessStylesheetAsset, self).to_html()
+
+    def get_source(self):
+        content = self.inline or self._fetch_content()
+        return "/*! %s */\n%s" % (self.id, content)
+
+    def get_command(self):
+        defpath = os.environ.get('PATH', os.defpath).split(os.pathsep)
+        if os.name == 'nt':
+            lessc = which('lessc.cmd', path=os.pathsep.join(defpath))
+        else:
+            lessc = which('lessc', path=os.pathsep.join(defpath))
+        webpath = openerp.http.addons_manifest['web']['addons_path']
+        lesspath = os.path.join(webpath, 'web', 'static', 'lib', 'bootstrap', 'less')
+        return [lessc, '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]

+ 653 - 0
static/src/css/editor.css

@@ -0,0 +1,653 @@
+@charset "UTF-8";
+/* ---- CKEditor Minimal Reset ---- {{{ */
+.navbar.navbar-inverse .cke_chrome {
+  border: none;
+}
+
+.navbar.navbar-inverse .cke_inner {
+  background: rgba(0, 0, 0, 0);
+}
+
+.navbar.navbar-inverse .cke_toolbar {
+  position: relative;
+  top: 1px;
+}
+.navbar.navbar-inverse .cke_combo_button {
+  padding-top: 3px;
+  padding-bottom: 3px;
+}
+.navbar.navbar-inverse .cke_button {
+  padding-top: 7px;
+  padding-bottom: 7px;
+}
+
+.navbar.navbar-inverse .cke_top {
+  background: rgba(0, 0, 0, 0);
+  border: none;
+  -moz-box-shadow: none;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+  -ms-filter: "alpha(opacity=50)";
+}
+
+#cke_1_top {
+  padding: 0;
+}
+
+#cke_wrapwrap {
+  -moz-box-shadow: none;
+  -webkit-box-shadow: none;
+  box-shadow: none;
+}
+#cke_wrapwrap .cke_button {
+  padding-top: 5px;
+  padding-bottom: 5px;
+}
+#cke_wrapwrap .cke_combo_button {
+  padding-top: 1px;
+  padding-bottom: 1px;
+}
+
+/* ---- OpenERP Style ---- {{{ */
+.oe_website_editorbar {
+  position: fixed;
+  top: 0;
+  right: 0;
+  display: block;
+  width: 100%;
+  padding: 2px;
+  margin: 0;
+  z-index: 20000;
+  background: #414141, url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzY0NjA2MCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzI2MjYyNiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
+  background: #414141, -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #646060), color-stop(100%, #262626));
+  background: #414141, -moz-linear-gradient(#646060, #262626);
+  background: #414141, -webkit-linear-gradient(#646060, #262626);
+  background: #414141, linear-gradient(#646060, #262626);
+  -moz-box-sizing: border-box;
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.oe_website_editorbar li {
+  display: inline;
+  color: #eee;
+}
+.oe_website_editorbar li:hover {
+  background: rgba(0, 0, 0, 0.2);
+  text-shadow: black 0px 0px 3px;
+  color: white;
+}
+
+.oe_website_editorbar .oe_rte_toolbar div.dropdown {
+  display: inline-block;
+}
+.oe_website_editorbar .oe_rte_toolbar div.dropdown li {
+  display: list-item;
+}
+.oe_website_editorbar .oe_rte_toolbar button {
+  font-family: FontAwesome;
+  font-weight: normal;
+  font-style: normal;
+  text-decoration: inherit;
+}
+.oe_website_editorbar .oe_rte_toolbar button.oe_button_list {
+  padding-right: 3px;
+}
+.oe_website_editorbar .oe_rte_toolbar button.oe_button_list:after {
+  content: "";
+  padding-left: 6px;
+}
+
+.oe_editable:focus {
+  outline: none !important;
+}
+
+.css_editable_display {
+  display: block !important;
+}
+
+.css_editable_hidden {
+  display: none !important;
+}
+
+.cke_editable .css_editable_mode_hidden {
+  display: none;
+}
+
+.cke_editable .css_editable_mode_display {
+  display: block !important;
+}
+
+.oe_structure.oe_empty:empty, [data-oe-type=html]:empty, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child {
+  background-image: url("/website/static/src/img/edit_here.png") !important;
+}
+
+.oe_structure.oe_empty:empty:before, [data-oe-type=html]:empty:before, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child:before, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child:before {
+  content: "Press The Top-Left Edit Button" !important;
+}
+
+[data-oe-type=html].oe_no_empty:empty:before {
+  content: "" !important;
+}
+
+[data-oe-type=html].oe_no_empty:empty {
+  background-image: none !important;
+  height: 16px !important;
+}
+
+#website-top-edit {
+  width: 100%;
+}
+#website-top-edit > ul > li {
+  margin: 0;
+}
+
+#website-top-navbar {
+  min-height: 34px;
+  height: 34px;
+}
+#website-top-navbar form {
+  margin: 0;
+}
+#website-top-navbar form button, #website-top-navbar form a {
+  padding: 4px 8px 4px 8px;
+  margin-top: 2px;
+  font-size: 13px;
+}
+
+/* ---- EDITOR BAR ---- {{{ */
+table.editorbar-panel {
+  cursor: pointer;
+  width: 100%;
+}
+table.editorbar-panel td {
+  border: 1px solid #aaa;
+}
+table.editorbar-panel td.selected {
+  background-color: #b1c9d9;
+}
+
+.link-style .dropdown > .btn {
+  min-width: 160px;
+}
+.link-style .link-style {
+  display: none;
+}
+.link-style li {
+  text-align: center;
+}
+.link-style li label {
+  width: 100px;
+  margin: 0 5px;
+}
+.link-style .col-sm-2 > * {
+  line-height: 2em;
+}
+
+/* ---- TRANSLATIONS ---- {{{ */
+.oe_translate_or {
+  color: white;
+  padding: 0 0 0 1em;
+}
+
+.oe_translate_examples li {
+  margin: 10px;
+  padding: 4px;
+}
+
+.oe_translatable_text {
+  outline: 1px solid black;
+}
+
+.oe_translatable_field {
+  outline: 1px dashed black;
+}
+
+.oe_translatable_text.oe_dirty, .oe_translatable_field.oe_dirty {
+  outline-color: red;
+}
+
+.oe_translatable_text.oe_dirty:empty {
+  padding: 0 10px;
+}
+
+.oe_translatable_todo {
+  background: #ffffb6;
+}
+
+/* ---- MENU ---- {{{ */
+div.oe_menu_buttons {
+  top: -8px;
+  right: -8px;
+}
+
+ul.oe_menu_editor .fa-home {
+  display: none;
+}
+ul.oe_menu_editor > li:first-child > div > span > .fa-home {
+  display: block;
+}
+ul.oe_menu_editor .oe_menu_placeholder {
+  outline: 1px dashed #4183C4;
+}
+ul.oe_menu_editor ul {
+  list-style: none;
+}
+ul.oe_menu_editor li div {
+  cursor: move;
+}
+ul.oe_menu_editor .disclose {
+  cursor: pointer;
+  width: 10px;
+  display: none;
+}
+
+/* ---- RTE ---- {{{ */
+.oe_editable .btn, .btn.oe_editable {
+  -moz-user-select: auto;
+  -ms-user-select: auto;
+  -webkit-user-select: auto;
+  user-select: auto;
+  cursor: text !important;
+}
+
+.modal-dialog.select-media {
+  width: 80%;
+}
+
+.modal .existing-attachments .pager {
+  margin: 0;
+}
+
+.modal .image-preview {
+  margin-bottom: 0.5em;
+}
+
+.modal-footer {
+  text-align: left;
+}
+
+.modal.nosave .wait {
+  display: inline-block !important;
+  visibility: visible !important;
+}
+.modal.nosave .modal-body .filepicker, .modal.nosave .modal-body .image-preview {
+  display: none;
+}
+.modal.nosave .modal-body .wait {
+  width: 100%;
+}
+.modal.nosave .modal-footer .save {
+  display: none;
+}
+
+.modal .font-icons-icons {
+  font-size: 2em;
+  max-height: 9em;
+  overflow: auto;
+}
+.modal .font-icons-icons .font-icons-icon {
+  display: inline-block;
+  width: 2em;
+  padding: 0.25em;
+  text-align: center;
+  cursor: pointer;
+}
+.modal .font-icons {
+  position: relative;
+  display: block;
+}
+.modal .font-icons:before {
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=70);
+  opacity: 0.7;
+  position: absolute;
+  top: 2px;
+  left: 3px;
+  font-size: 2em;
+}
+.modal #icon-search {
+  padding-left: 2.5em;
+}
+.modal #fa-preview {
+  text-align: center;
+}
+.modal #fa-preview span {
+  cursor: pointer;
+  padding: 0 15px;
+}
+.modal #fa-preview .font-icons-selected {
+  background-color: #ddd;
+}
+
+.existing-attachments .pager .disabled {
+  display: none;
+}
+.existing-attachments .existing-attachment-cell {
+  position: relative;
+}
+.existing-attachments .existing-attachment-cell .img {
+  border: 1px solid #848490;
+}
+.existing-attachments .existing-attachment-cell .existing-attachment-remove {
+  position: absolute;
+  top: 0;
+  left: 15px;
+  cursor: pointer;
+  background: white;
+  padding: 2px;
+  border: 1px solid #848490;
+  border-top: none;
+  border-left: none;
+  -moz-border-radius-bottomright: 8px;
+  -webkit-border-bottom-right-radius: 8px;
+  border-bottom-right-radius: 8px;
+}
+.existing-attachments .existing-attachment-cell.media_selected > i, .existing-attachments .existing-attachment-cell.media_selected > img {
+  border-width: 5px;
+  border-color: #00f8f8;
+}
+
+.cke_widget_wrapper {
+  position: static !important;
+}
+
+.cke_widget_inline {
+  display: inline !important;
+}
+
+.cke_widget_editable:empty:after {
+  opacity: 0.3;
+  white-space: pre-wrap;
+}
+.cke_widget_editable:not([placeholder]):empty::after {
+  content: " ";
+}
+.cke_widget_editable[placeholder]:not(:focus):empty::after {
+  content: attr(placeholder);
+}
+
+.oe_carlos_danger {
+  outline: 1px solid red !important;
+  background-color: #ffd9dd !important;
+}
+
+.hover-edition {
+  display: inline-block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 1001;
+}
+
+.preview-container {
+  text-align: center;
+  line-height: 100px;
+  height: 100px;
+}
+.preview-container > * {
+  max-height: 100px;
+  line-height: 100px;
+  margin: 0 auto;
+  display: inline-block;
+}
+
+.cke_editable .fa {
+  cursor: pointer;
+}
+
+.img-responsive {
+  text-align: center;
+}
+
+/* ---- MOBILE PREVIEW ---- {{{ */
+.oe_mobile_preview.modal .modal-content {
+  height: 660px;
+  background-color: #000000;
+  border: 2px solid #1C1F1F;
+  -moz-border-radius: 10px;
+  -webkit-border-radius: 10px;
+  border-radius: 10px;
+  margin: auto;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  max-width: 330px;
+}
+.oe_mobile_preview.modal .modal-content .modal-header {
+  background-color: #000000;
+  border-bottom: 0;
+  -moz-border-radius-topleft: 10px;
+  -webkit-border-top-left-radius: 10px;
+  border-top-left-radius: 10px;
+  -moz-border-radius-topright: 10px;
+  -webkit-border-top-right-radius: 10px;
+  border-top-right-radius: 10px;
+}
+.oe_mobile_preview.modal .modal-content .modal-header .modal-title {
+  color: #1C1F1F;
+}
+.oe_mobile_preview.modal .modal-content .modal-header .close {
+  color: lightgrey;
+  filter: progid:DXImageTransform.Microsoft.Alpha(enabled=false);
+  opacity: 1;
+}
+.oe_mobile_preview.modal .modal-content .modal-header .close:hover {
+  color: #E00101;
+  filter: progid:DXImageTransform.Microsoft.Alpha(enabled=false);
+  opacity: 1;
+}
+.oe_mobile_preview.modal .modal-content .modal-body {
+  background-color: #000000;
+  max-height: 600px;
+  padding: 0;
+  margin: 0;
+}
+.oe_mobile_preview.modal .modal-content .modal-body .oe_mobile_viewport {
+  width: 320px;
+  height: 568px;
+  padding: 5px;
+  border: none;
+}
+.oe_mobile_preview.modal .modal-content .modal-footer {
+  background-color: #000000;
+}
+
+/* ---- SEO TOOLS ---- {{{ */
+.oe_seo_configuration .modal-dialog {
+  width: 80%;
+}
+.oe_seo_configuration .oe_remove {
+  color: #E00101;
+}
+.oe_seo_configuration .oe_seo_suggestion {
+  cursor: pointer;
+}
+.oe_seo_configuration .oe_seo_keyword {
+  padding: 0.2em 0.4em 0.2em 0.5em;
+  -moz-border-radius: 0.4em;
+  -webkit-border-radius: 0.4em;
+  border-radius: 0.4em;
+}
+.oe_seo_configuration li.oe_seo_preview_g {
+  line-height: 1.2;
+  list-style: none;
+  list-style-image: none;
+  list-style-position: outside;
+  list-style-type: none;
+  font-size: small;
+  font-family: arial, sans-serif;
+}
+.oe_seo_configuration li.oe_seo_preview_g h3 {
+  font-size: medium;
+}
+.oe_seo_configuration li.oe_seo_preview_g .r {
+  margin: 0;
+  font-size: 16px;
+  font-style: normal;
+  font-weight: normal;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.oe_seo_configuration li.oe_seo_preview_g .r a {
+  color: #1e0fbe;
+  text-decoration: underline;
+  text-transform: none;
+}
+.oe_seo_configuration li.oe_seo_preview_g .r a em {
+  font-style: normal !important;
+}
+.oe_seo_configuration li.oe_seo_preview_g .s {
+  color: #444;
+  max-width: 42em;
+}
+.oe_seo_configuration li.oe_seo_preview_g .kv, .oe_seo_configuration li.oe_seo_preview_g .slp {
+  display: block;
+  margin-bottom: 1px;
+}
+.oe_seo_configuration li.oe_seo_preview_g .f {
+  color: #666;
+  margin-bottom: 1px;
+}
+.oe_seo_configuration li.oe_seo_preview_g .f cite {
+  color: #006621;
+  font-style: normal;
+  font-size: 14px;
+}
+.oe_seo_configuration li.oe_seo_preview_g .st {
+  line-height: 1.24;
+}
+
+/* ---- ACE EDITOR ---- {{{ */
+.oe_ace_view_editor {
+  position: fixed;
+  right: 0;
+  z-index: 1001;
+  height: 100%;
+  background: #2F3129;
+  color: white;
+}
+.oe_ace_view_editor .oe_ace_view_editor_title {
+  width: 100%;
+  padding-top: 0;
+  padding-left: 0;
+  height: 30px;
+}
+.oe_ace_view_editor .oe_ace_view_editor_title .oe_view_list {
+  width: 50%;
+  height: 30px;
+  font-size: 14px;
+  font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace;
+  line-height: normal;
+}
+.oe_ace_view_editor .oe_ace_view_editor_title .btn {
+  height: 30px;
+  padding: 0 4px 0 4px;
+  font-size: 14px;
+  font-family: "Monaco", "Menlo", "Ubuntu Mono", "Consolas", "source-code-pro", monospace;
+  line-height: normal;
+}
+.oe_ace_view_editor .ace_editor {
+  position: absolute;
+  top: 50px;
+  right: 0;
+  left: 0;
+}
+.oe_ace_view_editor .ace_editor .ace_gutter {
+  cursor: ew-resize;
+}
+.oe_ace_view_editor #ace-view-id {
+  padding: 0 1em;
+}
+.oe_ace_view_editor.oe_ace_open {
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=97);
+  opacity: 0.97;
+}
+.oe_ace_view_editor.oe_ace_closed {
+  z-index: -1000;
+  filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=0);
+  opacity: 0;
+}
+
+/* ---- CUSTOMIZE THEME ---- {{{ */
+#theme_error {
+  background: #ffc;
+}
+
+#theme_customize_modal {
+  overflow: visible;
+  overflow-y: auto;
+  z-index: 1020;
+  background: rgba(0, 0, 0, 0);
+  display: block;
+}
+#theme_customize_modal .modal-dialog {
+  top: 75px;
+  width: auto;
+  margin: 0;
+  position: absolute;
+  right: 10px;
+  font-family: "Lato-Regular";
+  font-weight: normal;
+  text-transform: capitalize;
+  letter-spacing: normal;
+}
+#theme_customize_modal .modal-h5 {
+  color: #ffffff;
+  font-family: "Lato-Regular";
+  font-weight: normal;
+  text-transform: uppercase;
+  letter-spacing: normal;
+  font-size: 14px;
+  color: white;
+  padding: 4px 0 4px 4px;
+  background-color: #bdc3c7;
+}
+#theme_customize_modal table {
+  width: 100%;
+  margin-bottom: 8px;
+}
+#theme_customize_modal label {
+  display: block;
+  text-align: center;
+}
+#theme_customize_modal label > div, #theme_customize_modal label > img {
+  border: 1px solid #fff;
+  line-height: 30px;
+  font-size: 0.9em;
+  margin: 2px 4px;
+}
+#theme_customize_modal label.checked > div, #theme_customize_modal label.checked > img {
+  box-shadow: 2px 2px 3px #888;
+  border: 1px solid #666;
+}
+#theme_customize_modal label img {
+  width: 60px;
+  height: 35px;
+  margin: 2px;
+  border: 1px solid rgba(136, 136, 136, 0.5);
+}
+#theme_customize_modal label input {
+  display: none;
+}
+#theme_customize_modal .loading_backdrop {
+  display: none;
+}
+#theme_customize_modal.loading .loading_backdrop {
+  display: block;
+  width: 100%;
+  height: 100%;
+  background: #000;
+  opacity: 0.3;
+  position: absolute;
+  z-index: 1;
+}
+
+/* ---- force the browse to re-compute the stylesheets ---- */
+body.theme_customize_css_loading {
+  magin-top: 1px;
+}
+body.theme_customize_css_loading #wrapwrap {
+  magin-top: -1px;
+}

+ 565 - 0
static/src/css/editor.sass

@@ -0,0 +1,565 @@
+@import "common"
+
+/* ---- CKEditor Minimal Reset ---- {{{ */
+
+.navbar.navbar-inverse .cke_chrome
+    border: none
+
+.navbar.navbar-inverse .cke_inner
+    background: rgba(0, 0, 0, 0)
+
+.navbar.navbar-inverse
+    .cke_toolbar
+        position: relative
+        top: 1px
+    .cke_combo_button
+        padding-top: 3px
+        padding-bottom: 3px
+    .cke_button
+        padding-top: 7px
+        padding-bottom: 7px
+
+.navbar.navbar-inverse .cke_top
+    background: rgba(0, 0, 0, 0)
+    border: none
+    +box-shadow(none)
+    -ms-filter: "alpha(opacity=50)"
+
+#cke_1_top
+    padding: 0
+
+#cke_wrapwrap
+    -moz-box-shadow: none
+    -webkit-box-shadow: none
+    box-shadow: none
+    .cke_button
+        padding-top: 5px
+        padding-bottom: 5px
+    .cke_combo_button
+        padding-top: 1px
+        padding-bottom: 1px
+
+// }}}
+ 
+/* ---- OpenERP Style ---- {{{ */
+
+.oe_website_editorbar
+    position: fixed
+    top: 0
+    right: 0
+    display: block
+    width: 100%
+    padding: 2px
+    margin: 0
+    z-index: 20000
+    @include background(#414141, linear-gradient(#646060, #262626))
+    +box-sizing(border-box)
+    li
+        display: inline
+        color: #eee
+        &:hover
+            background: rgba(0,0,0,0.2)
+            +text-shadow(black 0px 0px 3px)
+            color: white
+
+.oe_website_editorbar .oe_rte_toolbar
+    div.dropdown
+        display: inline-block
+        li
+            display: list-item
+
+    button
+        font-family: FontAwesome
+        font-weight: normal
+        font-style: normal
+        text-decoration: inherit
+        &.oe_button_list
+            padding-right: 3px
+            &:after
+                content: "\F0D7"
+                padding-left: 6px
+
+.oe_editable:focus
+    outline: none !important
+
+.css_editable_display
+    display: block !important
+
+.css_editable_hidden
+    display: none !important
+
+.cke_editable .css_editable_mode_hidden
+    display: none
+
+.cke_editable .css_editable_mode_display
+    display: block !important
+
+.oe_structure.oe_empty:empty, [data-oe-type=html]:empty, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child
+    background-image: url('/website/static/src/img/edit_here.png') !important
+
+.oe_structure.oe_empty:empty:before, [data-oe-type=html]:empty:before, .oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child:before, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child:before
+    content: 'Press The Top-Left Edit Button' !important
+
+[data-oe-type=html].oe_no_empty:empty:before
+    content: '' !important
+
+[data-oe-type=html].oe_no_empty:empty
+    background-image: none !important
+    height: 16px !important
+
+#website-top-edit
+    width: 100%
+    > ul > li
+        margin: 0
+
+#website-top-navbar
+    min-height: 34px
+    height: 34px
+    form
+        margin: 0
+        button, a
+            padding: 4px 8px 4px 8px
+            margin-top: 2px
+            font-size: 13px
+
+// }}}
+
+/* ---- EDITOR BAR ---- {{{ */
+
+table.editorbar-panel
+    cursor: pointer
+    width: 100%
+    td
+        border: 1px solid #aaa
+    td.selected
+        background-color: #b1c9d9
+
+.link-style
+    .dropdown > .btn
+        min-width: 160px
+    .link-style
+        display: none
+    li
+        text-align: center
+        label
+            width: 100px
+            margin: 0 5px
+    .col-sm-2 > *
+        line-height: 2em
+
+// }}}
+
+/* ---- TRANSLATIONS ---- {{{ */
+
+.oe_translate_or
+    color: white
+    padding: 0 0 0 1em
+.oe_translate_examples li
+    margin: 10px
+    padding: 4px
+.oe_translatable_text
+    outline: 1px solid black
+.oe_translatable_field
+    outline: 1px dashed black
+.oe_translatable_text.oe_dirty, .oe_translatable_field.oe_dirty
+    outline-color: red
+.oe_translatable_text.oe_dirty:empty
+    padding: 0 10px
+.oe_translatable_todo
+    background: rgb(255, 255, 182)
+
+// }}}
+
+/* ---- MENU ---- {{{ */
+
+div.oe_menu_buttons
+    top: -8px
+    right: -8px
+
+ul.oe_menu_editor
+    .fa-home
+        display: none
+    > li:first-child > div > span > .fa-home
+        display: block
+    .oe_menu_placeholder
+        outline: 1px dashed #4183C4
+    ul
+        list-style: none
+    li div
+        cursor: move
+    .disclose
+        cursor: pointer
+        width: 10px
+        display: none
+
+// }}}
+
+/* ---- RTE ---- {{{ */
+
+// bootstrap makes .btn elements unselectable -> RTE double-click can't know
+// about them either
+.oe_editable .btn, .btn.oe_editable
+    +user-select(auto)
+    cursor: text !important
+
+.modal-dialog.select-media
+    width: 80%
+
+.modal .existing-attachments
+    .pager
+        margin: 0
+
+.modal .image-preview
+    margin-bottom: 0.5em
+
+.modal-footer
+    text-align: left
+
+.modal.nosave
+    .wait
+        display: inline-block !important
+        visibility: visible !important
+    .modal-body
+        .filepicker, .image-preview
+            display: none
+        .wait
+            width: 100%
+    .modal-footer .save
+        display: none
+
+// fontawesome modal
+.modal
+    .font-icons-icons
+        font-size: 2em
+        max-height: 9em
+        overflow: auto
+
+        .font-icons-icon
+            display: inline-block
+            width: 2em
+            padding: 0.25em
+            text-align: center
+            cursor: pointer
+
+    .font-icons
+        position: relative
+        display: block
+
+        &:before
+            +opacity(0.7)
+            position: absolute
+            top: 2px
+            left: 3px
+            font-size: 2em
+    #icon-search
+        padding-left: 2.5em
+
+    #fa-preview
+        text-align: center
+
+        span
+            cursor: pointer
+            padding: 0 15px
+        .font-icons-selected
+            background-color: #ddd
+
+$attachment-border-color: #848490
+.existing-attachments
+    .pager .disabled
+        display: none
+
+    .existing-attachment-cell
+        position: relative
+        .img
+            border: 1px solid $attachment-border-color
+        .existing-attachment-remove
+            position: absolute
+            top: 0
+            left: 15px // padding-left on col-*
+
+            cursor: pointer
+            background: white
+            padding: 2px
+            border: 1px solid $attachment-border-color
+            border-top: none
+            border-left: none
+            +border-bottom-right-radius(8px)
+        &.media_selected
+            > i, > img 
+                border-width: 5px
+                border-color: rgb(0, 248, 248)
+
+// wrapper positioned relatively for drag&drop widget which is disabled below.
+// Breaks completely horribly crazy products listing page, so take it out.
+.cke_widget_wrapper
+    position: static !important
+
+.cke_widget_inline
+    display: inline !important
+
+// prevent inline widgets from entirely disappearing when their (textual)
+// content is removed
+.cke_widget_editable
+    // basic config
+    &:empty:after
+        opacity: 0.3
+        white-space: pre-wrap
+    // no @placeholder -> just add some padding
+    &:not([placeholder]):empty::after
+        content: " "
+    // with placeholder and when not (yet) focused -> display placeholder
+    &[placeholder]:not(:focus):empty::after
+        content: attr(placeholder)
+
+.oe_carlos_danger
+    outline: 1px solid red !important
+    background-color: #ffd9dd !important
+
+.hover-edition
+    display: inline-block
+    position: absolute
+    top: 0
+    left: 0
+    // This z-index is due to .navbar of bootstrap & jQuery-transfo
+    z-index: 1001
+.preview-container
+    text-align: center
+    line-height: 100px
+    height: 100px
+    > *
+        max-height: 100px
+        line-height: 100px
+        margin: 0 auto
+        display: inline-block
+
+// fontawesome
+.cke_editable .fa
+    cursor: pointer
+
+.img-responsive
+    text-align: center
+
+// }}}
+
+/* ---- MOBILE PREVIEW ---- {{{ */
+
+$mobile_preview_background: #000000
+$mobile_preview_border: #1C1F1F
+$icon_close: #E00101
+.oe_mobile_preview
+    &.modal .modal-content
+        height: 660px
+        background-color: $mobile_preview_background
+        border: 2px solid $mobile_preview_border
+        +border-radius(10px)
+        margin: auto
+        top: 0
+        left: 0
+        bottom: 0
+        right: 0
+        max-width: 330px
+        .modal-header
+            background-color: $mobile_preview_background
+            border-bottom: 0
+            +border-top-left-radius(10px)
+            +border-top-right-radius(10px)
+            .modal-title
+                color: $mobile_preview_border
+            .close
+                color: lightgrey
+                +opacity(1)
+            .close:hover
+                color: $icon_close
+                +opacity(1)
+        .modal-body
+            background-color: $mobile_preview_background
+            max-height: 600px
+            padding: 0
+            margin: 0
+            .oe_mobile_viewport
+                width: 320px
+                height: 568px
+                padding: 5px
+                border: none
+        .modal-footer
+            background-color: $mobile_preview_background
+
+// }}}
+
+/* ---- SEO TOOLS ---- {{{ */
+
+$remove_color: $icon_close
+$in_title_color: #5cb85c
+$in_description_color: #428bca
+$in_body_color: #5bc0de
+$highlighted_text_color: #ffffff
+.oe_seo_configuration
+    .modal-dialog
+        width: 80%
+    .oe_remove
+        color: $remove_color
+    .oe_seo_suggestion
+        cursor: pointer
+    .oe_seo_keyword
+        padding: .2em .4em .2em .5em
+        +border-radius(.4em)
+    li.oe_seo_preview_g
+        line-height: 1.2
+        list-style: none
+        list-style-image: none
+        list-style-position: outside
+        list-style-type: none
+        font-size: small
+        font-family: arial,sans-serif
+        h3
+            font-size: medium
+        .r
+            margin: 0
+            font-size: 16px
+            font-style: normal
+            font-weight: normal
+            overflow: hidden
+            text-overflow: ellipsis
+            -webkit-text-overflow: ellipsis
+            white-space: nowrap
+            a
+                color: rgb(30, 15, 190)
+                text-decoration: underline
+                text-transform: none
+                em
+                    font-style: normal !important
+        .s
+            color: #444
+            max-width: 42em
+        .kv,.slp
+            display: block
+            margin-bottom: 1px
+        .f
+            color: #666
+            margin-bottom: 1px
+            cite
+                color: #006621
+                font-style: normal
+                font-size: 14px
+        .st
+            line-height: 1.24
+
+
+// }}}
+
+/* ---- ACE EDITOR ---- {{{ */
+
+$editorbar_height: 30px
+$infobar_height: 20px
+// TODO Fix => might break with themes
+
+.oe_ace_view_editor
+    position: fixed
+    // top property is set programmatically
+    right: 0
+    z-index: 1001
+    height: 100%
+    background: #2F3129
+    color: white
+    .oe_ace_view_editor_title
+        width: 100%
+        padding-top: 0
+        padding-left: 0
+        height: $editorbar_height
+        .oe_view_list
+            width: 50%
+            height: $editorbar_height
+            @include editor-font
+        .btn
+            height: $editorbar_height
+            padding: 0 4px 0 4px
+            @include editor-font
+    .ace_editor
+        position: absolute
+        top: $editorbar_height + $infobar_height
+        right: 0
+        // bottom property is set programmatically
+        left: 0
+        .ace_gutter
+            cursor: ew-resize
+    #ace-view-id
+        padding: 0 1em
+    &.oe_ace_open
+        +opacity(0.97)
+    &.oe_ace_closed
+        z-index: -1000
+        +opacity(0)
+
+// }}}
+
+/* ---- CUSTOMIZE THEME ---- {{{ */
+#theme_error
+    background: #ffc
+#theme_customize_modal
+    overflow: visible
+    z-index: 1020
+    background: rgba(0, 0, 0, 0)
+    display: block
+    .modal-dialog
+        top: 75px
+        width: auto
+        margin: 0
+        position: absolute
+        right: 10px
+        font-family: 'Lato-Regular'
+        font-weight: normal
+        text-transform: capitalize
+        letter-spacing: normal
+    .modal-h5
+        color: #ffffff
+        font-family: 'Lato-Regular'
+        font-weight: normal
+        text-transform: uppercase
+        letter-spacing: normal
+        font-size: 14px
+        color: white
+        padding: 4px 0 4px 4px
+        background-color: #bdc3c7
+    table
+        width: 100%
+        margin-bottom: 8px
+    label
+        display: block
+        text-align: center
+        > div, > img
+            border: 1px solid #fff
+            line-height: 30px
+            font-size: 0.9em
+            margin: 2px 4px
+        &.checked
+            > div, > img
+                box-shadow: 2px 2px 3px #888
+                border: 1px solid #666
+        img
+            width: 60px
+            height: 35px
+            margin: 2px
+            border: 1px solid rgba(136, 136, 136, 0.5)
+        input
+            display: none
+    .loading_backdrop
+        display: none
+    &.loading
+        .loading_backdrop
+            display: block
+            width: 100%
+            height: 100%
+            background: #000
+            opacity: 0.3
+            position: absolute
+            z-index: 1
+
+/* ---- force the browse to re-compute the stylesheets ---- */
+body.theme_customize_css_loading
+    magin-top: 1px
+    #wrapwrap
+        magin-top: -1px
+
+// }}}
+
+// vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker:

+ 727 - 0
static/src/css/snippets.css

@@ -0,0 +1,727 @@
+/* ---- SNIPPET EDITOR ---- {{{ */
+#oe_snippets {
+  position: fixed;
+  top: 34px;
+  left: 0px;
+  right: 0px;
+  padding-top: 6px;
+  background: #282828;
+  -webkit-box-shadow: 0px 10px 10px -10px black inset;
+  -moz-box-shadow: 0px 10px 10px -10px black inset;
+  box-shadow: 0px 10px 10px -10px black inset;
+  z-index: 1010;
+  overflow: hidden;
+}
+#oe_snippets:hover {
+  height: auto;
+}
+#oe_snippets .scroll {
+  white-space: nowrap;
+  overflow-y: none;
+}
+#oe_snippets .nav {
+  display: inline-block;
+  border-bottom: none !important;
+  vertical-align: middle;
+  min-width: 120px;
+  z-index: 1;
+}
+#oe_snippets .nav > li {
+  display: block;
+  float: none;
+}
+#oe_snippets .nav > li.active {
+  background: black !important;
+}
+#oe_snippets .nav > li > a {
+  padding: 2px 10px !important;
+  width: 100%;
+  display: block;
+  border: 0;
+}
+#oe_snippets .pill-content {
+  border: 0;
+}
+#oe_snippets .tab-content {
+  margin-right: 120px;
+  display: inline-block;
+  white-space: normal;
+  background: black;
+}
+#oe_snippets .tab-content > div {
+  background: black;
+}
+#oe_snippets .tab-content > div label {
+  width: 44px;
+  color: white;
+  padding-left: 10px;
+}
+#oe_snippets .tab-content > div label div {
+  width: 100px;
+  text-align: center;
+  -webkit-transform: translate(-39px, 44px);
+  -moz-transform: translate(-39px, 44px);
+  -ms-transform: translate(-39px, 44px);
+  -o-transform: translate(-39px, 44px);
+  transform: translate(-39px, 44px);
+  -webkit-transform-origin: 50% 50% 50%;
+  -moz-transform-origin: 50% 50% 50%;
+  -ms-transform-origin: 50% 50% 50%;
+  -o-transform-origin: 50% 50% 50%;
+  transform-origin: 50% 50% 50%;
+}
+
+.oe_snippet {
+  display: inline-block;
+  vertical-align: top;
+  width: 93px;
+  margin-left: 1px;
+  margin-top: 0px;
+  position: relative;
+  overflow: hidden;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  cursor: move;
+}
+.oe_snippet .oe_snippet_thumbnail {
+  text-align: center;
+  height: 100%;
+  background: rgba(0, 0, 0, 0);
+  color: white;
+  position: relative;
+}
+.oe_snippet .oe_snippet_thumbnail:hover .oe_snippet_thumbnail_img {
+  -webkit-transform: scale(0.95, 0.95);
+  -moz-transform: scale(0.95, 0.95);
+  -ms-transform: scale(0.95, 0.95);
+  -o-transform: scale(0.95, 0.95);
+  transform: scale(0.95, 0.95);
+}
+.oe_snippet .oe_snippet_thumbnail .oe_snippet_thumbnail_title {
+  font-size: 12px;
+  display: block;
+  text-shadow: 0 0 2px black;
+}
+.oe_snippet .oe_snippet_thumbnail .oe_snippet_thumbnail_img {
+  height: 74px;
+  background-size: cover;
+  margin-bottom: 5px;
+  -webkit-transform: scale(1, 1);
+  -moz-transform: scale(1, 1);
+  -ms-transform: scale(1, 1);
+  -o-transform: scale(1, 1);
+  transform: scale(1, 1);
+}
+.oe_snippet .oe_snippet_thumbnail span, .oe_snippet .oe_snippet_thumbnail div {
+  line-height: 18px;
+}
+.oe_snippet > :not(.oe_snippet_thumbnail) {
+  display: none !important;
+}
+
+#oe_snippets .oe_snippet_thumbnail {
+  background: #747474, -webkit-gradient(radial, 50% 50%, 0, 50% 50%, 100, color-stop(0%, rgba(0, 0, 0, 0.25)), color-stop(100%, rgba(0, 0, 0, 0.4)));
+  background: #747474, -webkit-radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+  background: #747474, -moz-radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+  background: #747474, -o-radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+  background: #747474, radial-gradient(rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.4));
+}
+
+/* ---- SNIPPETS DROP ZONES ---- {{{ */
+.oe_drop_zone.oe_insert {
+  display: block;
+  height: 48px;
+  margin: 0px;
+  margin-top: -4px;
+  margin-bottom: -44px;
+  -webkit-transition: margin 250ms linear;
+  -moz-transition: margin 250ms linear;
+  -o-transition: margin 250ms linear;
+  transition: margin 250ms linear;
+  width: 100%;
+  position: absolute;
+  z-index: 1000;
+}
+.oe_drop_zone.oe_insert:not(.oe_vertical):before {
+  content: "";
+  display: block;
+  border-top: dashed 2px rgba(209, 178, 255, 0.72);
+  position: relative;
+  top: 0px;
+}
+.oe_drop_zone.oe_insert.oe_hover:before {
+  border-top: dashed 2px rgba(116, 255, 161, 0.72);
+}
+.oe_drop_zone.oe_insert.oe_vertical {
+  width: 48px;
+  float: left;
+  position: relative;
+  margin: 0px -24px !important;
+}
+.oe_drop_zone.oe_insert.oe_overlay {
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  -ms-border-radius: 3px;
+  -o-border-radius: 3px;
+  border-radius: 3px;
+  background: rgba(153, 0, 255, 0.5);
+}
+
+.oe_drop_zone, .oe_drop_zone_style {
+  border: none;
+  background: rgba(153, 0, 255, 0.3);
+  -webkit-border-radius: 4px;
+  -moz-border-radius: 4px;
+  -ms-border-radius: 4px;
+  -o-border-radius: 4px;
+  border-radius: 4px;
+}
+.oe_drop_zone.oe_hover, .oe_drop_zone_style.oe_hover {
+  background: rgba(0, 255, 133, 0.3);
+  z-index: 1001;
+}
+
+.oe_drop_zone_style {
+  color: white;
+  height: 48px;
+  margin-bottom: 32px;
+  padding-top: 12px;
+}
+
+/* ---- SNIPPET MANIPULATOR ----  {{{ */
+.resize_editor_busy {
+  background-color: rgba(0, 0, 0, 0.3);
+}
+
+.oe_overlay {
+  display: none;
+  height: 0;
+  position: absolute;
+  background: rgba(0, 0, 0, 0);
+  z-index: 1001;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  -ms-border-radius: 3px;
+  -o-border-radius: 3px;
+  border-radius: 3px;
+  -webkit-transition: opacity 100ms linear;
+  -moz-transition: opacity 100ms linear;
+  -o-transition: opacity 100ms linear;
+  transition: opacity 100ms linear;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+}
+.oe_overlay.oe_active {
+  display: block;
+}
+.oe_overlay .oe_handle {
+  display: block !important;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  width: 16px;
+  height: 16px;
+  margin: -2px;
+}
+.oe_overlay .oe_handle > div {
+  z-index: 1;
+  position: absolute;
+  border-style: dashed;
+  border-width: 1px;
+  border-color: #666666;
+  -webkit-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
+  -moz-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
+  box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset;
+}
+.oe_overlay .oe_handle.e:before, .oe_overlay .oe_handle.w:before, .oe_overlay .oe_handle.s:before, .oe_overlay .oe_handle.n:before, .oe_overlay .oe_handle.size .oe_handle_button {
+  z-index: 2;
+  position: relative;
+  top: 50%;
+  left: 50%;
+  display: block;
+  background: white;
+  border: solid 1px rgba(0, 0, 0, 0.2);
+  -webkit-border-radius: 5px;
+  -moz-border-radius: 5px;
+  -ms-border-radius: 5px;
+  -o-border-radius: 5px;
+  border-radius: 5px;
+  width: 18px;
+  height: 18px;
+  margin: -9px;
+  padding-left: 1px;
+  font-size: 14px;
+  line-height: 14px;
+  font-family: FontAwesome;
+  color: rgba(0, 0, 0, 0.5);
+  -webkit-transition: background 100ms linear;
+  -moz-transition: background 100ms linear;
+  -o-transition: background 100ms linear;
+  transition: background 100ms linear;
+}
+.oe_overlay .oe_handle.e:hover:before, .oe_overlay .oe_handle.w:hover:before, .oe_overlay .oe_handle.s:hover:before, .oe_overlay .oe_handle.n:hover:before {
+  background: rgba(0, 0, 0, 0.5);
+  color: white;
+  -webkit-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+  -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+  box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+}
+.oe_overlay .oe_handle.e, .oe_overlay .oe_handle.w {
+  top: 4px;
+  height: 100%;
+}
+.oe_overlay .oe_handle.e:before, .oe_overlay .oe_handle.w:before {
+  content: "\f0d9-\f0da";
+  line-height: 16px;
+}
+.oe_overlay .oe_handle.e > div, .oe_overlay .oe_handle.w > div {
+  width: 0;
+  height: 100%;
+  top: 2px;
+  left: 8px;
+}
+.oe_overlay .oe_handle.e {
+  left: auto;
+  right: -6px;
+  cursor: w-resize;
+}
+.oe_overlay .oe_handle.w {
+  left: -6px;
+  cursor: e-resize;
+}
+.oe_overlay .oe_handle.s, .oe_overlay .oe_handle.n {
+  left: 2px;
+  width: 100%;
+}
+.oe_overlay .oe_handle.s:before, .oe_overlay .oe_handle.n:before {
+  content: "\f07d";
+  text-align: center;
+  padding: 1px;
+}
+.oe_overlay .oe_handle.s > div, .oe_overlay .oe_handle.n > div {
+  width: 100%;
+  height: 0;
+  top: 7px;
+  left: 1px;
+}
+.oe_overlay .oe_handle.s {
+  top: auto;
+  cursor: n-resize;
+}
+.oe_overlay .oe_handle.n {
+  cursor: s-resize;
+}
+.oe_overlay .oe_handle.n > div {
+  top: 5px;
+}
+.oe_overlay .oe_handle.size {
+  z-index: 3;
+  top: auto;
+  left: 50%;
+  bottom: -6px;
+  margin-left: -50px;
+}
+.oe_overlay .oe_handle.size .oe_handle_button {
+  position: relative;
+  z-index: 3;
+  text-align: center;
+  margin-left: -52px;
+  margin-top: -10px;
+  left: 0px;
+}
+.oe_overlay .oe_handle.size .oe_handle_button:hover {
+  background: rgba(30, 30, 30, 0.8);
+  color: white;
+  -webkit-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+  -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+  box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7);
+}
+.oe_overlay .oe_handle.size .size {
+  position: absolute;
+  width: 64px;
+  cursor: row-resize;
+  top: 9px;
+  margin-left: 52px;
+  padding: 0 5px;
+}
+.oe_overlay .oe_handle.size .auto_size {
+  position: absolute;
+  width: 100px;
+  top: 9px;
+  cursor: pointer;
+}
+.oe_overlay .oe_handle.readonly {
+  cursor: auto !important;
+}
+.oe_overlay .oe_handle.readonly:before, .oe_overlay .oe_handle.readonly.size {
+  display: none !important;
+}
+.oe_overlay .icon.btn {
+  display: inline-block;
+}
+.oe_overlay .oe_overlay_options {
+  position: absolute;
+  left: 50% !important;
+  text-align: center;
+  top: -20px;
+  z-index: 1002;
+}
+.oe_overlay .oe_overlay_options > .btn-group {
+  left: -50%;
+  white-space: nowrap;
+}
+.oe_overlay .oe_overlay_options > .btn-group > a {
+  cursor: pointer;
+  display: inline-block;
+  float: none;
+  margin: 0 -3px;
+}
+.oe_overlay .oe_overlay_options .btn, .oe_overlay .oe_overlay_options a {
+  cursor: pointer;
+}
+.oe_overlay .oe_overlay_options .dropdown {
+  display: inline-block;
+}
+.oe_overlay .oe_overlay_options .dropdown-menu {
+  text-align: left;
+  min-width: 180px;
+}
+.oe_overlay .oe_overlay_options .dropdown-menu select, .oe_overlay .oe_overlay_options .dropdown-menu input {
+  display: block;
+}
+.oe_overlay.block-w-left .w:before {
+  content: "\F061" !important;
+}
+.oe_overlay.block-w-right .w:before {
+  content: "\F060" !important;
+}
+.oe_overlay.block-w-left.block-w-right .w {
+  display: none !important;
+}
+.oe_overlay.block-e-left .e:before {
+  content: "\F061" !important;
+}
+.oe_overlay.block-e-right .e:before {
+  content: "\F060" !important;
+}
+.oe_overlay.block-e-left.block-e-right .e {
+  display: none !important;
+}
+.oe_overlay.block-s-top .s:before {
+  content: "\F063" !important;
+}
+.oe_overlay.block-s-bottom .s:before {
+  content: "\f062" !important;
+}
+.oe_overlay.block-n-top .n:before {
+  content: "\F063" !important;
+}
+.oe_overlay.block-n-bottom .n:before {
+  content: "\f062" !important;
+}
+
+.s-resize-important, .s-resize-important * {
+  cursor: s-resize !important;
+}
+
+.n-resize-important, .n-resize-important * {
+  cursor: n-resize !important;
+}
+
+.e-resize-important, .e-resize-important * {
+  cursor: e-resize !important;
+}
+
+.w-resize-important, .w-resize-important * {
+  cursor: w-resize !important;
+}
+
+.move-important, .move-important * {
+  cursor: move !important;
+}
+
+/* add CSS for bootstrap dropdown multi level */
+.dropdown-submenu {
+  position: relative;
+  z-index: 1000;
+}
+
+.dropdown-submenu > .dropdown-menu {
+  top: 0;
+  left: 100%;
+  margin-top: -6px;
+  margin-left: -1px;
+  -webkit-border-radius: 0 6px 6px 6px;
+  -moz-border-radius: 0 6px 6px 6px;
+  border-radius: 0 6px 6px 6px;
+}
+.dropdown-submenu:hover > .dropdown-menu {
+  display: block;
+}
+.dropdown-submenu:hover > a:after {
+  border-left-color: white;
+}
+.dropdown-submenu > a:after {
+  display: block;
+  content: " ";
+  float: right;
+  width: 0;
+  height: 0;
+  border-color: rgba(0, 0, 0, 0);
+  border-style: solid;
+  border-width: 5px 0 5px 5px;
+  border-left-color: #cccccc;
+  margin-top: 5px;
+  margin-right: -10px;
+}
+.dropdown-submenu.pull-left {
+  float: none;
+}
+.dropdown-submenu.pull-left > .dropdown-menu {
+  left: -100%;
+  margin-left: 10px;
+  -webkit-border-radius: 6px 0 6px 6px;
+  -moz-border-radius: 6px 0 6px 6px;
+  border-radius: 6px 0 6px 6px;
+}
+
+.oe_snippet_list {
+  width: auto;
+  white-space: nowrap;
+  margin-left: 20px;
+}
+
+.oe_snippet_editor {
+  position: fixed;
+  z-index: 1000;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 214px;
+  background: #a0a0a0;
+  box-shadow: 0 1px 3px #646464 inset;
+}
+.oe_snippet_editor .oe_snippet {
+  box-sizing: border-box;
+  display: inline-block;
+  width: 220px;
+  height: 160px;
+  border-radius: 3px;
+  background: white;
+  margin: 20px 20px 20px 0;
+  cursor: pointer;
+  border: 2px solid rgba(0, 0, 0, 0);
+  box-shadow: 0 1px 3px #646464;
+  position: relative;
+  top: 0;
+  overflow: hidden;
+  vertical-align: top;
+  user-select: none;
+  white-space: normal;
+}
+.oe_snippet_editor .oe_snippet:after {
+  content: attr(name);
+  position: absolute;
+  bottom: 0px;
+  left: 0px;
+  right: 0px;
+  background: rgba(255, 255, 255, 0.8);
+  text-align: center;
+  color: black;
+  padding: 8px;
+}
+.oe_snippet_editor .oe_snippet.oe_selected, .oe_snippet_editor .oe_snippet.oe_active {
+  border: 2px solid #9789ff;
+  box-shadow: 0px 3px 17px rgba(99, 53, 150, 0.59);
+}
+.oe_snippet_editor .oe_snippet > * {
+  margin-top: 16px;
+  line-height: 1em;
+  zoom: 0.6;
+}
+.oe_snippet_editor .oe_snippet > .container {
+  margin-top: 15px;
+  zoom: 0.17;
+  line-height: 0.999em;
+  line-height: 1em;
+}
+.oe_snippet_editor .oe_snippet > .row {
+  margin-top: 0px;
+  zoom: 0.23;
+  line-height: 0.999em;
+}
+.oe_snippet_editor .oe_snippet > .span6 {
+  margin-top: 18px;
+  zoom: 0.34;
+}
+.oe_snippet_editor .oe_snippet > .span12 {
+  margin-top: 16px;
+  zoom: 0.23;
+}
+.oe_snippet_editor .oe_snippet > p {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  bottom: 0;
+  font-size: 20px;
+  padding: 16px;
+  zoom: 1;
+  margin: 0px;
+}
+.oe_snippet_editor .oe_snippet.oe_new {
+  background: gray;
+  box-shadow: 0px 1px 3px #5a5a5a inset;
+  border: 0px;
+}
+.oe_snippet_editor .oe_snippet.oe_new.oe_selected, .oe_snippet_editor .oe_snippet.oe_new.oe_active {
+  box-shadow: 0px 2px 0px 0px #9789ff inset, 0px 3px 17px rgba(99, 53, 150, 0.59) inset;
+}
+.oe_snippet_editor .oe_snippet.oe_new > * {
+  zoom: 1;
+  margin: 0;
+  line-height: 160px;
+  text-align: center;
+  color: white;
+  font-size: 48px;
+}
+.oe_snippet_editor .oe_snippet.oe_new:after {
+  position: absolute;
+  left: 0px;
+  right: 0px;
+  top: 0px;
+  bottom: 0px;
+  background: rgba(0, 0, 0, 0);
+  color: white;
+  text-shadow: 0px 1px 3px black;
+  line-height: 160px;
+  padding: 0px;
+}
+
+.oe_snippet_drop {
+  position: relative;
+  border: 2px dashed #ae34ff;
+  background: rgba(147, 52, 255, 0.1);
+  min-height: 28px;
+  margin: -16px auto;
+  border-radius: 5px;
+  max-width: 50%;
+}
+.oe_snippet_drop.oe_accepting {
+  border: 2px dashed #34ffa6;
+  background: rgba(52, 255, 190, 0.1);
+}
+
+#website-top-edit li.oe_snippet_editorbar {
+  padding: 1px 8px 2px;
+  font: normal normal normal 12px Arial, Helvetica, Tahoma, Verdana, Sans-Serif;
+  float: left;
+  margin-top: 6px;
+  border: 1px solid #a6a6a6;
+  border-bottom-color: #979797;
+  background: #eeeeee;
+  border-radius: 3px;
+}
+#website-top-edit li.oe_snippet_editorbar > * {
+  display: inline-block;
+  height: 22px;
+  padding: 4px 6px;
+  outline: 0;
+  border: 0;
+}
+#website-top-edit li.oe_snippet_editorbar a.button .icon {
+  cursor: inherit;
+  background-repeat: no-repeat;
+  margin-top: 1px;
+  width: 16px;
+  height: 16px;
+  display: inline-block;
+}
+
+/* ---- COLOR-PICKER ----  {{{ */
+.colorpicker {
+  margin: 0 auto;
+  background: rgba(0, 0, 0, 0);
+  border: 0;
+}
+.colorpicker td {
+  padding: 0;
+  background: rgba(0, 0, 0, 0);
+}
+.colorpicker td > * {
+  width: 16px;
+  height: 16px;
+  border-radius: 2px;
+  margin: 3px;
+  padding: 0;
+  border-width: 1px;
+  display: block;
+}
+.colorpicker .only-text {
+  display: none;
+}
+.colorpicker .automatic-color {
+  background: white;
+  border-left: 7px solid #ff3333;
+  border-top: 7px solid #00ee00;
+  border-right: 8px solid #3333ff;
+  border-bottom: 8px solid #ffee00;
+  margin: 4px 3px 3px 3px;
+  width: 0;
+  height: 0;
+}
+
+.cke_panel_container table.colorpicker tr:first-child td {
+  padding-top: 6px;
+}
+.cke_panel_container table.colorpicker tr:last-child td {
+  padding-bottom: 6px;
+}
+
+
+/* snippets default margins */
+.s_banner {
+  margin-bottom: 32px;
+}
+
+.s_big_message {
+  margin-top: 16px;
+  margin-bottom: 16px;
+}
+
+.s_text_block {
+  margin-top: 16px;
+  margin-bottom: 16px;
+}
+
+.s_features {
+  margin-top: 16px;
+}
+
+.s_big_picture {
+  margin-top: 16px;
+  margin-bottom: 16px;
+}
+
+.s_three_columns {
+  margin-top: 16px;
+  margin-bottom: 16px;
+}
+
+.s_references {
+  margin-top: 16px;
+  margin-bottom: 32px;
+}
+
+.s_quotes_slider {
+  margin-bottom: 0px;
+}
+
+.s_features_grid {
+  margin-bottom: 16px;
+}

+ 590 - 0
static/src/css/snippets.sass

@@ -0,0 +1,590 @@
+@import "compass/css3"
+@import "compass/css3/user-interface"
+@import "compass/css3/transition"
+
+
+/* ---- SNIPPET EDITOR ---- {{{ */
+
+#oe_snippets
+    position: fixed
+    top: 34px
+    left: 0px
+    right: 0px
+    padding-top: 6px
+    // top property is set programmatically
+    background: rgb(40,40,40)
+    +box-shadow(0px 10px 10px -10px black inset)
+    z-index: 1010
+    overflow: hidden
+    &:hover
+        height: auto
+    .scroll
+        white-space: nowrap
+        overflow-y: none
+    .nav
+        display: inline-block
+        border-bottom: none !important
+        vertical-align: middle
+        min-width: 120px
+        > li
+            display: block
+            float: none
+            &.active
+                background: black !important
+            > a
+                padding: 2px 10px !important
+                width: 100%
+                display: block
+                border: 0
+        z-index: 1
+    .pill-content
+        border: 0
+    .tab-content
+        margin-right: 120px
+        display: inline-block
+        white-space: normal
+        background: black
+        > div
+            background: rgb(0,0,0)
+            label
+                width: 44px
+                color: #fff
+                padding-left: 10px
+
+                div
+                    width: 100px
+                    text-align: center
+                    @include transform( translate(-39px, 44px) , rotate(-90deg) )
+                    @include transform-origin(50% 50%)
+
+.oe_snippet
+    display: inline-block
+    vertical-align: top
+    width: 93px
+    margin-left: 1px
+    margin-top: 0px
+    position: relative
+    overflow: hidden
+    +user-select(none)
+    cursor: move
+    .oe_snippet_thumbnail
+        text-align: center
+        height: 100%
+        background: rgba(0, 0, 0, 0)
+        color: white
+        position: relative
+        &:hover
+            .oe_snippet_thumbnail_img
+                @include transform( scale(.95,.95))
+        .oe_snippet_thumbnail_title
+            font-size: 12px
+            display: block
+            +text-shadow(0 0 2px rgb(0,0,0))
+        .oe_snippet_thumbnail_img
+            height: 74px
+            background-size: cover
+            margin-bottom: 5px
+            @include transform( scale(1,1))
+        span, div
+            line-height: 18px
+    & > :not(.oe_snippet_thumbnail)
+        display: none !important
+
+#oe_snippets .oe_snippet_thumbnail
+    @include background(#747474, radial-gradient(rgba(0,0,0,0.25),rgba(0,0,0,0.4)))
+
+// }}}}
+
+/* ---- SNIPPETS DROP ZONES ---- {{{ */
+
+.oe_drop_zone.oe_insert
+    display: block
+    height: 48px
+    margin: 0px
+    margin-top: -4px
+    margin-bottom: -44px
+    @include transition(margin 250ms linear)
+    width: 100%
+    position: absolute
+    z-index: 1000
+    &:not(.oe_vertical):before
+        content: ""
+        display: block
+        border-top: dashed 2px rgba(209, 178, 255, 0.72)
+        position: relative
+        top: 0px
+    &.oe_hover:before
+        border-top: dashed 2px rgba(116, 255, 161, 0.72)
+    &.oe_vertical
+        width: 48px
+        float: left
+        position: relative
+        margin: 0px -24px !important
+    &.oe_overlay
+        +border-radius(3px)
+        //@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.1) ,rgba(255,255,255,.1) 35px, rgba(0,0,0,.1) 35px, rgba(0,0,0,.1) 75px))
+        //background-size: 100px 100px
+        background: rgba(153, 0, 255,.5)
+
+.oe_drop_zone, .oe_drop_zone_style
+    border: none
+    //@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.1) ,rgba(255,255,255,.1) 35px, rgba(0,0,0,.1) 35px, rgba(0,0,0,.1) 75px))
+    //background-size: 100px 100px
+    background: rgba(153, 0, 255, .3)
+    +border-radius(4px)
+    &.oe_hover
+        background: rgba(0, 255, 133, .3)
+        z-index: 1001
+
+.oe_drop_zone_style
+    color: white
+    height: 48px
+    margin-bottom: 32px
+    padding-top: 12px
+
+// }}}
+
+/* ---- SNIPPET MANIPULATOR ----  {{{ */
+
+.resize_editor_busy
+    background-color: rgba(0,0,0,0.3)
+
+.oe_overlay
+    display: none
+    height: 0
+    position: absolute
+    background: rgba(0, 0, 0, 0)
+    z-index: 1001
+    //@include background-image( repeating-linear-gradient(45deg, rgba(255,255,255,.02) ,rgba(255,255,255,.02) 35px, rgba(0,0,0,.02) 35px, rgba(0,0,0,.02) 75px))
+    +border-radius(3px)
+    @include transition(opacity 100ms linear)
+    +box-sizing(border-box)
+    &.oe_active
+        display: block
+    .oe_handle
+        display: block !important
+        position: absolute
+        top: 50%
+        left: 50%
+        +box-sizing(border-box)
+        width: 16px
+        height: 16px
+        margin: -2px
+        > div
+            z-index: 1
+            position: absolute
+            border-style: dashed
+            border-width: 1px
+            border-color: #666666
+            +box-shadow(0px 0px 0px 1px rgba(255,255,255,0.5), 0px 0px 0px 1px rgba(255,255,255,0.5) inset)
+        &.e:before, &.w:before, &.s:before, &.n:before, &.size .oe_handle_button
+            z-index: 2
+            position: relative
+            top: 50%
+            left: 50%
+            display: block
+            background: rgba(255, 255, 255, 1)
+            border: solid 1px rgba(0, 0, 0, .2)
+            +border-radius(5px)
+            width: 18px
+            height: 18px
+            margin: -9px
+            padding-left: 1px
+            font-size: 14px
+            line-height: 14px
+            font-family: FontAwesome
+            color: rgba(0,0,0,.5)
+            @include transition(background 100ms linear)
+        &.e, &.w, &.s, &.n
+            &:hover:before
+                background: rgba(0, 0, 0, .5)
+                color: #fff
+                +box-shadow(0 0 5px 3px rgba(255,255,255,.7))
+        &.e, &.w
+            top: 4px
+            height: 100%
+            &:before
+                content: "\f0d9-\f0da"
+                line-height: 16px
+            > div
+                width: 0
+                height: 100%
+                top: 2px
+                left: 8px
+        &.e
+            left: auto
+            right: -6px
+            cursor: w-resize
+        &.w
+            left: -6px
+            cursor: e-resize
+        &.s, &.n
+            left: 2px
+            width: 100%
+            &:before
+                content: "\f07d"
+                text-align: center
+                padding: 1px
+            > div
+                width: 100%
+                height: 0
+                top: 7px
+                left: 1px
+        &.s
+            top: auto
+            cursor: n-resize
+        &.n
+            cursor: s-resize
+            > div
+                top: 5px
+        &.size
+            z-index: 3
+            top: auto
+            left: 50%
+            bottom: -6px
+            margin-left: -50px
+            .oe_handle_button
+                position: relative
+                z-index: 3
+                text-align: center
+                margin-left: -52px
+                margin-top: -10px
+                left: 0px
+                &:hover
+                    background: rgba(30, 30, 30, .8)
+                    color: #fff
+                    +box-shadow(0 0 5px 3px rgba(255,255,255,.7))
+            .size
+                position: absolute
+                width: 64px
+                cursor: row-resize
+                top: 9px
+                margin-left: 52px
+                padding: 0 5px
+            .auto_size
+                position: absolute
+                width: 100px
+                top: 9px
+                cursor: pointer
+        &.readonly
+            cursor: auto !important
+            &:before, &.size
+                display: none !important
+
+    .icon.btn
+        display: inline-block
+
+    .oe_overlay_options
+        position: absolute
+        left: 50% !important
+        text-align: center
+        top: -20px
+        z-index: 1002
+        > .btn-group
+            left: -50%
+            white-space: nowrap
+            > a
+                cursor: pointer
+                display: inline-block
+                float: none
+                margin: 0 -3px
+        .btn, a
+            cursor: pointer
+        .dropdown
+            display: inline-block
+        .dropdown-menu
+            text-align: left
+            min-width: 180px
+        .dropdown-menu select,.dropdown-menu input
+            display: block
+
+    &.block-w-left .w:before
+        content: "\F061" !important
+    &.block-w-right .w:before
+        content: "\F060" !important
+    &.block-w-left.block-w-right .w
+        display: none !important
+    &.block-e-left .e:before
+        content: "\F061" !important
+    &.block-e-right .e:before
+        content: "\F060" !important
+    &.block-e-left.block-e-right .e
+        display: none !important
+
+    &.block-s-top .s:before
+        content: "\F063" !important
+    &.block-s-bottom .s:before
+        content: "\f062" !important
+    &.block-n-top .n:before
+        content: "\F063" !important
+    &.block-n-bottom .n:before
+        content: "\f062" !important
+
+
+.s-resize-important, .s-resize-important *
+    cursor: s-resize !important
+.n-resize-important, .n-resize-important *
+    cursor: n-resize !important
+.e-resize-important, .e-resize-important *
+    cursor: e-resize !important
+.w-resize-important, .w-resize-important *
+    cursor: w-resize !important
+.move-important, .move-important *
+    cursor: move !important
+
+// }}}
+
+
+/* add CSS for bootstrap dropdown multi level */
+.dropdown-submenu
+    position: relative
+    z-index: 1000
+.dropdown-submenu
+    &>.dropdown-menu
+        top: 0
+        left: 100%
+        margin-top: -6px
+        margin-left: -1px
+        -webkit-border-radius: 0 6px 6px 6px
+        -moz-border-radius: 0 6px 6px 6px
+        border-radius: 0 6px 6px 6px
+    &:hover
+        &>.dropdown-menu
+            display: block
+        &>a:after
+            border-left-color: #ffffff
+    &>a:after
+        display: block
+        content: " "
+        float: right
+        width: 0
+        height: 0
+        border-color: rgba(0, 0, 0, 0)
+        border-style: solid
+        border-width: 5px 0 5px 5px
+        border-left-color: #cccccc
+        margin-top: 5px
+        margin-right: -10px
+    &.pull-left
+        float: none
+        &>.dropdown-menu
+            left: -100%
+            margin-left: 10px
+            -webkit-border-radius: 6px 0 6px 6px
+            -moz-border-radius: 6px 0 6px 6px
+            border-radius: 6px 0 6px 6px
+
+.oe_snippet_list
+    width: auto
+    white-space: nowrap
+    margin-left: 20px
+
+.oe_snippet_editor
+    position: fixed
+    z-index: 1000
+    bottom: 0
+    left: 0
+    right: 0
+    height: 214px
+    background: rgb(160,160,160)
+    box-shadow: 0 1px 3px rgb(100,100,100) inset
+    .oe_snippet
+        box-sizing: border-box
+        display: inline-block
+        width: 220px
+        height: 160px
+        border-radius: 3px
+        background: white
+        margin: 20px 20px 20px 0
+        cursor: pointer
+        border: 2px solid rgba(0, 0, 0, 0)
+        box-shadow: 0 1px 3px rgb(100,100,100)
+        position: relative
+        top: 0
+        overflow: hidden
+        vertical-align: top
+        user-select: none
+        white-space: normal
+        &:after
+            content: attr(name)
+            position: absolute
+            bottom: 0px
+            left: 0px
+            right: 0px
+            background: rgba(255,255,255,0.8)
+            text-align: center
+            color: black
+            padding: 8px
+        &.oe_selected, &.oe_active
+            border: 2px solid rgb(151, 137, 255)
+            box-shadow: 0px 3px 17px rgba(99, 53, 150, 0.59)
+        & > *
+            margin-top: 16px
+            line-height: 1em
+            zoom: 0.6
+        & > .container
+            margin-top: 15px
+            zoom: 0.17
+            line-height: 0.999em
+            line-height: 1em
+        & > .row
+            margin-top: 0px
+            zoom: 0.23
+            line-height: 0.999em
+        & > .span6
+            margin-top: 18px
+            zoom: 0.34
+        & > .span12
+            margin-top: 16px
+            zoom: 0.23
+        & > p
+            position: absolute
+            top: 0
+            right: 0
+            left: 0
+            bottom: 0
+            font-size: 20px
+            padding: 16px
+            zoom: 1
+            margin: 0px
+        &.oe_new
+            background: gray
+            box-shadow: 0px 1px 3px rgb(90,90,90) inset
+            border: 0px
+            &.oe_selected, &.oe_active
+                box-shadow: 0px 2px 0px 0px rgb(151,137,255) inset, 0px 3px 17px rgba(99, 53, 150, 0.59) inset
+            & > *
+                zoom: 1
+                margin: 0
+                line-height: 160px
+                text-align: center
+                color: white
+                font-size: 48px
+            &:after
+                position: absolute
+                left: 0px
+                right: 0px
+                top: 0px
+                bottom: 0px
+                background: rgba(0, 0, 0, 0)
+                color: white
+                text-shadow: 0px 1px 3px black
+                line-height: 160px
+                padding: 0px
+
+.oe_snippet_drop 
+    position: relative
+    border: 2px dashed rgb(174, 52, 255)
+    background: rgba(147, 52, 255, 0.1)
+    min-height: 28px
+    margin: -16px auto
+    border-radius: 5px
+    max-width: 50%
+    &.oe_accepting 
+        border: 2px dashed rgb(52, 255, 166)
+        background: rgba(52, 255, 190, 0.1)
+
+#website-top-edit
+    li.oe_snippet_editorbar 
+        padding: 1px 8px 2px
+        font: normal normal normal 12px Arial,Helvetica,Tahoma,Verdana,Sans-Serif
+        float: left
+        margin-top: 6px
+        border: 1px solid #a6a6a6
+        border-bottom-color: #979797
+        background: #eeeeee
+        border-radius: 3px
+        & > * 
+            display: inline-block
+            height: 22px
+            padding: 4px 6px
+            outline: 0
+            border: 0
+
+        a.button .icon 
+            cursor: inherit
+            background-repeat: no-repeat
+            margin-top: 1px
+            width: 16px
+            height: 16px
+            display: inline-block
+
+/* ---- COLOR-PICKER ----  {{{ */
+
+.colorpicker
+    margin: 0 auto
+    background: rgba(0, 0, 0, 0)
+    border: 0
+    td
+        padding: 0
+        background: rgba(0, 0, 0, 0)
+        > *
+            width: 16px
+            height: 16px
+            border-radius: 2px
+            margin: 3px
+            padding: 0
+            border-width: 1px
+            display: block
+    .only-text
+        display: none
+    .automatic-color
+        background: #fff
+        border-left: 7px solid #f33
+        border-top: 7px solid #0e0
+        border-right: 8px solid #33f
+        border-bottom: 8px solid #fe0
+        margin: 4px 3px 3px 3px
+        width: 0
+        height: 0
+.cke_panel_container table.colorpicker
+    tr:first-child td
+        padding-top: 6px
+    tr:last-child td
+        padding-bottom: 6px
+
+
+/* snippets default margins */
+.s_banner {
+  margin-bottom: 32px
+}
+
+.s_big_message {
+  margin-top: 16px
+  margin-bottom: 16px
+}
+
+.s_text_block {
+  margin-top: 16px
+  margin-bottom: 16px
+}
+
+.s_features {
+  margin-top: 16px
+}
+
+.s_big_picture {
+  margin-top: 16px
+  margin-bottom: 16px
+}
+
+.s_three_columns {
+  margin-top: 16px
+  margin-bottom: 16px
+}
+
+.s_references {
+  margin-top: 16px
+  margin-bottom: 32px
+}
+
+.s_quotes_slider {
+  margin-bottom: 0px
+}
+
+.s_features_grid {
+  margin-bottom: 16px
+}
+
+
+// }}}

BIN
static/src/img/theme/layout-boxed.gif


BIN
static/src/img/theme/layout-full.gif


BIN
static/src/img/theme/variant-amethyst.gif


BIN
static/src/img/theme/variant-cobalt.gif


BIN
static/src/img/theme/variant-emerald.gif


BIN
static/src/img/theme/variant-gold.gif


BIN
static/src/img/theme/variant-ruby.gif


BIN
static/src/img/theme/variant-stone.gif


+ 118 - 0
static/src/js/website.editor.js

@@ -0,0 +1,118 @@
+(function () {
+    'use strict';
+
+    var website = openerp.website;
+
+    CKEDITOR.plugins.add('customColor', {
+        requires: 'panelbutton,floatpanel',
+        init: function (editor) {
+            function create_button (buttonID, label) {
+                var btnID = buttonID;
+                editor.ui.add(buttonID, CKEDITOR.UI_PANELBUTTON, {
+                    label: label,
+                    title: label,
+                    modes: { wysiwyg: true },
+                    editorFocus: true,
+                    context: 'font',
+                    panel: {
+                        css: [  '/web/css/web.assets_common/' + (new Date().getTime()),
+                                '/web/css/website.assets_frontend/' + (new Date().getTime()),
+                                '/web/css/website.assets_editor/' + (new Date().getTime())],
+                        attributes: { 'role': 'listbox', 'aria-label': label },
+                    },
+                    enable: function () {
+                        this.setState(CKEDITOR.TRISTATE_OFF);
+                    },
+                    disable: function () {
+                        this.setState(CKEDITOR.TRISTATE_DISABLED);
+                    },
+                    onBlock: function (panel, block) {
+                        var self = this;
+                        var html = openerp.qweb.render('website_less.colorpicker');
+                        block.autoSize = true;
+                        block.element.setHtml( html );
+                        $(block.element.$).on('click', 'button', function () {
+                            self.clicked(this);
+                        });
+                        if (btnID === "TextColor") {
+                            $(".only-text", block.element.$).css("display", "block");
+                            $(".only-bg", block.element.$).css("display", "none");
+                        }
+                        var $body = $(block.element.$).parents("body");
+                        setTimeout(function () {
+                            $body.css('background-color', '#fff');
+                        }, 0);
+                    },
+                    getClasses: function () {
+                        var self = this;
+                        var classes = [];
+                        var id = this._.id;
+                        var block = this._.panel._.panel._.blocks[id];
+                        var $root = $(block.element.$);
+                        $root.find("button").map(function () {
+                            var color = self.getClass(this);
+                            if(color) classes.push( color );
+                        });
+                        return classes;
+                    },
+                    getClass: function (button) {
+                        var color = btnID === "BGColor" ? $(button).attr("class") : $(button).attr("class").replace(/^bg-/i, 'text-');
+                        return color.length && color;
+                    },
+                    clicked: function (button) {
+                        var className = this.getClass(button);
+                        var ancestor = editor.getSelection().getCommonAncestor();
+
+                        editor.focus();
+                        this._.panel.hide();
+                        editor.fire('saveSnapshot');
+
+                        // remove style
+                        var classes = [];
+                        var $ancestor = $(ancestor.$);
+                        var $fonts = $(ancestor.$).find('font');
+                        if (!ancestor.$.tagName) {
+                            $ancestor = $ancestor.parent();
+                        }
+                        if ($ancestor.is('font')) {
+                            $fonts = $fonts.add($ancestor[0]);
+                        }
+
+                        $fonts.filter("."+this.getClasses().join(",.")).map(function () {
+                            var className = $(this).attr("class");
+                            if (classes.indexOf(className) === -1) {
+                                classes.push(className);
+                            }
+                        });
+                        for (var k in classes) {
+                            editor.removeStyle( new CKEDITOR.style({
+                                element: 'font',
+                                attributes: { 'class': classes[k] },
+                            }) );
+                        }
+
+                        // add new style
+                        if (className) {
+                            editor.applyStyle( new CKEDITOR.style({
+                                element: 'font',
+                                attributes: { 'class': className },
+                            }) );
+                        }
+                        editor.fire('saveSnapshot');
+                    }
+
+                });
+            }
+            create_button("BGColor", "Background Color");
+            create_button("TextColor", "Text Color");
+        }
+    });
+
+    website.RTE = website.RTE.extend({
+        _config: function () {
+            var config = this._super.apply(this, arguments);
+            config.extraPlugins = 'customColor,' + config.extraPlugins;
+            return config
+        },
+    });
+})();

+ 80 - 0
static/src/js/website.snippets.animation.js

@@ -0,0 +1,80 @@
+// Backported from 52ca425
+
+(function () {
+    'use strict';
+
+    var website = openerp.website;
+
+    function load_called_template () {
+        var ids_or_xml_ids = _.uniq($("[data-oe-call]").map(function () {return $(this).data('oe-call');}).get());
+        if (ids_or_xml_ids.length) {
+            openerp.jsonRpc('/website/multi_render', 'call', {
+                'ids_or_xml_ids': ids_or_xml_ids
+            }).then(function (data) {
+                for (var k in data) {
+                    var $data = $(data[k]);
+                    // clean t-oe
+                    $data.each(function () {
+                        for (var k=0; k<this.attributes.length; k++) {
+                            if (this.attributes[k].name.indexOf('data-oe-') === 0) {
+                                $(this).removeAttr(this.attributes[k].name);
+                                k--;
+                            }
+                        }
+                    });
+                    // end
+                    $("[data-oe-call='"+k+"']").each(function () {
+                        $(this).replaceWith($data.clone());
+                    });
+                }
+                $(document).trigger('scroll');
+            });
+        }
+    }
+
+    $(document).ready(load_called_template);
+
+    $(document).ready(function () {
+        $(".o_animation")
+            .removeClass("o_displayed o_displayed_top o_displayed_middle o_displayed_bottom");
+        $(document)
+            .on('scroll', function (event) {
+                var docViewTop = $(document.body).scrollTop();
+                var docViewBottom = docViewTop + $(document.body).height();
+
+                $(".o_animation")
+                    .each(function () {
+                        var $el = $(this);
+                        var elemTop = $el.offset().top;
+                        var elemBottom = elemTop + $el.height();
+                        var elemMiddle = elemTop + $el.height()*0.4;
+                        var visible = elemBottom >= docViewTop && elemTop <= docViewBottom;
+
+                        if (visible) {
+                            $el.addClass("o_displayed o_visible");
+                        } else {
+                            $el.removeClass("o_visible");
+                        }
+
+                        if (visible && elemTop > docViewTop) {
+                            $el.addClass("o_displayed_top o_visible_top");
+                        } else {
+                            $el.removeClass("o_visible_top");
+                        }
+                        if (visible && elemMiddle < docViewBottom && elemMiddle > docViewTop) {
+                            $el.addClass("o_displayed_middle o_visible_middle");
+                        } else {
+                            $el.removeClass("o_visible_middle");
+                        }
+                        if (visible && elemBottom < docViewBottom) {
+                            $el.addClass("o_displayed_bottom o_visible_bottom");
+                        } else {
+                            $el.removeClass("o_visible_bottom");
+                        }
+
+                    });
+            })
+            .trigger('scroll');
+    });
+
+})();

+ 1931 - 0
static/src/js/website.snippets.editor.js

@@ -0,0 +1,1931 @@
+(function () {
+    'use strict';
+
+    var dummy = function () {};
+
+    var website = openerp.website;
+    website.add_template_file('/website/static/src/xml/website.snippets.xml');
+
+    website.EditorBar.include({
+        start: function () {
+            var self = this;
+            $("[data-oe-model]").on('click', function (event) {
+                var $this = $(event.srcElement);
+                var tag = $this[0] && $this[0].tagName.toLowerCase();
+                if (!(tag === 'a' || tag === "button") && !$this.parents("a, button").length) {
+                    self.$('[data-action="edit"]').parent().effect('bounce', {distance: 18, times: 5}, 250);
+                }
+            });
+            return this._super();
+        },
+        edit: function () {
+            var self = this;
+            $("[data-oe-model] *, [data-oe-type=html] *").off('click');
+            window.snippets = this.snippets = new website.snippet.BuildingBlock(this);
+            this.snippets.appendTo(this.$el);
+            website.snippet.stop_animation();
+            this.on('rte:ready', this, function () {
+                self.snippets.$button.removeClass("hidden");
+                website.snippet.start_animation(true);
+                $("#wrapwrap *").off('mousedown mouseup click');
+            });
+
+            return this._super.apply(this, arguments);
+        },
+        save: function () {
+            this.snippets.clean_for_save();
+            this._super();
+        },
+    });
+
+    /* ----- SNIPPET SELECTOR ---- */
+
+    var observer = new website.Observer(function (mutations) {
+        if (!_(mutations).find(function (m) {
+                    return m.type === 'childList' && m.addedNodes.length > 0;
+                })) {
+            return;
+        }
+    });
+
+    $.extend($.expr[':'],{
+        checkData: function(node,i,m){
+            var dataName = m[3];
+            while (node) {
+                if (node.dataset && node.dataset[dataName]) {
+                    return true;
+                } else {
+                    node = node.parentNode;
+                }
+            }
+            return false;
+        },
+        hasData: function(node,i,m){
+            return !!_.toArray(node.dataset).length;
+        },
+    });
+
+    if (!website.snippet) website.snippet = {};
+    website.snippet.templateOptions = [];
+    website.snippet.globalSelector = "";
+    website.snippet.selector = [];
+    website.snippet.BuildingBlock = openerp.Widget.extend({
+        template: 'website.snippets',
+        activeSnippets: [],
+        init: function (parent) {
+            this.parent = parent;
+            this._super.apply(this, arguments);
+            if(!$('#oe_manipulators').length){
+                $("<div id='oe_manipulators'></div>").appendTo('body');
+            }
+            this.$active_snipped_id = false;
+            this.snippets = [];
+
+            observer.observe(document.body, {
+                childList: true,
+                subtree: true,
+            });
+        },
+        start: function() {
+            var self = this;
+
+            this.$button = $(openerp.qweb.render('website.snippets_button'))
+                .prependTo(this.parent.$("#website-top-edit ul"))
+                .find("button");
+
+            this.$button.click(_.bind(this.show_blocks, this));
+
+            this.$snippet = $("#oe_snippets");
+            this.$wrapwrap = $("#wrapwrap");
+            this.$wrapwrap.click(function () {
+                self.$el.addClass("hidden");
+            });
+
+            this.fetch_snippet_templates();
+            this.bind_snippet_click_editor();
+            this.$el.addClass("hidden");
+
+            $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) {
+                e.preventDefault();
+            });
+
+            this.getParent().on('change:height', this, function (editor) {
+                self.$el.css('top', editor.get('height'));
+            });
+            this.$el.css('top', this.parent.get('height'));
+        },
+        show_blocks: function () {
+            var self = this;
+            this.make_active(false);
+            this.$el.toggleClass("hidden");
+            if (this.$el.hasClass("hidden")) {
+                return;
+            }
+
+            //this.enable_snippets( this.$snippet.find(".tab-pane.active") );
+            var categories = this.$snippet.find(".tab-pane.active")
+                .add(this.$snippet.find(".tab-pane:not(.active)"))
+                .get().reverse();
+            function enable() {
+                self.enable_snippets( $(categories.pop()) );
+                if (categories.length) {
+                    setTimeout(enable,10);
+                }
+            }
+            setTimeout(enable,0);
+        },
+        enable_snippets: function ($category) {
+            var self = this;
+            $category.find(".oe_snippet_body").each(function () {
+                var $snippet = $(this);
+
+                if (!$snippet.data('selectors')) {
+                    var selectors = [];
+                    for (var k in website.snippet.templateOptions) {
+                        var option = website.snippet.templateOptions[k];
+                        if ($snippet.is(option.base_selector)) {
+
+                            var dropzone = [];
+                            if (option['drop-near']) dropzone.push(option['drop-near']);
+                            if (option['drop-in']) dropzone.push(option['drop-in']);
+                            if (option['drop-in-vertical']) dropzone.push(option['drop-in-vertical']);
+                            selectors = selectors.concat(dropzone);
+                        }
+                    }
+                    $snippet.data('selectors', selectors.length ? selectors.join(":first, ") + ":first" : "");
+                }
+
+                if ($snippet.data('selectors').length && self.$wrapwrap.find($snippet.data('selectors')).size()) {
+                    $snippet.closest(".oe_snippet").removeClass("disable");
+                } else {
+                    $snippet.closest(".oe_snippet").addClass("disable");
+                }
+            });
+            $('#oe_snippets .scroll a[data-toggle="tab"][href="#' + $category.attr("id") + '"]')
+                .toggle(!!$category.find(".oe_snippet:not(.disable)").size());
+        },
+        _get_snippet_url: function () {
+            return '/website/snippets';
+        },
+        _add_check_selector : function (selector, no_check) {
+            var data = selector.split(",");
+            var selectors = [];
+            for (var k in data) {
+                selectors.push(data[k].replace(/^\s+|\s+$/g, '') + (no_check ? "" : ":checkData(oeModel)"));
+            }
+            return selectors.join(", ");
+        },
+        fetch_snippet_templates: function () {
+            var self = this;
+
+            openerp.jsonRpc(this._get_snippet_url(), 'call', {})
+                .then(function (html) {
+                    var $html = $(html);
+
+                    // t-snippet
+                    $html.find('> .tab-content > div > [data-oe-type="snippet"]').each(function () {
+                        var $div = $('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name'))
+                    });
+                    // end
+
+                    backward_compatibility_80($html);
+
+                    var selector = [];
+                    var $styles = $html.find("[data-js], [data-selector]");
+                    $styles.each(function () {
+                        var $style = $(this);
+                        var no_check = $style.data('no-check');
+                        var option_id = $style.data('js');
+                        var option = {
+                            'option' : option_id,
+                            'base_selector': $style.data('selector'),
+                            'selector': self._add_check_selector($style.data('selector'), no_check),
+                            '$el': $style,
+                            'drop-near': $style.data('drop-near') && self._add_check_selector($style.data('drop-near'), no_check),
+                            'drop-in': $style.data('drop-in') && self._add_check_selector($style.data('drop-in'), no_check),
+                            'data': $style.data()
+                        };
+                        website.snippet.templateOptions.push(option);
+                        selector.push(option.selector);
+                    });
+                    $styles.addClass("hidden");
+                    website.snippet.globalSelector = selector.join(",");
+
+                    self.$snippets = $html.find(".tab-content > div > div")
+                        .addClass("oe_snippet")
+                        .each(function () {
+                            if (!$('.oe_snippet_thumbnail', this).size()) {
+                                var $div = $(
+                                    '<div class="oe_snippet_thumbnail">'+
+                                        '<div class="oe_snippet_thumbnail_img"/>'+
+                                        '<span class="oe_snippet_thumbnail_title"></span>'+
+                                    '</div>');
+                                $div.find('span').text($(this).attr("name"));
+                                $(this).prepend($div);
+
+                                // from t-snippet
+                                var thumbnail = $("[data-oe-thumbnail]", this).data("oe-thumbnail");
+                                if (thumbnail) {
+                                    $div.find('.oe_snippet_thumbnail_img').css('background-image', 'url(' + thumbnail + ')');
+                                }
+                                // end
+                            }
+                            if (!$(this).data("selector")) {
+                                $("> *:not(.oe_snippet_thumbnail)", this).addClass('oe_snippet_body');
+                            }
+                        });
+
+                    // clean t-oe
+                    $html.find('[data-oe-model], [data-oe-type]').each(function () {
+                        for (var k=0; k<this.attributes.length; k++) {
+                            if (this.attributes[k].name.indexOf('data-oe-') === 0) {
+                                $(this).removeAttr(this.attributes[k].name);
+                                k--;
+                            }
+                        }
+                    });
+                    // end
+
+                    self.$el.append($html);
+
+                    self.make_snippet_draggable(self.$snippets);
+                });
+        },
+        cover_target: function ($el, $target){
+            var pos = $target.offset();
+            var mt = parseInt($target.css("margin-top") || 0);
+            var mb = parseInt($target.css("margin-bottom") || 0);
+            $el.css({
+                'width': $target.outerWidth(),
+                'top': pos.top - mt - 5,
+                'left': pos.left
+            });
+            $el.find(".oe_handle.e,.oe_handle.w").css({'height': $target.outerHeight() + mt + mb+1});
+            $el.find(".oe_handle.s").css({'top': $target.outerHeight() + mt + mb});
+            $el.find(".oe_handle.size").css({'top': $target.outerHeight() + mt});
+            $el.find(".oe_handle.s,.oe_handle.n").css({'width': $target.outerWidth()-2});
+        },
+        show: function () {
+            this.$el.removeClass("hidden");
+        },
+        hide: function () {
+            this.$el.addClass("hidden");
+        },
+        bind_snippet_click_editor: function () {
+            var self = this;
+            var snipped_event_flag;
+            self.$wrapwrap.on('click', function (event) {
+                var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target));
+                if (snipped_event_flag || !srcElement) {
+                    return;
+                }
+                snipped_event_flag = true;
+
+                setTimeout(function () {snipped_event_flag = false;}, 0);
+                var $target = $(srcElement);
+
+                if ($target.parents(".oe_overlay").length) {
+                    return;
+                }
+
+                if (!$target.is(website.snippet.globalSelector)) {
+                    $target = $target.parents(website.snippet.globalSelector).first();
+                }
+
+                if (self.$active_snipped_id && self.$active_snipped_id.is($target)) {
+                    return;
+                }
+                self.make_active($target);
+            });
+        },
+        snippet_blur: function ($snippet) {
+            if ($snippet) {
+                if ($snippet.data("snippet-editor")) {
+                    $snippet.data("snippet-editor").on_blur();
+                }
+            }
+        },
+        snippet_focus: function ($snippet) {
+            if ($snippet) {
+                if ($snippet.data("snippet-editor")) {
+                    $snippet.data("snippet-editor").on_focus();
+                }
+            }
+        },
+        clean_for_save: function () {
+            var self = this;
+            var options = website.snippet.options;
+            var template = website.snippet.templateOptions;
+            for (var k in template) {
+                var Option = options[template[k]['option']];
+                if (Option && Option.prototype.clean_for_save !== dummy) {
+                    self.$wrapwrap.find(template[k].selector).each(function () {
+                        new Option(self, null, $(this), k).clean_for_save();
+                    });
+                }
+            }
+            self.$wrapwrap.find("*[contentEditable], *[attributeEditable]")
+                .removeAttr('contentEditable')
+                .removeAttr('attributeEditable');
+
+            self.$wrapwrap.find(".o_animation").removeClass("o_display o_displayed o_display_down o_display_up o_display_middle");
+        },
+        make_active: function ($snippet) {
+            if ($snippet && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snippet.get(0)) {
+                return;
+            }
+            if (this.$active_snipped_id) {
+                this.snippet_blur(this.$active_snipped_id);
+                this.$active_snipped_id = false;
+            }
+            if ($snippet && $snippet.length) {
+                if(_.indexOf(this.snippets, $snippet.get(0)) === -1) {
+                    this.snippets.push($snippet.get(0));
+                }
+                this.$active_snipped_id = $snippet;
+                this.create_overlay(this.$active_snipped_id);
+                this.snippet_focus($snippet);
+            }
+            this.$snippet.trigger('snippet-activated', $snippet);
+            if ($snippet) {
+                $snippet.trigger('snippet-activated', $snippet);
+            }
+        },
+        create_overlay: function ($snippet) {
+            if (typeof $snippet.data("snippet-editor") === 'undefined') {
+                var $targets = this.activate_overlay_zones($snippet);
+                if (!$targets.length) return;
+                $snippet.data("snippet-editor", new website.snippet.Editor(this, $snippet));
+            }
+            this.cover_target($snippet.data('overlay'), $snippet);
+        },
+
+        // activate drag and drop for the snippets in the snippet toolbar
+        make_snippet_draggable: function($snippets){
+            var self = this;
+            var $tumb = $snippets.find(".oe_snippet_thumbnail_img:first");
+            var left = $tumb.outerWidth()/2;
+            var top = $tumb.outerHeight()/2;
+            var $toInsert, dropped, $snippet, action, snipped_id;
+
+            $snippets.draggable({
+                greedy: true,
+                helper: 'clone',
+                zIndex: '1000',
+                appendTo: 'body',
+                cursor: "move",
+                handle: ".oe_snippet_thumbnail",
+                cursorAt: {
+                    'left': left,
+                    'top': top
+                },
+                start: function(){
+                    self.hide();
+                    dropped = false;
+                    // snippet_selectors => to get drop-near, drop-in
+                    $snippet = $(this);
+                    var $base_body = $snippet.find('.oe_snippet_body');
+                    var selector = [];
+                    var selector_siblings = [];
+                    var selector_children = [];
+                    var vertical = false;
+                    var temp = website.snippet.templateOptions;
+                    for (var k in temp) {
+                        if ($base_body.is(temp[k].base_selector)) {
+                            selector.push(temp[k].base_selector);
+                            if (temp[k]['drop-near'])
+                                selector_siblings.push(temp[k]['drop-near']);
+                            if (temp[k]['drop-in'])
+                                selector_children.push(temp[k]['drop-in']);
+                        }
+                    }
+
+                    $toInsert = $base_body.clone();
+                    action = $snippet.find('.oe_snippet_body').size() ? 'insert' : 'mutate';
+
+                    if( action === 'insert'){
+                        if (!selector_siblings.length && !selector_children.length) {
+                            console.debug($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in");
+                            return;
+                        }
+                        self.activate_insertion_zones({
+                            siblings: selector_siblings.join(","),
+                            children: selector_children.join(","),
+                        });
+
+                    } else if( action === 'mutate' ){
+                        if (!$snippet.data('selector')) {
+                            console.debug($snippet.data("option") + " have not oe_snippet_body class and have not data-selector tag");
+                            return;
+                        }
+                        var $targets = self.activate_overlay_zones(selector_children.join(","));
+                        $targets.each(function(){
+                            var $clone = $(this).data('overlay').clone();
+                             $clone.addClass("oe_drop_zone").data('target', $(this));
+                            $(this).data('overlay').after($clone);
+                        });
+
+                    }
+
+                    $('.oe_drop_zone').droppable({
+                        over:   function(){
+                            if( action === 'insert'){
+                                dropped = true;
+                                $(this).first().after($toInsert);
+                            }
+                        },
+                        out:    function(){
+                            var prev = $toInsert.prev();
+                            if( action === 'insert' && this === prev[0]){
+                                dropped = false;
+                                $toInsert.detach();
+                            }
+                        }
+                    });
+                },
+                stop: function(ev, ui){
+                    $toInsert.removeClass('oe_snippet_body');
+
+                    if (action === 'insert' && ! dropped && self.$wrapwrap.find('.oe_drop_zone') && ui.position.top > 3) {
+                        var el = self.$wrapwrap.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
+                        if (el.length) {
+                            el.after($toInsert);
+                            dropped = true;
+                        }
+                    }
+
+                    self.$wrapwrap.find('.oe_drop_zone').droppable('destroy').remove();
+
+                    if (dropped) {
+                        var $target = false;
+                        $target = $toInsert;
+
+                        setTimeout(function () {
+                            self.$snippet.trigger('snippet-dropped', $target);
+
+                            website.snippet.start_animation(true, $target);
+
+                            // reset snippet for rte
+                            $target.removeData("snippet-editor");
+                            if ($target.data("overlay")) {
+                                $target.data("overlay").remove();
+                                $target.removeData("overlay");
+                            }
+                            $target.find(website.snippet.globalSelector).each(function () {
+                                var $snippet = $(this);
+                                $snippet.removeData("snippet-editor");
+                                if ($snippet.data("overlay")) {
+                                    $snippet.data("overlay").remove();
+                                    $snippet.removeData("overlay");
+                                }
+                            });
+                            // end
+
+                            // drop_and_build_snippet
+                            self.create_overlay($target);
+                            if ($target.data("snippet-editor")) {
+                                $target.data("snippet-editor").drop_and_build_snippet();
+                            }
+                            for (var k in website.snippet.templateOptions) {
+                                $target.find(website.snippet.templateOptions[k].selector).each(function () {
+                                    var $snippet = $(this);
+                                    self.create_overlay($snippet);
+                                    if ($snippet.data("snippet-editor")) {
+                                        $snippet.data("snippet-editor").drop_and_build_snippet();
+                                    }
+                                });
+                            }
+                            // end
+
+                            self.make_active($target);
+                        },0);
+                    } else {
+                        $toInsert.remove();
+                    }
+                },
+            });
+        },
+
+        // return the original snippet in the editor bar from a snippet id (string)
+        get_snippet_from_id: function(id){
+            return $('.oe_snippet').filter(function(){
+                    return $(this).data('option') === id;
+                }).first();
+        },
+
+        // Create element insertion drop zones. two css selectors can be provided
+        // selector.children -> will insert drop zones as direct child of the selected elements
+        //   in case the selected elements have children themselves, dropzones will be interleaved
+        //   with them.
+        // selector.siblings -> will insert drop zones after and before selected elements
+        activate_insertion_zones: function(selector){
+            var self = this;
+            var child_selector = selector.children;
+            var sibling_selector = selector.siblings;
+
+            var zone_template = $("<div class='oe_drop_zone oe_insert'></div>");
+
+            if(child_selector){
+                self.$wrapwrap.find(child_selector).each(function (){
+                    var $zone = $(this);
+                    var vertical;
+                    var float = window.getComputedStyle(this).float;
+                    if (float === "left" || float === "right") {
+                        vertical = $zone.parent().outerHeight()+'px';
+                    }
+                    var $drop = zone_template.clone();
+                    if (vertical) {
+                        $drop.addClass("oe_vertical").css('height', vertical);
+                    }
+                    $zone.find('> *:not(.oe_drop_zone):visible').after($drop);
+                    $zone.prepend($drop.clone());
+                });
+            }
+
+            if(sibling_selector){
+                self.$wrapwrap.find(sibling_selector, true).each(function (){
+                    var $zone = $(this);
+                    var $drop, vertical;
+                    var float = window.getComputedStyle(this).float;
+                    if (float === "left" || float === "right") {
+                        vertical = $zone.parent().outerHeight()+'px';
+                    }
+
+                    if($zone.prev('.oe_drop_zone:visible').length === 0){
+                        $drop = zone_template.clone();
+                        if (vertical) {
+                            $drop.addClass("oe_vertical").css('height', vertical);
+                        }
+                        $zone.before($drop);
+                    }
+                    if($zone.next('.oe_drop_zone:visible').length === 0){
+                        $drop = zone_template.clone();
+                        if (vertical) {
+                            $drop.addClass("oe_vertical").css('height', vertical);
+                        }
+                        $zone.after($drop);
+                    }
+                });
+            }
+
+            var count;
+            do {
+                count = 0;
+                // var $zones = $('.oe_drop_zone + .oe_drop_zone');    // no two consecutive zones
+                // count += $zones.length;
+                // $zones.remove();
+
+                $zones = self.$wrapwrap.find('.oe_drop_zone > .oe_drop_zone:not(.oe_vertical)').remove();   // no recursive zones
+                count += $zones.length;
+                $zones.remove();
+            } while (count > 0);
+
+            // Cleaning consecutive zone and up zones placed between floating or inline elements. We do not like these kind of zones.
+            var $zones = self.$wrapwrap.find('.oe_drop_zone:not(.oe_vertical)');
+            $zones.each(function (){
+                var zone = $(this);
+                var prev = zone.prev();
+                var next = zone.next();
+                // remove consecutive zone
+                if (!zone.hasClass('.oe_vertical') && (prev.is('.oe_drop_zone:not(.oe_vertical)') || next.is('.oe_drop_zone:not(.oe_vertical)'))) {
+                    zone.remove();
+                    return;
+                }
+                var float_prev = prev.css('float')   || 'none';
+                var float_next = next.css('float')   || 'none';
+                var disp_prev  = prev.css('display') ||  null;
+                var disp_next  = next.css('display') ||  null;
+                if(     (float_prev === 'left' || float_prev === 'right')
+                    &&  (float_next === 'left' || float_next === 'right')  ){
+                    zone.remove();
+                }else if( !( disp_prev === null
+                          || disp_next === null
+                          || disp_prev === 'block'
+                          || disp_next === 'block' )){
+                    zone.remove();
+                }
+            });
+        },
+
+        // generate drop zones covering the elements selected by the selector
+        // we generate overlay drop zones only to get an idea of where the snippet are, the drop
+        activate_overlay_zones: function(selector){
+            var $targets = typeof selector === "string" ? this.$wrapwrap.find(selector) : selector;
+            var self = this;
+
+            function is_visible($el){
+                return     $el.css('display')    != 'none'
+                        && $el.css('opacity')    != '0'
+                        && $el.css('visibility') != 'hidden';
+            }
+
+            // filter out invisible elements
+            $targets = $targets.filter(function(){ return is_visible($(this)); });
+
+            // filter out elements with invisible parents
+            $targets = $targets.filter(function(){
+                var parents = $(this).parents().filter(function(){ return !is_visible($(this)); });
+                return parents.length === 0;
+            });
+
+            $targets.each(function () {
+                var $target = $(this);
+                if (!$target.data('overlay')) {
+                    var $zone = $(openerp.qweb.render('website.snippet_overlay'));
+
+                    // fix for pointer-events: none with ie9
+                    if (document.body && document.body.addEventListener) {
+                        $zone.on("click mousedown mousedown", function passThrough(event) {
+                            event.preventDefault();
+                            $target.each(function() {
+                               // check if clicked point (taken from event) is inside element
+                                event.srcElement = this;
+                                $(this).trigger(event.type);
+                            });
+                            return false;
+                        });
+                    }
+
+                    $zone.appendTo('#oe_manipulators');
+                    $zone.data('target',$target);
+                    $target.data('overlay',$zone);
+
+                    $target.on("DOMNodeInserted DOMNodeRemoved DOMSubtreeModified", function () {
+                        self.cover_target($zone, $target);
+                    });
+                    var resize = function () {
+                        if ($zone.parent().length) {
+                            self.cover_target($zone, $target);
+                        } else {
+                            $('body').off("resize", resize);
+                        }
+                    };
+                    $('body').on("resize", resize);
+                }
+                self.cover_target($target.data('overlay'), $target);
+            });
+            return $targets;
+        }
+    });
+
+
+    website.snippet.options = {};
+    website.snippet.Option = openerp.Class.extend({
+        // initialisation (don't overwrite)
+        init: function (BuildingBlock, editor, $target, option_id) {
+            this.BuildingBlock = BuildingBlock;
+            this.editor = editor;
+            this.$target = $target;
+            var option = website.snippet.templateOptions[option_id];
+            var styles = this.$target.data("snippet-option-ids") || {};
+            styles[option_id] = this;
+            this.$target.data("snippet-option-ids", styles);
+            this.$overlay = this.$target.data('overlay') || $('<div>');
+            this.option= option_id;
+            this.$el = option.$el.find(">li").clone();
+            this.data = option.$el.data();
+
+            this.set_active();
+            this.$target.on('snippet-option-reset', _.bind(this.set_active, this));
+            this._bind_li_menu();
+
+            this.start();
+        },
+
+        _bind_li_menu: function () {
+            this.$el.filter("li:hasData").find('a:first')
+                .off('mouseenter click')
+                .on('mouseenter click', _.bind(this._mouse, this));
+
+            this.$el
+                .off('mouseenter click', "li:hasData a")
+                .on('mouseenter click', "li:hasData a", _.bind(this._mouse, this));
+
+            this.$el.closest("ul").add(this.$el)
+                .off('mouseleave')
+                .on('mouseleave', _.bind(this.option_reset, this));
+
+            this.$el
+                .off('mouseleave', "ul")
+                .on('mouseleave', "ul", _.bind(this.option_reset, this));
+
+            this.reset_methods = [];
+            this.reset_time = null;
+        },
+
+        /**
+         * this method handles mouse:over and mouse:leave on the snippet editor menu
+         */
+         _time_mouseleave: null,
+        _mouse: function (event) {
+            var $next = $(event.currentTarget).parent();
+
+            // triggers preview or apply methods if a menu item has been clicked
+            this.option_select(event.type === "click" ? "click" : "over", $next);
+            if (event.type === 'click') {
+                this.set_active();
+                this.$target.trigger("snippet-option-change", [this]);
+            } else {
+                this.$target.trigger("snippet-option-preview", [this]);
+            }
+        },
+        /*
+        *  select and set item active or not (add highlight item and his parents)
+        *  called before start
+        */
+        set_active: function () {
+            var classes = _.uniq((this.$target.attr("class") || '').split(/\s+/));
+            this.$el.find('[data-toggle_class], [data-select_class]')
+                .add(this.$el)
+                .filter('[data-toggle_class], [data-select_class]')
+                .removeClass("active")
+                .filter('[data-toggle_class="' + classes.join('"], [data-toggle_class="') + '"] ,'+
+                    '[data-select_class="' + classes.join('"], [data-select_class="') + '"]')
+                .addClass("active");
+
+            if (!this.$el.find('li.active').size()) {
+                this.$el.find('li[data-select_class=""]').addClass("active");
+            } else {
+                this.$el.find('li[data-select_class=""]').removeClass("active");
+            }
+        },
+
+        start: function () {
+        },
+
+        on_focus : function () {
+            this._bind_li_menu();
+        },
+
+        on_blur : function () {
+        },
+
+        on_clone: function ($clone) {
+        },
+
+        on_remove: function () {
+        },
+
+        drop_and_build_snippet: function () {
+        },
+
+        option_reset: function (event) {
+            var self = this;
+            var lis = self.$el.add(self.$el.find('li')).filter('.active').get();
+            lis.reverse();
+            _.each(lis, function (li) {
+                var $li = $(li);
+                for (var k in self.reset_methods) {
+                    var method = self.reset_methods[k];
+                    if ($li.is('[data-'+method+']') || $li.closest('[data-'+method+']').size()) {
+                        delete self.reset_methods[k];
+                    }
+                }
+                self.option_select("reset", $li);
+            });
+
+            for (var k in self.reset_methods) {
+                var method = self.reset_methods[k];
+                if (method) {
+                    self[method]("reset", null);
+                }
+            }
+            self.reset_methods = [];
+            self.$target.trigger("snippet-option-reset", [this]);
+        },
+
+        // call data-method args as method
+        option_select: function (type, $li) {
+            var self = this,
+                $methods = [],
+                el = $li[0],
+                $el;
+            clearTimeout(this.reset_time);
+
+            function filter (k) { return k !== 'oeId' && k !== 'oeModel' && k !== 'oeField' && k !== 'oeXpath' && k !== 'oeSourceId';}
+            function hasData(el) {
+                for (var k in el.dataset) {
+                    if (filter (k)) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+            function method(el) {
+                var data = {};
+                for (var k in el.dataset) {
+                    if (filter (k)) {
+                        data[k] = el.dataset[k];
+                    }
+                }
+                return data;
+            }
+
+            while (el && this.$el.is(el) || _.some(this.$el.map(function () {return $.contains(this, el);}).get()) ) {
+                if (hasData(el)) {
+                    $methods.push(el);
+                }
+                el = el.parentNode;
+            }
+
+            $methods.reverse();
+
+            _.each($methods, function (el) {
+                var $el = $(el);
+                var methods = method(el);
+
+                for (var k in methods) {
+                    if (self[k]) {
+                        if (type !== "reset" && self.reset_methods.indexOf(k) === -1) {
+                            self.reset_methods.push(k);
+                        }
+                        self[k](type, methods[k], $el);
+                    } else {
+                        console.info("'"+self.__id__+"' snippet option has no method '"+k+"'");
+                    }
+                }
+            });
+        },
+
+        // default method for snippet
+        toggle_class: function (type, value, $li) {
+            var $lis = this.$el.find('[data-toggle_class]').add(this.$el).filter('[data-toggle_class]');
+
+            function map ($lis) {
+                return $lis.map(function () {return $(this).data("toggle_class");}).get().join(" ");
+            }
+            var classes = map($lis);
+            var active_classes = map($lis.filter('.active, :has(.active)'));
+
+            this.$target.removeClass(classes);
+            this.$target.addClass(active_classes);
+
+            if (type !== 'reset') {
+                this.$target.toggleClass(value);
+            }
+        },
+        select_class: function (type, value, $li) {
+            var $lis = this.$el.find('[data-select_class]').add(this.$el).filter('[data-select_class]');
+
+            var classes = $lis.map(function () {return $(this).data('select_class');}).get();
+
+            this.$target.removeClass(classes.join(" "));
+            if(value) this.$target.addClass(value);
+        },
+        eval: function (type, value, $li) {
+            var fn = new Function("node", "type", "value", "$li", value);
+            fn.call(this, this, type, value, $li);
+        },
+
+        clean_for_save: dummy
+    });
+
+
+    // backward compatibility for odoo 8.0
+
+    function backward_compatibility_80 ($html) {
+        $html.find("[data-snippet-option-id]").each(function () {
+            // console.info("Please use the new Snippet & BuildingBlock API", $(this).data("snippet-option-id"));
+
+            var $option = $(this);
+            var copy = $option.data();
+            var data = {};
+            for (var k in copy) {
+                data[k] = copy[k];
+            }
+            var required = false;
+
+            $option.removeAttr("data-snippet-option-id");
+            $option.removeData("snippet-option-id");
+            $option.attr("data-js", data.snippetOptionId);
+            $option.data("js", data.snippetOptionId);
+
+            $option.removeAttr("data-selector-children");
+            $option.removeAttr("data-selector-vertical-children");
+            $option.removeData("selector-children");
+            $option.removeData("selector-vertical-children");
+            $option.attr("data-drop-in", data.selectorChildren || data.selectorVerticalChildren);
+            $option.data("drop-in", data.selectorChildren || data.selectorVerticalChildren);
+
+            $option.removeAttr("data-selector-siblings");
+            $option.removeData("selector-siblings");
+            $option.attr("data-drop-near", data.selectorSiblings);
+            $option.data("drop-near", data.selectorSiblings);
+
+            $("li:hasData", this).each(function () {
+                var $li = $(this);
+                var copy = _.clone($li.data());
+                var data = {};
+                for (var k in copy) {
+                    $li.removeAttr("data-"+k);
+                    $li.removeData(k);
+
+                    if (k === "required") {
+                        required = true;
+                    } else if (k === "value") {
+                        if (isNaN(+copy[k]) || !copy[k].length) {
+                            data[required ? "select_class" : "toggle_class"] = copy[k];
+                        }
+                    } else if (k === "src") {
+                        data["background"] = copy[k];
+                    } else {
+                        data[k] = copy[k];
+                    }
+                }
+
+                for (var k in data) {
+                    $li.data(k, data[k]).attr("data-" + k, data[k]);
+                    $li.find("li").each(function () {
+                        $(this).data(k, data[k]).attr("data-" + k, data[k]);
+                    });
+                }
+            });
+
+            // carousel
+            $("li:has(a.js_add)", this).each(function () {
+                $(this).attr("data-add_slide", true);
+                $(this).data("add_slide", true);
+            });
+            $("li:has(a.js_remove)", this).each(function () {
+                $(this).attr("data-remove_slide", true);
+                $(this).data("remove_slide", true);
+            });
+
+            // media
+            $("li:has(a.edition)", this).each(function () {
+                $(this).attr("data-edition", true);
+                $(this).data("edition", true);
+            });
+            // transform
+            $("li:has(a.style)", this).each(function () {
+                $(this).attr("data-style", true);
+                $(this).data("style", true);
+            });
+            $("li:has(a.clear-style)", this).each(function () {
+                $(this).attr("data-clear_style", true);
+                $(this).data("clear_style", true);
+            });
+        });
+    }
+    website.snippet.Option.include({
+        onFocus : function () {
+            this._bind_li_menu();
+        },
+        onBlur : function () {
+        },
+        on_focus : function () {
+            this.onFocus();
+        },
+        on_blur : function () {
+            this.onBlur();
+        },
+
+        value: function (type, value, $li) {
+            if (this.scroll) {
+                this.scroll(type, value, $li);
+            }
+            if (!$li) {
+                $li = this.$el.find('li.active');
+            }
+
+            var np = {
+                $next: $li,
+                $prev: this.$li_prev
+            };
+            this.$li_prev = $li;
+
+            if (type === "reset") {
+                np.$li = null;
+                if ($li.data("required")) {
+                    return;
+                }
+            }
+            if (type !== "click") {
+                if (this.preview) {
+                    this.preview(np);
+                }
+            } else if (this.select) {
+                this.select(np);
+            }
+        }
+    });
+
+    // end
+
+
+    website.snippet.options.background = website.snippet.Option.extend({
+        start: function () {
+            this._super();
+            var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
+            if (this.$target.hasClass('oe_custom_bg')) {
+                this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src);
+            }
+        },
+        background: function(type, value, $li) {
+            if (value && value.length) {
+                this.$target.css("background-image", 'url(' + value + ')');
+                this.$target.addClass("oe_img_bg");
+            } else {
+                this.$target.css("background-image", "");
+                this.$target.removeClass("oe_img_bg").removeClass("oe_custom_bg");
+            }
+        },
+        select_class: function (type, value, $li) {
+            this.background(type, "", $li);
+            this._super(type, value, $li);
+        },
+        choose_image: function(type, value, $li) {
+            if(type !== "click") return;
+
+            var self = this;
+            var $image = $('<img class="hidden"/>');
+            $image.attr("src", value);
+            $image.appendTo(self.$target);
+
+            self.element = new CKEDITOR.dom.element($image[0]);
+            var editor = new website.editor.MediaDialog(self, self.element);
+            editor.appendTo(document.body);
+            editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+
+            $image.on('saved', self, function (o) {
+                var value = $image.attr("src");
+                self.$target.css("background-image", 'url(' + value + ')');
+                self.$el.find('li[data-choose_image]').data("background", value).attr("data-background", value);
+                self.$target.trigger("snippet-option-change", [self]);
+                $image.remove();
+                self.$target.addClass('oe_custom_bg oe_img_bg');
+                self.set_active();
+            });
+            editor.on('cancel', self, function () {
+                self.$target.trigger("snippet-option-change", [self]);
+                $image.remove();
+            });
+        },
+        set_active: function () {
+            var self = this;
+            var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
+            this.$el.find('li.active').removeClass('active');
+            this._super();
+
+            if (this.$el.find('li[data-select_class!=""].active').length) {
+                return;
+            }
+            this.$el.find('li[data-background]:not([data-background=""])')
+                .removeClass("active")
+                .each(function () {
+                    var background = $(this).data("background") || $(this).attr("data-background");
+                    if ((src.length && background.length && src.indexOf(background) !== -1) || (!src.length && !background.length)) {
+                        $(this).addClass("active");
+                    }
+                });
+
+            if (!this.$el.find('li.active').size()) {
+                if (src.length) {
+                    this.$el.find('li[data-choose_image]').addClass("active");
+                } else {
+                    this.$el.find('li[data-select_class=""], li[data-background=""]').addClass("active");
+                }
+            } else {
+                this.$el.find('li[data-choose_image]').removeClass("active");
+            }
+        }
+    });
+
+    website.snippet.options.colorpicker = website.snippet.Option.extend({
+        start: function () {
+            var self = this;
+            var res = this._super();
+
+            this.$el.find('li:empty').append( openerp.qweb.render('website_less.colorpicker') );
+
+            if (this.$el.data('area')) {
+                this.$target = this.$target.find(this.$el.data('area'));
+                this.$el.removeData('area').removeAttr('area');
+            }
+
+            var classes = [];
+            this.$el.find("table.colorpicker td > *").map(function () {
+                var $color = $(this);
+                var color = $color.attr("class");
+                if (self.$target.hasClass(color)) {
+                    self.color = color;
+                    $color.parent().addClass("selected");
+                }
+                classes.push(color);
+            });
+            this.classes = classes.join(" ");
+
+            this.bind_events();
+            return res;
+        },
+        bind_events: function () {
+            var self = this;
+            var $td = this.$el.find("table.colorpicker td");
+            var $colors = $td.children(':not(hr)');
+            $colors
+                .mouseenter(function () {
+                    self.$target.removeClass(self.classes).addClass($(this).attr("class"));
+                })
+                .mouseleave(function () {
+                    self.$target.removeClass(self.classes).addClass($td.filter(".selected").children().attr("class"));
+                })
+                .click(function () {
+                    $td.removeClass("selected");
+                    $(this).parent().addClass("selected");
+                });
+        }
+    });
+
+    website.snippet.options.slider = website.snippet.Option.extend({
+        unique_id: function () {
+            var id = 0;
+            $(".carousel").each(function () {
+                var cid = 1 + parseInt($(this).attr("id").replace(/[^0123456789]/g, ''),10);
+                if (id < cid) id = cid;
+            });
+            return "myCarousel" + id;
+        },
+        drop_and_build_snippet: function() {
+            this.id = this.unique_id();
+            this.$target.attr("id", this.id);
+            this.$target.find("[data-slide]").attr("data-cke-saved-href", "#" + this.id);
+            this.$target.find("[data-target]").attr("data-target", "#" + this.id);
+            this.rebind_event();
+        },
+        on_clone: function ($clone) {
+            var id = this.unique_id();
+            $clone.attr("id", id);
+            $clone.find("[data-slide]").attr("href", "#" + id);
+            $clone.find("[data-slide-to]").attr("data-target", "#" + id);
+        },
+        // rebind event to active carousel on edit mode
+        rebind_event: function () {
+            var self = this;
+            this.$target.find('.carousel-indicators [data-slide-to]').off('click').on('click', function () {
+                self.$target.carousel(+$(this).data('slide-to')); });
+
+            this.$target.attr('contentEditable', 'false');
+            this.$target.find('.oe_structure, .content.row, [data-slide]').attr('contentEditable', 'true');
+        },
+        clean_for_save: function () {
+            this._super();
+            this.$target.find(".item").removeClass("next prev left right active")
+                .first().addClass("active");
+            this.$indicators.find('li').removeClass('active')
+                .first().addClass("active");
+        },
+        start : function () {
+            var self = this;
+            this._super();
+            this.$target.carousel({interval: false});
+            this.id = this.$target.attr("id");
+            this.$inner = this.$target.find('.carousel-inner');
+            this.$indicators = this.$target.find('.carousel-indicators');
+            this.$target.carousel('pause');
+            this.rebind_event();
+        },
+        add_slide: function (type, value) {
+            if(type !== "click") return;
+
+            var self = this;
+            var cycle = this.$inner.find('.item').length;
+            var $active = this.$inner.find('.item.active, .item.prev, .item.next').first();
+            var index = $active.index();
+            this.$target.find('.carousel-control, .carousel-indicators').removeClass("hidden");
+            this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>');
+
+            var $clone = this.$target.find('.item.active').clone();
+
+            // insert
+            $clone.removeClass('active').insertAfter($active);
+            setTimeout(function() {
+                self.$target.carousel().carousel(++index);
+                self.rebind_event();
+            },0);
+            return $clone;
+        },
+        remove_slide: function (type, value) {
+            if(type !== "click") return;
+
+            if (this.remove_process) {
+                return;
+            }
+            var self = this;
+            var new_index = 0;
+            var cycle = this.$inner.find('.item').length - 1;
+            var index = this.$inner.find('.item.active').index();
+
+            if (cycle > 0) {
+                this.remove_process = true;
+                var $el = this.$inner.find('.item.active');
+                self.$target.on('slid.bs.carousel', function (event) {
+                    $el.remove();
+                    self.$indicators.find("li:last").remove();
+                    self.$target.off('slid.bs.carousel');
+                    self.rebind_event();
+                    self.remove_process = false;
+                    if (cycle == 1) {
+                        self.on_remove_slide(event);
+                    }
+                });
+                setTimeout(function () {
+                    self.$target.carousel( index > 0 ? --index : cycle );
+                }, 500);
+            } else {
+                this.$target.find('.carousel-control, .carousel-indicators').addClass("hidden");
+            }
+        },
+        interval : function(type, value) {
+            this.$target.attr("data-interval", value);
+        },
+        set_active: function () {
+            this.$el.find('li[data-interval]').removeClass("active")
+                .filter('li[data-interval='+this.$target.attr("data-interval")+']').addClass("active");
+        },
+    });
+    website.snippet.options.carousel = website.snippet.options.slider.extend({
+        getSize: function () {
+            this.grid = this._super();
+            this.grid.size = 8;
+            return this.grid;
+        },
+        clean_for_save: function () {
+            this._super();
+            this.$target.removeClass('oe_img_bg ' + this._class).css("background-image", "");
+        },
+        load_style_options : function () {
+            this._super();
+            $(".snippet-option-size li[data-value='']").remove();
+        },
+        start : function () {
+            var self = this;
+            this._super();
+
+            // set background and prepare to clean for save
+            var add_class = function (c){
+                if (c) self._class = (self._class || "").replace(new RegExp("[ ]+" + c.replace(" ", "|[ ]+")), '') + ' ' + c;
+                return self._class || "";
+            };
+            this.$target.on('slid.bs.carousel', function () {
+                if(self.editor && self.editor.styles.background) {
+                    self.editor.styles.background.$target = self.$target.find(".item.active");
+                    self.editor.styles.background.set_active();
+                }
+                self.$target.carousel("pause");
+            });
+            this.$target.trigger('slid.bs.carousel');
+        },
+        add_slide: function (type, data) {
+            if(type !== "click") return;
+
+            var $clone = this._super(type, data);
+            // choose an other background
+            var bg = this.$target.data("snippet-option-ids").background;
+            if (!bg) return $clone;
+
+            var $styles = bg.$el.find("li[data-background]");
+            var $select = $styles.filter(".active").removeClass("active").next("li[data-background]");
+            if (!$select.length) {
+                $select = $styles.first();
+            }
+            $select.addClass("active");
+            $clone.css("background-image", $select.data("background") ? "url('"+ $select.data("background") +"')" : "");
+
+            return $clone;
+        },
+        // rebind event to active carousel on edit mode
+        rebind_event: function () {
+            var self = this;
+            this.$target.find('.carousel-control').off('click').on('click', function () {
+                self.$target.carousel( $(this).data('slide')); });
+            this._super();
+
+            /* Fix: backward compatibility saas-3 */
+            this.$target.find('.item.text_image, .item.image_text, .item.text_only').find('.container > .carousel-caption > div, .container > img.carousel-image').attr('contentEditable', 'true');
+        },
+    });
+    website.snippet.options.marginAndResize = website.snippet.Option.extend({
+        start: function () {
+            var self = this;
+            this._super();
+
+            var resize_values = this.getSize();
+            if (resize_values.n) this.$overlay.find(".oe_handle.n").removeClass("readonly");
+            if (resize_values.s) this.$overlay.find(".oe_handle.s").removeClass("readonly");
+            if (resize_values.e) this.$overlay.find(".oe_handle.e").removeClass("readonly");
+            if (resize_values.w) this.$overlay.find(".oe_handle.w").removeClass("readonly");
+            if (resize_values.size) this.$overlay.find(".oe_handle.size").removeClass("readonly");
+
+            this.$overlay.find(".oe_handle:not(.size), .oe_handle.size .size").on('mousedown', function (event){
+                event.preventDefault();
+
+                var $handle = $(this);
+
+                var resize_values = self.getSize();
+                var compass = false;
+                var XY = false;
+                if ($handle.hasClass('n')) {
+                    compass = 'n';
+                    XY = 'Y';
+                }
+                else if ($handle.hasClass('s')) {
+                    compass = 's';
+                    XY = 'Y';
+                }
+                else if ($handle.hasClass('e')) {
+                    compass = 'e';
+                    XY = 'X';
+                }
+                else if ($handle.hasClass('w')) {
+                    compass = 'w';
+                    XY = 'X';
+                }
+                else if ($handle.hasClass('size')) {
+                    compass = 'size';
+                    XY = 'Y';
+                }
+
+                var resize = resize_values[compass];
+                if (!resize) return;
+
+
+                if (compass === 'size') {
+                    var offset = self.$target.offset().top;
+                    if (self.$target.css("background").match(/rgba\(0, 0, 0, 0\)/)) {
+                        self.$target.addClass("resize_editor_busy");
+                    }
+                } else {
+                    var xy = event['page'+XY];
+                    var current = resize[2] || 0;
+                    _.each(resize[0], function (val, key) {
+                        if (self.$target.hasClass(val)) {
+                            current = key;
+                        }
+                    });
+                    var begin = current;
+                    var beginClass = self.$target.attr("class");
+                    var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
+                }
+
+                self.BuildingBlock.editor_busy = true;
+
+                var cursor = $handle.css("cursor")+'-important';
+                var $body = $(document.body);
+                $body.addClass(cursor);
+
+                var body_mousemove = function (event){
+                    event.preventDefault();
+                    if (compass === 'size') {
+                        var dy = event.pageY-offset;
+                        dy = dy - dy%resize;
+                        if (dy <= 0) dy = resize;
+                        self.$target.css("height", dy+"px");
+                        self.$target.css("overflow", "hidden");
+                        self.on_resize(compass, null, dy);
+                        self.BuildingBlock.cover_target(self.$overlay, self.$target);
+                        return;
+                    }
+                    var dd = event['page'+XY] - xy + resize[1][begin];
+                    var next = current+1 === resize[1].length ? current : (current+1);
+                    var prev = current ? (current-1) : 0;
+
+                    var change = false;
+                    if (dd > (2*resize[1][next] + resize[1][current])/3) {
+                        self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
+                        self.$target.addClass(resize[0][next]);
+                        current = next;
+                        change = true;
+                    }
+                    if (prev != current && dd < (2*resize[1][prev] + resize[1][current])/3) {
+                        self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
+                        self.$target.addClass(resize[0][prev]);
+                        current = prev;
+                        change = true;
+                    }
+
+                    if (change) {
+                        self.on_resize(compass, beginClass, current);
+                        self.BuildingBlock.cover_target(self.$overlay, self.$target);
+                    }
+                };
+
+                var body_mouseup = function(){
+                    $body.unbind('mousemove', body_mousemove);
+                    $body.unbind('mouseup', body_mouseup);
+                    $body.removeClass(cursor);
+                    self.BuildingBlock.editor_busy = false;
+                    self.$target.removeClass("resize_editor_busy");
+                };
+                $body.mousemove(body_mousemove);
+                $body.mouseup(body_mouseup);
+            });
+            this.$overlay.find(".oe_handle.size .auto_size").on('click', function (event){
+                self.$target.css("height", "");
+                self.$target.css("overflow", "");
+                self.BuildingBlock.cover_target(self.$overlay, self.$target);
+                return false;
+            });
+        },
+        getSize: function () {
+            this.grid = {};
+            return this.grid;
+        },
+
+        on_focus : function () {
+            this._super();
+            this.change_cursor();
+        },
+
+        change_cursor : function () {
+            var _class = this.$target.attr("class") || "";
+
+            var col = _class.match(/col-md-([0-9-]+)/i);
+            col = col ? +col[1] : 0;
+
+            var offset = _class.match(/col-md-offset-([0-9-]+)/i);
+            offset = offset ? +offset[1] : 0;
+
+            var overlay_class = this.$overlay.attr("class").replace(/(^|\s+)block-[^\s]*/gi, '');
+            if (col+offset >= 12) overlay_class+= " block-e-right";
+            if (col === 1) overlay_class+= " block-w-right block-e-left";
+            if (offset === 0) overlay_class+= " block-w-left";
+
+            var mb = _class.match(/mb([0-9-]+)/i);
+            mb = mb ? +mb[1] : 0;
+            if (mb >= 128) overlay_class+= " block-s-bottom";
+            else if (!mb) overlay_class+= " block-s-top";
+
+            var mt = _class.match(/mt([0-9-]+)/i);
+            mt = mt ? +mt[1] : 0;
+            if (mt >= 128) overlay_class+= " block-n-top";
+            else if (!mt) overlay_class+= " block-n-bottom";
+
+            this.$overlay.attr("class", overlay_class);
+        },
+
+        /* on_resize
+        *  called when the box is resizing and the class change, before the cover_target
+        *  @compass: resize direction : 'n', 's', 'e', 'w'
+        *  @beginClass: attributes class at the begin
+        *  @current: curent increment in this.grid
+        */
+        on_resize: function (compass, beginClass, current) {
+            this.change_cursor();
+        }
+    });
+    website.snippet.options["margin-y"] = website.snippet.options.marginAndResize.extend({
+        getSize: function () {
+            this.grid = this._super();
+            var grid = [0,4,8,16,32,48,64,92,128];
+            this.grid = {
+                // list of class (Array), grid (Array), default value (INT)
+                n: [_.map(grid, function (v) {return 'mt'+v;}), grid],
+                s: [_.map(grid, function (v) {return 'mb'+v;}), grid],
+                // INT if the user can resize the snippet (resizing per INT px)
+                size: null
+            };
+            return this.grid;
+        },
+    });
+    website.snippet.options["margin-x"] = website.snippet.options.marginAndResize.extend({
+        getSize: function () {
+            this.grid = this._super();
+            var width = this.$target.parents(".row:first").first().outerWidth();
+
+            var grid = [1,2,3,4,5,6,7,8,9,10,11,12];
+            this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})];
+
+            var grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11];
+            this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12];
+
+            return this.grid;
+        },
+        _drag_and_drop_after_insert_dropzone: function(){
+            var self = this;
+            var $zones = $(".row:has(> .oe_drop_zone)").each(function () {
+                var $row = $(this);
+                var width = $row.innerWidth();
+                var pos = 0;
+                while (width > pos + self.size.width) {
+                    var $last = $row.find("> .oe_drop_zone:last");
+                    $last.each(function () {
+                        pos = $(this).position().left;
+                    });
+                    if (width > pos + self.size.width) {
+                        $row.append("<div class='col-md-1 oe_drop_to_remove'/>");
+                        var $add_drop = $last.clone();
+                        $row.append($add_drop);
+                        self._drag_and_drop_active_drop_zone($add_drop);
+                    }
+                }
+            });
+        },
+        _drag_and_drop_start: function () {
+            this._super();
+            this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, ''));
+        },
+        _drag_and_drop_stop: function () {
+            this.$target.addClass("col-md-offset-" + this.$target.prevAll(".oe_drop_to_remove").length);
+            this._super();
+        },
+        hide_remove_button: function() {
+            this.$overlay.find('.oe_snippet_remove').toggleClass("hidden", !this.$target.siblings().length);
+        },
+        on_focus : function () {
+            this._super();
+            this.hide_remove_button();
+        },
+        on_clone: function ($clone) {
+            var _class = $clone.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, '');
+            $clone.attr("class", _class);
+            this.hide_remove_button();
+            return false;
+        },
+        on_remove: function () {
+            this._super();
+            this.hide_remove_button();
+        },
+        on_resize: function (compass, beginClass, current) {
+            if (compass === 'w') {
+                // don't change the right border position when we change the offset (replace col size)
+                var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0);
+                var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0);
+                var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0);
+                if (offset < 0) {
+                    offset = 0;
+                }
+                var colSize = beginCol - (offset - beginOffset);
+                if (colSize <= 0) {
+                    colSize = 1;
+                    offset = beginOffset + beginCol - 1;
+                }
+                this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, ''));
+
+                this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize));
+                if (offset > 0) {
+                    this.$target.addClass('col-md-offset-' + offset);
+                }
+            }
+            this._super(compass, beginClass, current);
+        },
+    });
+
+    website.snippet.options.resize = website.snippet.options.marginAndResize.extend({
+        getSize: function () {
+            this.grid = this._super();
+            this.grid.size = 8;
+            return this.grid;
+        },
+    });
+
+    website.snippet.options.parallax = website.snippet.Option.extend({
+        getSize: function () {
+            this.grid = this._super();
+            this.grid.size = 8;
+            return this.grid;
+        },
+        on_resize: function (compass, beginClass, current) {
+            this.$target.data("snippet-view").set_values();
+        },
+        start : function () {
+            var self = this;
+            this._super();
+            if (!self.$target.data("snippet-view")) {
+                this.$target.data("snippet-view", new website.snippet.animationRegistry.parallax(this.$target));
+            }
+            this.scroll();
+            this.$target.on('snippet-option-change snippet-option-preview', function () {
+                self.$target.data("snippet-view").set_values();
+            });
+            this.$target.attr('contentEditable', 'false');
+            this.$target.children().children().attr('contentEditable', 'true');
+        },
+        scroll: function (type, value) {
+            this.$target.attr('data-scroll-background-ratio', value);
+            this.$target.data("snippet-view").set_values();
+        },
+        set_active: function () {
+            var value = this.$target.attr('data-scroll-background-ratio') || 0;
+            this.$el.find('[data-scroll]').removeClass("active")
+                .filter('[data-scroll="' + (this.$target.attr('data-scroll-background-ratio') || 0) + '"]').addClass("active");
+        },
+        clean_for_save: function () {
+            this._super();
+            this.$target.find(".parallax")
+                .css("background-position", '')
+                .removeAttr("data-scroll-background-offset");
+        }
+    });
+
+    website.snippet.options.transform = website.snippet.Option.extend({
+        start: function () {
+            var self = this;
+            this._super();
+            this.$overlay.find('.oe_snippet_clone, .oe_handles').addClass('hidden');
+            this.$overlay.find('[data-toggle="dropdown"]')
+                .on("mousedown", function () {
+                    self.$target.transfo("hide");
+                });
+        },
+        style: function (type, value) {
+            if (type !== 'click') return;
+            var settings = this.$target.data("transfo").settings;
+            this.$target.transfo({ hide: (settings.hide = !settings.hide) });
+        },
+        clear_style: function (type, value) {
+            if (type !== 'click') return;
+            this.$target.removeClass("fa-spin").attr("style", "");
+            this.resetTransfo();
+        },
+        resetTransfo: function () {
+            var self = this;
+            this.$target.transfo("destroy");
+            this.$target.transfo({
+                hide: true,
+                callback: function () {
+                    var center = $(this).data("transfo").$markup.find('.transfo-scaler-mc').offset();
+                    var $option = self.$overlay.find('.btn-group:first');
+                    self.$overlay.css({
+                        'top': center.top - $option.height()/2,
+                        'left': center.left,
+                        'position': 'absolute',
+                    });
+                    self.$overlay.find(".oe_overlay_options").attr("style", "width:0; left:0!important; top:0;");
+                    self.$overlay.find(".oe_overlay_options > .btn-group").attr("style", "width:160px; left:-80px;");
+                }});
+            this.$target.data('transfo').$markup
+                .on("mouseover", function () {
+                    self.$target.trigger("mouseover");
+                })
+                .mouseover();
+        },
+        on_focus : function () {
+            this.resetTransfo();
+        },
+        on_blur : function () {
+            this.$target.transfo("hide");
+        },
+    });
+
+    website.snippet.options.media = website.snippet.Option.extend({
+        start: function () {
+            var self = this;
+            this._super();
+
+            website.snippet.start_animation(true, this.$target);
+
+            $(document.body).on("media-saved", self, function (event, prev , item) {
+                self.editor.on_blur();
+                self.BuildingBlock.make_active(false);
+                if (self.$target.parent().data("oe-field") !== "image") {
+                    self.BuildingBlock.make_active($(item));
+                }
+            });
+        },
+        edition: function (type, value) {
+            if(type !== "click") return;
+            this.element = new CKEDITOR.dom.element(this.$target[0]);
+            new website.editor.MediaDialog(this, this.element).appendTo(document.body);
+        },
+        on_focus : function () {
+            var self = this;
+            if (this.$target.parent().data("oe-field") === "image") {
+                this.$overlay.addClass("hidden");
+                self.element = new CKEDITOR.dom.element(self.$target[0]);
+                new website.editor.MediaDialog(self, self.element).appendTo(document.body);
+                self.BuildingBlock.make_active(false);
+            }
+            setTimeout(function () {
+                self.$target.find(".css_editable_mode_display").removeAttr("_moz_abspos");
+            },0);
+        },
+    });
+
+    website.snippet.Editor = openerp.Class.extend({
+        init: function (BuildingBlock, dom) {
+            this.BuildingBlock = BuildingBlock;
+            this.$target = $(dom);
+            this.$overlay = this.$target.data('overlay');
+            this.load_style_options();
+            this.get_parent_block();
+            this.start();
+        },
+
+        // activate drag and drop for the snippets in the snippet toolbar
+        _drag_and_drop: function(){
+            var self = this;
+            this.dropped = false;
+            this.$overlay.draggable({
+                greedy: true,
+                appendTo: 'body',
+                cursor: "move",
+                handle: ".oe_snippet_move",
+                cursorAt: {
+                    left: 18,
+                    top: 14
+                },
+                helper: function() {
+                    var $clone = $(this).clone().css({width: "24px", height: "24px", border: 0});
+                    $clone.find(".oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle").remove();
+                    $clone.find(":not(.glyphicon)").css({position: 'absolute', top: 0, left: 0});
+                    $clone.appendTo("body").removeClass("hidden");
+                    return $clone;
+                },
+                start: _.bind(self._drag_and_drop_start, self),
+                stop: _.bind(self._drag_and_drop_stop, self)
+            });
+        },
+        _drag_and_drop_after_insert_dropzone: function (){},
+        _drag_and_drop_active_drop_zone: function ($zones){
+            var self = this;
+            $zones.droppable({
+                over:   function(){
+                    $(".oe_drop_zone.hide").removeClass("hide");
+                    $(this).addClass("hide").first().after(self.$target);
+                    self.dropped = true;
+                },
+                out:    function(){
+                    $(this).removeClass("hide");
+                    self.$target.detach();
+                    self.dropped = false;
+                },
+            });
+        },
+        _drag_and_drop_start: function (){
+            var self = this;
+            self.BuildingBlock.hide();
+            self.BuildingBlock.editor_busy = true;
+            self.size = {
+                width: self.$target.width(),
+                height: self.$target.height()
+            };
+            self.$target.after("<div class='oe_drop_clone' style='display: none;'/>");
+            self.$target.detach();
+            self.$overlay.addClass("hidden");
+
+            self.BuildingBlock.activate_insertion_zones({
+                siblings: self.selector_siblings,
+                children: self.selector_children,
+            });
+
+            $("body").addClass('move-important');
+
+            self._drag_and_drop_after_insert_dropzone();
+            self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
+        },
+        _drag_and_drop_stop: function (){
+            var self = this;
+            if (!self.dropped) {
+                $(".oe_drop_clone").after(self.$target);
+            }
+            self.$overlay.removeClass("hidden");
+            $("body").removeClass('move-important');
+            $('.oe_drop_zone').droppable('destroy').remove();
+            $(".oe_drop_clone, .oe_drop_to_remove").remove();
+            self.BuildingBlock.editor_busy = false;
+            self.get_parent_block();
+            setTimeout(function () {self.BuildingBlock.create_overlay(self.$target);},0);
+        },
+
+        load_style_options: function () {
+            var self = this;
+            var $styles = this.$overlay.find('.oe_options');
+            var $ul = $styles.find('ul:first');
+            this.styles = {};
+            this.selector_siblings = [];
+            this.selector_children = [];
+            _.each(website.snippet.templateOptions, function (val, option_id) {
+                if (!self.$target.is(val.selector)) {
+                    return;
+                }
+                if (val['drop-near']) self.selector_siblings.push(val['drop-near']);
+                if (val['drop-in']) self.selector_children.push(val['drop-in']);
+
+                var option = val['option'];
+                var Editor = website.snippet.options[option] || website.snippet.Option;
+                var editor = self.styles[option] = new Editor(self.BuildingBlock, self, self.$target, option_id);
+                editor.__id__ = option;
+                $ul.append(editor.$el.addClass("snippet-option-" + option));
+            });
+            this.selector_siblings = this.selector_siblings.join(",");
+            if (this.selector_siblings === "")
+                this.selector_siblings = false;
+            this.selector_children = this.selector_children.join(",");
+            if (this.selector_children === "")
+                this.selector_children = false;
+
+            if (!this.selector_siblings && !this.selector_children) {
+                this.$overlay.find(".oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove").addClass('hidden');
+            }
+
+
+            if ($ul.find("li").length) {
+                $styles.removeClass("hidden");
+            }
+            this.$overlay.find('[data-toggle="dropdown"]').dropdown();
+        },
+
+        get_parent_block: function () {
+            var self = this;
+            var $button = this.$overlay.find('.oe_snippet_parent');
+            var $parent = this.$target.parents(website.snippet.globalSelector).first();
+            if ($parent.length) {
+                $button.removeClass("hidden");
+                $button.off("click").on('click', function (event) {
+                    event.preventDefault();
+                    setTimeout(function () {
+                        self.BuildingBlock.make_active($parent);
+                    }, 0);
+                });
+            } else {
+                $button.addClass("hidden");
+            }
+        },
+
+        /*
+        *  start
+        *  This method is called after init and _readXMLData
+        */
+        start: function () {
+            var self = this;
+            this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this));
+            this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this));
+            this._drag_and_drop();
+        },
+
+        on_clone: function () {
+            var $clone = this.$target.clone(false);
+            this.$target.after($clone);
+            for (var i in this.styles){
+                this.styles[i].on_clone($clone);
+            }
+            return false;
+        },
+
+        on_remove: function () {
+            this.on_blur();
+            var index = _.indexOf(this.BuildingBlock.snippets, this.$target.get(0));
+            for (var i in this.styles){
+                this.styles[i].on_remove();
+            }
+            delete this.BuildingBlock.snippets[index];
+
+            // remove node and his empty
+            var parent,
+                node = this.$target.parent()[0];
+
+            this.$target.remove();
+            function check(node) {
+                if ($(node).outerHeight() > 8) {
+                    return false;
+                }
+                for (var k=0; k<node.children.length; k++) {
+                    if (node.children[k].tagName || node.children[k].textContent.match(/[^\s]/)) {
+                        return false;
+                    }
+                }
+                return true;
+            }
+            while (check(node)) {
+                parent = node.parentNode;
+                parent.removeChild(node);
+                node = parent;
+            }
+
+            this.$overlay.remove();
+            return false;
+        },
+
+        /*
+        *  drop_and_build_snippet
+        *  This method is called just after that a thumbnail is drag and dropped into a drop zone
+        *  (after the insertion of this.$body, if this.$body exists)
+        */
+        drop_and_build_snippet: function () {
+            for (var i in this.styles){
+                this.styles[i].drop_and_build_snippet();
+            }
+        },
+
+        /* on_focus
+        *  This method is called when the user click inside the snippet in the dom
+        */
+        on_focus : function () {
+            this.$overlay.addClass('oe_active');
+            for (var i in this.styles){
+                this.styles[i].on_focus();
+            }
+        },
+
+        /* on_focus
+        *  This method is called when the user click outside the snippet in the dom, after a focus
+        */
+        on_blur : function () {
+            for (var i in this.styles){
+                this.styles[i].on_blur();
+            }
+            this.$overlay.removeClass('oe_active');
+        },
+    });
+
+})();

+ 289 - 0
static/src/js/website.theme.js

@@ -0,0 +1,289 @@
+(function () {
+    'use strict';
+
+    openerp.jsonRpc('/web/dataset/call', 'call', {
+            'model': 'ir.ui.view',
+            'method': 'read_template',
+            'args': ['website_less.theme_customize', openerp.website.get_context()]
+        }).done(function (data) {
+        openerp.qweb.add_template(data);
+    });
+    openerp.jsonRpc('/web/dataset/call', 'call', {
+            'model': 'ir.ui.view',
+            'method': 'read_template',
+            'args': ['website_less.colorpicker', openerp.website.get_context()]
+        }).done(function (data) {
+        openerp.qweb.add_template(data);
+    });
+
+    openerp.website.Theme = openerp.Widget.extend({
+        template: 'website_less.theme_customize',
+        events: {
+            'change input[data-xmlid],input[data-enable],input[data-disable]': 'change_selection',
+            'click .close': 'close',
+            'click': 'click',
+        },
+        start: function () {
+            var self = this;
+            this.timer = null;
+            this.reload = false;
+            this.flag = false;
+            this.$el.addClass("theme_customize_modal");
+            this.active_select_tags();
+            this.$inputs = $("input[data-xmlid],input[data-enable],input[data-disable]");
+            setTimeout(function () {self.$el.addClass('in');}, 0);
+            this.keydown_escape = function (event) {
+                if (event.keyCode === 27) {
+                    self.close();
+                }
+            };
+            $(document).on('keydown', this.keydown_escape);
+            return this.load_xml_data().then(function () {
+                self.flag = true;
+            });
+        },
+        active_select_tags: function () {
+            var uniqueID = 0;
+            var self = this;
+            var $selects = this.$('select:has(option[data-xmlid],option[data-enable],option[data-disable])');
+            $selects.each(function () {
+                uniqueID++;
+                var $select = $(this);
+                var $options = $select.find('option[data-xmlid], option[data-enable], option[data-disable]');
+                $options.each(function () {
+                    var $option = $(this);
+                    var $input = $('<input style="display: none;" type="radio" name="theme_customize_modal-select-'+uniqueID+'"/>');
+                    $input.attr('id', $option.attr('id'));
+                    $input.attr('data-xmlid', $option.data('xmlid'));
+                    $input.attr('data-enable', $option.data('enable'));
+                    $input.attr('data-disable', $option.data('disable'));
+                    $option.removeAttr('id');
+                    $option.data('input', $input);
+                    $input.on('update', function () {
+                        $option.attr('selected', $(this).prop("checked"));
+                    });
+                    self.$el.append($input);
+                });
+                $select.data("value", $options.first());
+                $options.first().attr("selected", true);
+            });
+            $selects.change(function () {
+                var $option = $(this).find('option:selected');
+                $(this).data("value").data("input").prop("checked", true).change();
+                $(this).data("value", $option);
+                $option.data("input").change();
+            });
+        },
+        load_xml_data: function (xml_ids) {
+            var self = this;
+            $('#theme_error').remove();
+            return openerp.jsonRpc('/website/theme_customize_get', 'call', {
+                    'xml_ids': this.get_xml_ids(this.$inputs)
+                }).done(function (data) {
+                    self.$inputs.filter('[data-xmlid]').each(function () {
+                        if (!_.difference(self.get_xml_ids($(this)), data[1]).length) {
+                            $(this).prop("checked", false).change();
+                        }
+                        if (!_.difference(self.get_xml_ids($(this)), data[0]).length) {
+                            $(this).prop("checked", true).change();
+                        }
+                    });
+                }).fail(function (d, error) {
+                    $('body').prepend($('<div id="theme_error"/>').text(error.data.message));
+                });
+        },
+        get_inputs: function (string) {
+            return this.$inputs.filter('#'+string.split(",").join(", #"));
+        },
+        get_xml_ids: function ($inputs) {
+            var xml_ids = [];
+            $inputs.each(function () {
+                if ($(this).data('xmlid') && $(this).data('xmlid').length) {
+                    xml_ids = xml_ids.concat($(this).data('xmlid').split(","));
+                }
+            });
+            return xml_ids;
+        },
+        compute_stylesheets: function () {
+            var self = this;
+            self.has_error = false;
+            $('link[href*=".assets_"]').attr('data-loading', true);
+            function theme_customize_css_onload() {
+                if ($('link[data-loading]').size()) {
+                    $('body').toggleClass('theme_customize_css_loading');
+                    setTimeout(theme_customize_css_onload, 50);
+                } else {
+                    $('body').removeClass('theme_customize_css_loading');
+                    self.$el.removeClass("loading");
+
+                    if (window.getComputedStyle($('button[data-toggle="collapse"]:first')[0]).getPropertyValue('position') === 'static' ||
+                        window.getComputedStyle($('#theme_customize_modal')[0]).getPropertyValue('display') === 'none') {
+                        if (self.has_error) {
+                            window.location.hash = "theme=true";
+                            window.location.reload();
+                        } else {
+                            self.has_error = true;
+                            $('link[href*=".assets_"][data-error]').removeAttr('data-error').attr('data-loading', true);
+                            self.update_stylesheets();
+                            setTimeout(theme_customize_css_onload, 50);
+                        }
+                    }
+                }
+            }
+            theme_customize_css_onload();
+        },
+        update_stylesheets: function () {
+            $('link[href*=".assets_"]').each(function update () {
+                var $style = $(this);
+                var href = $style.attr("href").replace(/[^\/]+$/, new Date().getTime());
+                var $asset = $('<link rel="stylesheet" href="'+href+'"/>');
+                $asset.attr("onload", "$(this).prev().attr('disable', true).remove(); $(this).removeAttr('onload').removeAttr('onerror');");
+                $asset.attr("onerror", "$(this).prev().removeAttr('data-loading').attr('data-error','loading'); $(this).attr('disable', true).remove();");
+                $style.after($asset);
+            });
+        },
+        update_style: function (enable, disable, reload) {
+            var self = this;
+            if (this.$el.hasClass("loading")) return;
+            this.$el.addClass("loading");
+
+            if (!reload && $('link[href*=".assets_"]').size()) {
+                this.compute_stylesheets();
+                return openerp.jsonRpc('/website/theme_customize', 'call', {
+                        'enable': enable,
+                        'disable': disable
+                    }).then(function () {
+                        self.update_stylesheets();
+                    });
+            } else {
+                var href = '/website/theme_customize_reload'+
+                    '?href='+encodeURIComponent(window.location.href)+
+                    '&enable='+encodeURIComponent(enable.join(","))+
+                    '&disable='+encodeURIComponent(disable.join(","));
+                window.location.href = href;
+                return $.Deferred();
+            }
+        },
+        enable_disable: function (data, enable) {
+            if (!data) return;
+            this.$('#'+data.split(",").join(", #")).each(function () {
+                var check = $(this).prop("checked");
+                var $label = $(this).closest("label");
+                $(this).attr("checked", enable);
+                if (enable) $label.addClass("checked");
+                else $label.removeClass("checked");
+                if (check != enable) {
+                    $(this).change();
+                }
+            });
+        },
+        change_selection: function (event) {
+            var self = this;
+            if (this.$el.hasClass("loading")) return;
+
+            var $option = $(event.target),
+                checked = $option.prop("checked");
+
+            if (checked) {
+                this.enable_disable($option.data('enable'), true);
+                this.enable_disable($option.data('disable'), false);
+                $option.closest("label").addClass("checked");
+            } else {
+                $option.closest("label").removeClass("checked");
+            }
+            $option.prop("checked", checked);
+
+            var $enable = this.$inputs.filter('[data-xmlid]:checked');
+            $enable.closest("label").addClass("checked");
+            var $disable = this.$inputs.filter('[data-xmlid]:not(:checked)');
+            $disable.closest("label").removeClass("checked");
+
+            var $sets = this.$inputs.filter('input[data-enable]:not([data-xmlid]), input[data-disable]:not([data-xmlid])');
+            $sets.each(function () {
+                var $set = $(this);
+                var checked = true;
+                if ($set.data("enable")) {
+                    self.get_inputs($(this).data("enable")).each(function () {
+                        if (!$(this).prop("checked")) checked = false;
+                    });
+                }
+                if ($set.data("disable")) {
+                    self.get_inputs($(this).data("disable")).each(function () {
+                        if ($(this).prop("checked")) checked = false;
+                    });
+                }
+                if (checked) {
+                    $set.prop("checked", true).closest("label").addClass("checked");
+                } else {
+                    $set.prop("checked", false).closest("label").removeClass("checked");
+                }
+                $set.trigger('update');
+            });
+
+            if (this.flag && $option.data('reload') && document.location.href.match(new RegExp( $option.data('reload') ))) {
+                this.reload = true;
+            }
+
+            clearTimeout(this.timer);
+            if (this.flag) {
+                this.timer = setTimeout(function () {
+                    self.update_style(self.get_xml_ids($enable), self.get_xml_ids($disable), self.reload);
+                    self.reload = false;
+                },0);
+            } else {
+                this.timer = setTimeout(function () { self.reload = false; },0);
+            }
+        },
+        click: function (event) {
+            if (!$(event.target).closest("#theme_customize_modal > *").length) {
+                this.close();
+            }
+        },
+        close: function () {
+            var self = this;
+            $(document).off('keydown', this.keydown_escape);
+            $('#theme_error').remove();
+            $('link[href*=".assets_"]').removeAttr('data-loading');
+            this.$el.removeClass('in');
+            this.$el.addClass('out');
+            setTimeout(function () {self.destroy();}, 500);
+        }
+    });
+
+    function themeError(message) {
+        var _t = openerp._t;
+
+        if (message.indexOf('lessc')) {
+            message = '<span class="text-muted">' + message + "</span><br/><br/>" + _t("Please install or update node-less");
+        }
+
+        var $error = $( openerp.qweb.render('website.error_dialog', {
+            title: _t("Theme Error"),
+            message: message
+        }));
+        $error.appendTo("body").modal();
+        $error.on('hidden.bs.modal', function () {
+            $(this).remove();
+        });
+    }
+
+
+    openerp.website.ready().done(function() {
+        function theme_customize() {
+            var error = window.getComputedStyle(document.body, ':before').getPropertyValue('content');
+            if (error && error !== 'none') {
+                return themeError(eval(error));
+            }
+            var Theme = openerp.website.Theme;
+            if (Theme.open && !Theme.open.isDestroyed()) return;
+            Theme.open = new Theme();
+            Theme.open.appendTo("body");
+        }
+        $(document).on('click', "#theme_customize a",theme_customize);
+        if ((window.location.hash || "").indexOf("theme=true") !== -1) {
+            theme_customize();
+            window.location.hash = "";
+        }
+    });
+
+})();

+ 95 - 0
static/src/less/colors.less

@@ -0,0 +1,95 @@
+/* Clean Colors */
+
+@color-blue: #3498DB;
+@color-turquoise: #1ABC9C;
+@color-green: #2ECC71;
+@color-yellow: #F1C40F;
+@color-orange: #E67E22;
+@color-red: #E74C3C;
+@color-pink: #f74b94;
+@color-purple: #9B59B6;
+@color-brown: #7b5844;
+
+@color-blue-dark: #2980B9;
+@color-turquoise-dark: #16A085;
+@color-green-dark: #27AE60;
+@color-yellow-dark: #f3ac12;
+@color-orange-dark: #D35400;
+@color-red-dark: #C0392B;
+@color-pink-dark: #ea3884;
+@color-purple-dark: #8E44AD;
+@color-brown-dark: #604434;
+
+@color-blue-light: lighten(@color-blue, 20%);
+@color-turquoise-light: lighten(@color-turquoise, 20%);
+@color-green-light: lighten(@color-green, 20%);
+@color-yellow-light: lighten(@color-yellow, 20%);
+@color-orange-light: lighten(@color-orange, 20%);
+@color-red-light: lighten(@color-red, 20%);
+@color-pink-light: lighten(@color-pink, 20%);
+@color-purple-light: lighten(@color-purple, 20%);
+@color-brown-light: lighten(@color-brown, 20%);
+
+@color-clouds: #ECF0F1;
+@color-silver: #BDC3C7;
+@color-concrete: #95A5A6;
+@color-stone: #7F8C8D;
+@color-asphalt: #34495E;
+@color-midnight: #2C3E50;
+
+
+@color-concrete-dark: #445b5c;
+
+/* +++++ NEW COLORS +++++ */
+
+@color-wind: #f7f7f7;
+@color-clay: #e1dcd5;
+@color-sand: #c5bcb1;
+@color-beach: #f3bf91;
+@color-forest: #254c3a;
+@color-twilight: #3b2c50;
+@color-burgundy: #8e362d;
+@color-marine: #212d3a;
+
+/* GRAYS --------------------------------------------------------- */
+
+@gray-darker:          lighten(#000, 20%);
+@gray-dark:            lighten(#000, 40%);
+@gray:                 lighten(#000, 55%);
+@gray-light:           lighten(#000, 70%);
+@gray-lighter:         lighten(#000, 80%);
+
+/* Backgrounds Colors ------------------------------------------------*/
+
+@bg-blue: background(@color-blue);
+@bg-turquoise: background(@color-turquoise);
+@bg-green: background(@color-green);
+@bg-yellow: background(@color-yellow);
+@bg-orange: background(@color-orange);
+@bg-red: background(@color-red);
+@bg-pink: background(@color-pink);
+@bg-purple: background(@color-purple);
+@bg-brown: background(@color-brown);
+
+
+/* ---- Default Color HTML Class ---- */
+
+.bg-blue      { background-color: @color-blue;}
+.bg-turquoise { background-color: @color-turquoise;}
+.bg-green     { background-color: @color-green;}
+.bg-yellow    { background-color: @color-yellow;}
+.bg-orange    { background-color: @color-orange;}
+.bg-red       { background-color: @color-red;}
+.bg-pink      { background-color: @color-pink;}
+.bg-purple    { background-color: @color-purple;}
+.bg-brown     { background-color: @color-brown;}
+
+.text-blue      { color: @color-blue;}
+.text-turquoise { color: @color-turquoise;}
+.text-green     { color: @color-green;}
+.text-yellow    { color: @color-yellow;}
+.text-orange    { color: @color-orange;}
+.text-red       { color: @color-red;}
+.text-pink      { color: @color-pink;}
+.text-purple    { color: @color-purple;}
+.text-brown     { color: @color-brown;}

+ 1 - 0
static/src/less/import_bootstrap.less

@@ -0,0 +1 @@
+@import "bootstrap";

+ 77 - 0
static/src/less/option_color_amethyst.less

@@ -0,0 +1,77 @@
+/* GRAYS --------------------------------------------------------- */
+
+@gray-darker:            lighten(#000, 30%);
+
+/* COLORS --------------------------------------------------------- */
+
+@color-primary:         @color-purple;
+@color-success:         @color-purple-dark;
+@color-info:            @color-twilight;
+@color-warning:         @color-red-dark;
+@color-danger:          @color-midnight;
+
+/* COLORED BG -------------------------------------------------------- */
+
+.bg-primary {
+    background-color: @color-clouds;
+}
+.bg-success {
+    background-color: @color-purple;
+}
+.bg-info {
+    background-color: @color-twilight;
+}
+.bg-warning {
+    background-color: @color-purple-dark;
+}
+.bg-danger {
+    background-color: @color-midnight;
+}
+
+/* TEXTS & LINKS --------------------------------------------------- */
+
+@text-muted:        @color-primary;
+@link-color:        @color-primary;
+@link-hover-color:  @color-success;
+
+/* BUTTONS --------------------------------------------------------- */
+
+@btn-primary-bg:                 @color-silver;
+@btn-success-bg:                 @color-concrete;
+@btn-info-bg:                    @color-purple;
+@btn-warning-bg:                 @color-purple-light;
+@btn-danger-bg:                  @color-red;
+
+.btn .fa {
+	color: white;}
+
+.btn-default .fa {
+	color: @gray-dark;}
+
+/* rgba(0, 0, 0, 0) BG --------------------------------------------------- */
+/* Used in snippets Prx Image Color and Prx Image Shade ------------- */
+
+.bg-rgba(0, 0, 0, 0)-color 		{ background: @color-twilight; opacity: 0.85;}
+.bg-rgba(0, 0, 0, 0)-shade		{ background: black; opacity: 0.5;}
+
+/* Footer ----------------------------------------------------------- */
+
+#wrapwrap footer {
+   background-color: @color-concrete-dark;
+    color: @gray-light;
+    }
+footer ul li a {
+	color: @color-clouds;
+	}
+footer ul li a:hover {
+	color: white;
+    text-decoration: none;
+	}
+footer h4 {
+	color: @gray-lighter;
+	padding-bottom: 12px;
+    text-transform: uppercase;
+    }
+footer .fa {
+    color: @color-concrete;
+  }

+ 74 - 0
static/src/less/option_color_cobalt.less

@@ -0,0 +1,74 @@
+/* COLORS --------------------------------------------------------- */
+
+@color-primary:         @color-blue;
+@color-success:         @color-turquoise;
+@color-info:            @color-green;
+@color-warning:         @color-asphalt;
+@color-danger:          @color-midnight;
+
+/* COLORED BG -------------------------------------------------------- */
+
+.bg-primary {
+    background-color: @color-wind;
+}
+.bg-success {
+    background-color: @color-silver;
+}
+.bg-info {
+    background-color: @color-blue;
+}
+.bg-warning {
+    background-color: @color-blue-dark;
+}
+.bg-danger {
+    background-color: @color-asphalt;
+}
+
+/* TEXTS & LINKS --------------------------------------------------- */
+
+@text-muted:        @color-primary;
+@link-color:        @color-primary;
+@link-hover-color:  @color-info;
+
+/* BUTTONS --------------------------------------------------------- */
+
+@btn-primary-bg:                 @color-blue;
+@btn-success-bg:                 @color-turquoise;
+@btn-info-bg:                    @color-green;
+@btn-warning-bg:                 @color-orange;
+@btn-danger-bg:                  @color-red;
+
+.btn .fa {
+	color: white;}
+
+.btn-default .fa {
+	color: @gray-dark;}
+
+/* rgba(0, 0, 0, 0) BG --------------------------------------------------- */
+/* Used in snippets Prx Image Color and Prx Image Shade ------------- */
+
+.bg-rgba(0, 0, 0, 0)-color { background: @color-blue-dark; opacity: 0.9;}
+.bg-rgba(0, 0, 0, 0)-shade { background: black; opacity: 0.5;}
+
+/* Footer ----------------------------------------------------------- */
+
+#wrapwrap footer {
+   background-color: @color-midnight;
+   color: @gray;
+}
+footer ul li a {
+	color: @color-silver;
+}
+
+footer ul li a:hover {
+	color: @color-clouds;
+    text-decoration: none;
+}
+footer h4 {
+	color: @color-stone;
+	padding-bottom: 12px;
+    text-transform: uppercase;
+}
+footer .fa {
+    color: @gray-light;
+}

+ 73 - 0
static/src/less/option_color_emerald.less

@@ -0,0 +1,73 @@
+/* COLORS --------------------------------------------------------- */
+
+@color-primary:         @color-green;
+@color-success:         @color-turquoise;
+@color-info:            @color-turquoise-dark;
+@color-warning:         @color-asphalt;
+@color-danger:          @color-midnight;
+
+/* COLORED BG -------------------------------------------------------- */
+
+.bg-primary {
+    background-color: @color-wind;
+}
+.bg-success {
+    background-color: @color-silver;
+}
+.bg-info {
+    background-color: @color-green-dark;
+}
+.bg-warning {
+    background-color: @color-green;
+}
+.bg-danger {
+    background-color: @color-forest;
+}
+
+/* TEXTS & LINKS --------------------------------------------------- */
+
+@text-muted:        @color-success;
+@link-color:        @color-success;
+@link-hover-color:  @color-primary;
+
+/* BUTTONS --------------------------------------------------------- */
+
+@btn-primary-bg:                 @color-green;
+@btn-success-bg:                 @color-turquoise;
+@btn-info-bg:                    @color-blue;
+@btn-warning-bg:                 @color-orange;
+@btn-danger-bg:                  @color-red;
+
+.btn .fa {
+	color: white;}
+
+.btn-default .fa {
+	color: @gray-dark;}
+
+/* rgba(0, 0, 0, 0) BG --------------------------------------------------- */
+/* Used in snippets Prx Image Color and Prx Image Shade ------------- */
+
+.bg-rgba(0, 0, 0, 0)-color 		{ background: @color-green-dark; opacity: 0.9;}
+.bg-rgba(0, 0, 0, 0)-shade		{ background: black; opacity: 0.5;}
+
+/* Footer ----------------------------------------------------------- */
+
+#wrapwrap footer {
+   background-color: @color-concrete-dark;
+    color: @gray;
+    }
+footer ul li a {
+	color: @color-clouds;
+	}
+footer ul li a:hover {
+	color: white;
+    text-decoration: none;
+	}
+footer h4 {
+	color: @color-silver;
+	padding-bottom: 12px;
+    text-transform: uppercase;
+    }
+footer .fa {
+    color: @color-clouds;
+  }

+ 77 - 0
static/src/less/option_color_gold.less

@@ -0,0 +1,77 @@
+/* COLORS --------------------------------------------------------- */
+
+@color-primary:         @color-orange;
+@color-success:         @color-red;
+@color-info:            @color-yellow-dark;
+@color-warning:         @color-red-dark;
+@color-danger:          @color-orange-dark;
+
+/* COLORED BG -------------------------------------------------------- */
+
+.bg-primary {
+    background-color: @color-sand;
+}
+.bg-success {
+    background-color: @color-yellow;
+}
+.bg-info {
+    background-color: @color-orange;
+}
+.bg-warning {
+    background-color: @color-yellow-dark;
+}
+.bg-danger {
+    background-color: @color-orange-dark;
+}
+
+/* TEXTS & LINKS ------------------------------------------------------- */
+
+@text-muted:        @gray;
+@link-color:        @color-info;
+@link-hover-color:  @color-primary;
+
+/* BUTTONS --------------------------------------------------------- */
+
+@btn-primary-bg:                 @color-yellow;
+@btn-success-bg:                 @color-orange;
+@btn-info-bg:                    @color-yellow-dark;
+@btn-warning-bg:                 @color-orange-dark;
+@btn-danger-bg:                  @color-red-dark;
+
+.btn .fa {
+	color: white;}
+
+.btn-default .fa {
+	color: @gray-dark;}
+
+/* rgba(0, 0, 0, 0) BG --------------------------------------------------- */
+/* Used in snippets Prx Image Color and Prx Image Shade ------------- */
+
+.bg-rgba(0, 0, 0, 0)-color 		{ background: @color-orange-dark; opacity: 0.85;}
+.bg-rgba(0, 0, 0, 0)-shade		{ background: black; opacity: 0.5;}
+
+/* Footer ----------------------------------------------------------- */
+
+#wrapwrap footer {
+   background-color: @color-orange-dark;
+   color: white;
+    }
+footer ul li a {
+	color: @color-beach;
+	}
+footer ul li a:hover {
+	color:  @color-yellow;
+    text-decoration: none;
+	}
+footer h4 {
+	color:  @color-beach;
+	padding-bottom: 12px;
+    text-transform: uppercase;
+    }
+footer .fa {
+    color:  @color-beach;
+  }
+
+
+
+

+ 78 - 0
static/src/less/option_color_ruby.less

@@ -0,0 +1,78 @@
+/* GRAYS --------------------------------------------------------- */
+
+@gray:                   lighten(#000, 50%);
+
+/* COLORS --------------------------------------------------------- */
+
+@color-primary:         @color-red;
+@color-success:         @color-red-dark;
+@color-info:            @color-orange;
+@color-warning:         @color-purple;
+@color-danger:          @color-orange-dark;
+
+/* COLORED BG -------------------------------------------------------- */
+
+.bg-primary {
+    background-color: @color-sand;
+}
+.bg-success {
+    background-color: @color-red;
+}
+.bg-info {
+    background-color: @color-red-dark;
+}
+.bg-warning {
+    background-color: @color-orange;
+}
+.bg-danger {
+    background-color: @color-orange-dark;
+}
+
+/* TEXT & LINKS -------------------------------------------------------- */
+
+@text-muted:        @gray;
+@link-color:        @color-primary;
+@link-hover-color:  @color-success;
+
+/* BUTTONS --------------------------------------------------------- */
+
+@btn-primary-bg:                 @color-red;
+@btn-success-bg:                 @color-orange;
+@btn-info-bg:                    @color-yellow-dark;
+@btn-warning-bg:                 @color-orange-dark;
+@btn-danger-bg:                  @color-red-dark;
+
+.btn .fa {
+	color: white;}
+
+.btn-default .fa {
+	color: @gray-dark;}
+
+/* rgba(0, 0, 0, 0) BG --------------------------------------------------- */
+/* Used in snippets Prx Image Color and Prx Image Shade ------------- */
+
+.bg-rgba(0, 0, 0, 0)-color 		{ background: @color-red-dark; opacity: 0.85;}
+.bg-rgba(0, 0, 0, 0)-shade		{ background: black; opacity: 0.5;}
+
+/* Footer ----------------------------------------------------------- */
+
+#wrapwrap footer {
+   background-color: @color-burgundy;
+   color: @gray-lighter;
+    }
+footer ul li a {
+	color: white;
+	}
+footer ul li a:hover {
+	color: @color-orange;
+    text-decoration: none;
+	}
+footer h4 {
+	color: @color-sand;
+    padding-bottom: 12px;
+    text-transform: uppercase;
+    }
+footer .fa {
+    color: @color-red;
+  }
+

+ 76 - 0
static/src/less/option_color_stone.less

@@ -0,0 +1,76 @@
+/* COLORS --------------------------------------------------------- */
+
+@color-primary:         @color-concrete;
+@color-success:         @color-turquoise;
+@color-info:            @color-stone;
+@color-warning:         @color-concrete-dark;
+@color-danger:          @color-midnight;
+
+/* COLORED BG -------------------------------------------------------- */
+
+.bg-primary {
+    background-color: @color-clouds;
+}
+.bg-success {
+    background-color: @color-silver;
+}
+.bg-info {
+    background-color: @color-stone;
+}
+.bg-warning {
+    background-color: @color-concrete-dark;
+}
+.bg-danger {
+    background-color: @color-asphalt;
+}
+
+/* TEXT & LINKS -------------------------------------------------------- */
+
+@text-muted:        @color-primary;
+@link-color:        @color-primary;
+@link-hover-color:  @color-info;
+
+/* BUTTONS --------------------------------------------------------- */
+
+@btn-primary-bg:                 @color-concrete;
+@btn-success-bg:                 @color-blue;
+@btn-info-bg:                    @color-asphalt;
+@btn-warning-bg:                 @color-red;
+@btn-danger-bg:                  @color-red-dark;
+
+.btn .fa {
+	color: white;}
+
+.btn-default .fa {
+	color: @gray-dark;}
+
+/* rgba(0, 0, 0, 0) BG --------------------------------------------------- */
+/* Used in snippets Prx Image Color and Prx Image Shade ------------- */
+
+.bg-rgba(0, 0, 0, 0)-color 		{ background: @color-stone; opacity: 0.9;}
+.bg-rgba(0, 0, 0, 0)-shade		{ background: black; opacity: 0.5;}
+
+/* Footer ----------------------------------------------------------- */
+
+#wrapwrap footer {
+   background-color: @color-concrete-dark;
+   color: @color-concrete;
+    }
+footer ul li a {
+	color: @color-clouds;
+	}
+footer ul li a:hover {
+	color: white;
+    text-decoration: none;
+	}
+footer h4 {
+	color: @color-silver;
+	padding-bottom: 12px;
+    text-transform: uppercase;
+    }
+footer .fa {
+    color: @color-clouds;
+  }
+
+
+

+ 2 - 0
static/src/less/option_font.less

@@ -0,0 +1,2 @@
+@font-family-sans-serif: Georgia, "Times New Roman", Times, serif;
+@font-family-serif: "Helvetica Neue", Helvetica, Arial, sans-serif;

+ 4 - 0
static/src/less/option_layout_boxed.less

@@ -0,0 +1,4 @@
+#wrapwrap {
+    width: 85%;
+    margin: 0 auto;
+}

+ 13 - 0
static/src/less/website.less

@@ -0,0 +1,13 @@
+#wrapwrap {
+    display: table;
+    table-layout: fixed;
+    width: 100%;
+
+    > * {
+        display: table-row;
+    }
+
+    > footer {
+        height: 100%;
+    }
+}

+ 294 - 0
views/snippets.xml

@@ -0,0 +1,294 @@
+<openerp>
+    <data>
+
+<template id="website_less.snippets" inherit_id="website.snippets">
+
+    <!-- Add name attributes to snippets -->
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Banner']]" position="attributes">
+        <attribute name="name">Banner</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Text-Image']]" position="attributes">
+        <attribute name="name">Text-Image</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Image-Text']]" position="attributes">
+        <attribute name="name">Image-Text</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Image-Floating']]" position="attributes">
+        <attribute name="name">Image-Floating</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Big Message']]" position="attributes">
+        <attribute name="name">Big Message</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Text Block']]" position="attributes">
+        <attribute name="name">Text Block</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Title']]" position="attributes">
+        <attribute name="name">Title</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Features']]" position="attributes">
+        <attribute name="name">Features</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Big Picture']]" position="attributes">
+        <attribute name="name">Big Picture</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Three Columns']]" position="attributes">
+        <attribute name="name">Three Columns</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Well']]" position="attributes">
+        <attribute name="name">Well</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Quote']]" position="attributes">
+        <attribute name="name">Quote</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Panel']]" position="attributes">
+        <attribute name="name">Panel</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Image Floating']]" position="attributes">
+        <attribute name="name">Image Floating</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Separator']]" position="attributes">
+        <attribute name="name">Separator</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Share']]" position="attributes">
+        <attribute name="name">Share</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Image Gallery']]" position="attributes">
+        <attribute name="name">Image Gallery</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Comparisons']]" position="attributes">
+        <attribute name="name">Comparisons</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Button']]" position="attributes">
+        <attribute name="name">Button</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='FAQ']]" position="attributes">
+        <attribute name="name">FAQ</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='References']]" position="attributes">
+        <attribute name="name">References</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Quotes Slider']]" position="attributes">
+        <attribute name="name">Quotes Slider</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Feature Grid']]" position="attributes">
+        <attribute name="name">Feature Grid</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Parallax']]" position="attributes">
+        <attribute name="name">Parallax</attribute>
+    </xpath>
+
+    <xpath expr="//div[div[span[@class='oe_snippet_thumbnail_title']][span='Parallax Slider']]" position="attributes">
+        <attribute name="name">Parallax Slider</attribute>
+    </xpath>
+
+    <!-- Add class names on snippets and remove margins classes -->
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Banner']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body carousel slide s_banner</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Text-Image']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_text_image</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Image-Text']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_image_text</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Image-Floating']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body para_large s_image_floating</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Big Message']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body jumbotron s_big_message</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Text Block']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_text_block</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Title']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_title</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Features']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_features</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Big Picture']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body oe_dark s_big_picture</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Three Columns']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_three_columns</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Well']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body well s_well</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Quote']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_quote</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Panel']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body panel panel-default s_panel</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Image Floating']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body o_image_floating o_margin_l pull-right s_image_floating</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Separator']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_separator</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Share']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body oe_share s_share</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Image Gallery']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_image_gallery</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Comparisons']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_comparisons</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Button']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body jumbotron s_button</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='FAQ']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_faq</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='References']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_references</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Quotes Slider']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body carousel quotecarousel slide s_quotes_slider</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Feature Grid']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body s_feature_grid</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Parallax']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body parallax s_parallax</attribute>
+    </xpath>
+
+    <xpath expr="//div[span[@class='oe_snippet_thumbnail_title']][span='Parallax Slider']/following-sibling::*[1]" position="attributes">
+        <attribute name="class">oe_snippet_body parallax s_parallax_slider</attribute>
+    </xpath>
+
+</template>
+
+<template id="website_less.snippet_options" inherit_id="website.snippet_options">
+
+    <!-- Add new snippet options API attributes to old snippet options -->
+    <xpath expr="//div[@data-snippet-option-id='blog-style']" position="attributes">
+        <attribute name="data-js">blog-style</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='background']" position="attributes">
+        <attribute name="data-js">background</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='carousel']" position="attributes">
+        <attribute name="data-js">carousel</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='margin-y']" position="attributes">
+        <attribute name="data-js">margin-y</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='resize']" position="attributes">
+        <attribute name="data-js">resize</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='margin-x']" position="attributes">
+        <attribute name="data-js">margin-x</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='content']" position="attributes">
+        <attribute name="data-js">content</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='separator']" position="attributes">
+        <attribute name="data-js">separator</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='image_floating_margin']" position="attributes">
+        <attribute name="data-js">image_floating_margin</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='image_floating_hidelink']" position="attributes">
+        <attribute name="data-js">image_floating_hidelink</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='image_floating_side']" position="attributes">
+        <attribute name="data-js">image_floating_side</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='parallax']" position="attributes">
+        <attribute name="data-js">parallax</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='media']" position="attributes">
+        <attribute name="data-js">media</attribute>
+    </xpath>
+    <xpath expr="//div[@data-snippet-option-id='transform']" position="attributes">
+        <attribute name="data-js">transform</attribute>
+    </xpath>
+
+    <xpath expr="//div[@data-js='background']//ul[@class='dropdown-menu']" position="replace">
+        <ul class="dropdown-menu">
+            <li data-background="/website/static/src/img/parallax/parallax_bg.jpg"><a>Sunflower</a></li>
+            <li data-background="/website/static/src/img/banner/business_guy.jpg"><a>Business Guy</a></li>
+            <li data-background="/website/static/src/img/banner/flower_field.jpg"><a>Flowers Field</a></li>
+            <li data-background="/website/static/src/img/banner/landscape.jpg"><a>Landscape</a></li>
+            <li data-background="/website/static/src/img/banner/mountains.jpg"><a>Mountains</a></li>
+            <li data-background="/website/static/src/img/banner/greenfields.jpg"><a>Greenfields</a></li>
+            <li data-background="/website/static/src/img/banner/aqua.jpg"><a>Aqua</a></li>
+            <li data-background="/website/static/src/img/banner/baby_blue.jpg"><a>Baby Blue</a></li>
+            <li data-background="/website/static/src/img/banner/black.jpg"><a>Black</a></li>
+            <li data-background="/website/static/src/img/banner/color_splash.jpg"><a>Color Splash</a></li>
+            <li data-background="/website/static/src/img/banner/mango.jpg"><a>Mango</a></li>
+            <li data-background="/website/static/src/img/banner/orange_red.jpg"><a>Orange Red</a></li>
+            <li data-background="/website/static/src/img/banner/flower.jpg"><a>Purple</a></li>
+            <li data-background="/website/static/src/img/banner/velour.jpg"><a>Velour</a></li>
+            <li data-background="/website/static/src/img/banner/wood.jpg"><a>Wood</a></li>
+            <li data-background="/website/static/src/img/banner/yellow_green.jpg"><a>Yellow Green</a></li>
+            <li data-background="/website/static/src/img/parallax/quote.png"><a>Quote</a></li>
+            <li data-background=""><a>None</a></li>
+            <li><a style="background: none; padding: 5px; border-top: 1px solid #ddd;"></a></li>
+            <li data-choose_image="choose_image"><a><b>Choose an image...</b></a></li>
+        </ul>
+    </xpath>
+
+    <xpath expr="." position="inside">
+        <div data-js='colorpicker' data-selector="section, .carousel, .parallax">
+            <li class="dropdown-submenu">
+                <a tabindex="-1" href="#">Color</a>
+                <ul class="dropdown-menu">
+                    <li></li>
+                </ul>
+            </li>
+        </div>
+    </xpath>
+</template>
+
+    </data>
+</openerp>

+ 268 - 0
views/themes.xml

@@ -0,0 +1,268 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+<data>
+
+<!--
+    Customize Themes
+
+    Use INPUT type 'checkbox' or 'radio' or use OPTION in select box
+    'data-xmlid' (optional) xml id of the template to add if the input is checked.
+        You can set a list of xml id separate by comma (all template is enable or
+        disable in same time)
+    'data-enable' (optional) to checked one or more HTML ids, or list separate by comma
+    'data-disable' (optional) to unchecked one or more HTML ids, or list separate by comma
+    'data-reload="/"' (optional) force the reloading of the page if the url match with
+        the string ( = regexp).
+        Otherwise, only the '/web/css/website.assets_frontend' is reloaded
+
+    For the sets (data-enable and/or data-disable without data-xmlid), the set is
+    automatically checked if:
+    - all related fields are enabled for data-enable
+    - all related fields are disabled for data-disable
+    else unchecked
+
+    HTML apply classes:
+    - 'checked': on the parent label when input is checked
+    - 'in': on the container (e.g.: bootstrap modal) after added in DOM (removed together
+       out is added)
+    - 'out': on the container 1 second before removed from ths DOM
+    - 'loading': on the container/modal when the new css is loading
+-->
+
+<template id="website_less.theme_customize" name="Theme Modal for Customization">
+  <div id="theme_customize_modal" class="modal fade">
+      <div class="modal-dialog">
+          <div class="modal-content">
+              <input id="less" data-xmlid="website_less.option_bootstrap_less" style="display: none;"/>
+
+              <div class="loading_backdrop"></div>
+              <div class="modal-header">
+                  <button type="button" class="close">×</button>
+                  <h4 class="modal-title" id="mySmallModalLabel">Customize your theme</h4>
+              </div>
+              <div class="modal-body">
+                  <h5 class="modal-h5">LAYOUT</h5>
+                  <table name="layout">
+                      <tr>
+                          <td class=" text-center" width="50%">
+                              <label class=" center-block">
+                                  <div class="text-center" style="background-image: url(/website_less/static/src/img/theme/layout-full.gif); background-size: 100%; line-height: 40px;">FULL WIDTH</div>
+                                  <input name="layoutvar" type="radio" data-xmlid=""/>
+                              </label>
+                          </td>
+                          <td class=" text-center">
+                              <label class=" center-block">
+                                 <div class="text-center" style="background-image: url(/website_less/static/src/img/theme/layout-boxed.gif); background-size: 100%; line-height: 40px;">BOXED</div>
+                                  <input name="layoutvar" type="radio" data-xmlid="website_less.option_layout_boxed" data-enable="less"/>
+                              </label>
+                          </td>
+                      </tr>
+                  </table>
+
+                  <h5 class="modal-h5">MAIN COLOR</h5>
+                  <table name="color">
+                      <tr>
+                          <td>
+                              <label class="chd-color-combi">
+                                  <img src="/website_less/static/src/img/theme/variant-stone.gif" alt="Stone" class="chd-color-combi-img"/>
+                                  <input name="colorvar" type="radio" data-xmlid="" data-disable="less"/>
+                              </label>
+                          </td>
+                          <td>
+                              <label class="chd-color-combi">
+                                  <img src="/website_less/static/src/img/theme/variant-emerald.gif" alt="Emerald" class="chd-color-combi-img"/>
+                                  <input name="colorvar" type="radio" data-xmlid="website_less.option_color_emerald" data-enable="less"/>
+                              </label>
+                          </td>
+                          <td>
+                              <label class="chd-color-combi">
+                                  <img src="/website_less/static/src/img/theme/variant-cobalt.gif" alt="Cobalt" class="chd-color-combi-img"/>
+                                  <input name="colorvar" type="radio" data-xmlid="website_less.option_color_cobalt" data-enable="less"/>
+                              </label>
+                          </td>
+                          <td>
+                              <label class="chd-color-combi">
+                                  <img src="/website_less/static/src/img/theme/variant-amethyst.gif" alt="Amethyst" class="chd-color-combi-img"/>
+                                  <input name="colorvar" type="radio" data-xmlid="website_less.option_color_amethyst" data-enable="less"/>
+                              </label>
+                          </td>
+                          <td>
+                              <label class="chd-color-combi">
+                                  <img src="/website_less/static/src/img/theme/variant-ruby.gif" alt="Blue" class="chd-color-combi-img"/>
+                                  <input name="colorvar" type="radio" data-xmlid="website_less.option_color_ruby" data-enable="less"/>
+                              </label>
+                          </td>
+                          <td>
+                              <label class="chd-color-combi">
+                                  <img src="/website_less/static/src/img/theme/variant-gold.gif" alt="Gold" class="chd-color-combi-img"/>
+                                  <input name="colorvar" type="radio" data-xmlid="website_less.option_color_gold" data-enable="less"/>
+                              </label>
+                          </td>
+                      </tr>
+                  </table>
+
+                  <h5 class="modal-h5">FONTS COMBINATIONS</h5>
+                  <table name="font">
+                      <tr>
+                          <td width="50%">
+                              <label>
+                                  <div>
+                                      <span style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-size:11px">Helvetica</span>
+                                      <span style="font-family:Georgia, 'Times New Roman', Times, serif; font-size:11px" >/ Georgia</span>
+                                  </div>
+                                  <input name="theme" type="radio" data-xmlid=""/>
+                              </label>
+                          </td>
+                          <td width="50%">
+                              <label>
+                                  <div>
+                                      <span style="font-family:Georgia, 'Times New Roman', Times, serif; font-size:11px" >Georgia</span>
+                                      <span style="font-family:'Helvetica Neue', Helvetica, Arial, sans-serif; font-size:11px">/ Helvetica</span>
+                                    <input name="theme" type="radio" data-xmlid="website_less.option_font" data-enable="less"/>
+                                  </div>
+                              </label>
+                          </td>
+                      </tr>
+                  </table>
+              </div>
+          </div>
+      </div>
+  </div>
+</template>
+
+<!-- color-picker -->
+<!-- HTML class to hide option for one mode : only-text, only-bg -->
+
+<template id="website_less.colorpicker" name="Color-Picker">
+    <table class="colorpicker">
+        <tr>
+            <td><button class="automatic-color" title="Automatic Color"/></td>
+            <td><button class="bg-primary" title="Primary Color"/></td>
+            <td><button class="bg-success" title="Success Color"/></td>
+            <td><button class="bg-info" title="Info Color"/></td>
+            <td><button class="bg-warning" title="Warning Color"/></td>
+            <td><button class="bg-danger" title="Danger Color"/></td>
+        </tr>
+        <tr><td colspan="8"><hr style="width: 100%; height: 1px;"/></td></tr>
+        <tr>
+            <td><button class="bg-blue"/></td>
+            <td><button class="bg-turquoise"/></td>
+            <td><button class="bg-green"/></td>
+            <td><button class="bg-yellow"/></td>
+            <td><button class="bg-red"/></td>
+            <td><button class="bg-pink"/></td>
+            <td><button class="bg-purple"/></td>
+            <td><button class="bg-brown"/></td>
+        </tr>
+    </table>
+</template>
+
+<!-- Disable old themes before activating the less option -->
+<record id="website.theme_amelia" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_cerulean" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_cosmo" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_cyborg" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_flatly" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_journal" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_readable" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_simplex" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_slate" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_spacelab" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_united" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+<record id="website.theme_yeti" model="ir.ui.view">
+    <field name="active" eval='False'/>
+</record>
+
+<!-- Bootstrap Less File layout
+
+Each theme module must override the following view to active bootstrap:
+    <record id="website_less.option_bootstrap_less" model="ir.ui.view">
+      <field name="active" eval='True'/>
+    </record>
+ -->
+<template id="website_less.option_bootstrap_less" name="option_bootstrap_less" inherit_id="website.theme" active="True" customize_show="False">
+    <xpath expr="//link[@id='bootstrap_css']" position="replace">
+        <link rel="stylesheet" href='/website_less/static/src/less/import_bootstrap.less'/>
+        <link rel="stylesheet" href='/website_less/static/src/less/colors.less' t-ignore="true"/>
+    </xpath>
+</template>
+
+<!-- Default Options Toggle -->
+<template id="website_less.default_options" name="Default Customize Options" inherit_id="website.theme">
+  <xpath expr="//link[last()]" position="after">
+    <!-- Default options inherit from this view so we can easily disable it to reset all options -->
+    <t id="default_options" t-if="True"></t>
+  </xpath>
+</template>
+
+<!-- Option layout -->
+
+<template id="website_less.option_layout_boxed" name="option_layout_boxed" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+      <link href="/website_less/static/src/less/option_layout_boxed.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+
+<!-- Option color -->
+
+<template id="website_less.option_color_stone" name="option_color_stone" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+    <link href="/website_less/static/src/less/option_color_stone.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+<template id="website_less.option_color_emerald" name="option_color_emerald" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+    <link href="/website_less/static/src/less/option_color_emerald.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+<template id="website_less.option_color_cobalt" name="option_color_cobalt" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+    <link href="/website_less/static/src/less/option_color_cobalt.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+<template id="website_less.option_color_amethyst" name="option_color_amethyst" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+    <link href="/website_less/static/src/less/option_color_amethyst.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+<template id="website_less.option_color_ruby" name="option_color_ruby" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+    <link href="/website_less/static/src/less/option_color_ruby.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+<template id="website_less.option_color_gold" name="option_color_gold" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+    <link href="/website_less/static/src/less/option_color_gold.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+
+<template id="website_less.option_font" name="option_font" inherit_id="website_less.default_options" active="False" customize_show="True">
+  <xpath expr="//t[@id='default_options']" position="inside">
+    <link href="/website_less/static/src/less/option_font.less" rel="stylesheet" type="text/less"/>
+  </xpath>
+</template>
+
+</data>
+</openerp>

+ 16 - 0
views/website_backend_navbar.xml

@@ -0,0 +1,16 @@
+<openerp>
+    <data>
+
+<template id="website_less.user_navbar" inherit_id="website.user_navbar">
+    <xpath expr="//ul[@class='dropdown-menu' and @role='menu']" position="replace">
+        <ul class="dropdown-menu" role="menu">
+            <li id="html_editor"><a href="#advanced-view-editor" data-action='ace'>HTML Editor</a></li>
+            <li id="theme_customize"><a href="#">Customize Theme</a></li>
+            <li id="install_apps"><a href="/web#return_label=Website&amp;action=website.action_module_website">Install Apps</a></li>
+            <li class="divider"></li>
+        </ul>
+    </xpath>
+</template>
+
+    </data>
+</openerp>

+ 45 - 0
views/website_templates.xml

@@ -0,0 +1,45 @@
+<openerp>
+    <data>
+
+<template id="website_less.theme" inherit_id="website.theme" name="Fix table-row in theme">
+    <xpath expr='//link[@href="/website/static/src/css/website.css"]' position="after">
+        <link rel="stylesheet" href='/website_less/static/src/less/website.less'/>
+    </xpath>
+</template>
+
+<template id="website_less.assets_editor" inherit_id="website.assets_editor" name="Editor assets for Less">
+    <xpath expr='//link[@href="/website/static/src/css/editor.css"]' position="replace">
+        <link rel='stylesheet' href='/website_less/static/src/css/editor.css'/>
+    </xpath>
+
+    <xpath expr='//link[@href="/website/static/src/css/snippets.css"]' position="replace">
+        <link rel='stylesheet' href='/website_less/static/src/css/snippets.css'/>
+    </xpath>
+
+    <xpath expr='//script[@src="/website/static/src/js/website.editor.js"]' position="after">
+        <script type="text/javascript" src="/website_less/static/src/js/website.editor.js"></script>
+    </xpath>
+
+    <xpath expr='//script[@src="/website/static/src/js/website.snippets.editor.js"]' position="replace">
+        <script type="text/javascript" src="/website_less/static/src/js/website.snippets.editor.js"></script>
+    </xpath>
+
+    <xpath expr='//link[last()]' position="after">
+        <script type="text/javascript" src="/website_less/static/src/js/website.theme.js"></script>
+    </xpath>
+</template>
+
+<template id="website_less.assets_frontend" inherit_id="website.assets_frontend">
+    <xpath expr="." position="inside">
+        <script type="text/javascript" src="/website_less/static/src/js/website.snippets.animation.js"></script>
+    </xpath>
+</template>
+
+<template id="website_less.footer_default" inherit_id="website.footer_default">
+    <xpath expr='//div[@class="container hidden-print"]' position="attributes">
+        <attribute name="id">footer</attribute>
+    </xpath>
+</template>
+
+    </data>
+</openerp>

+ 132 - 0
website.py

@@ -0,0 +1,132 @@
+import os
+import time
+import hashlib
+import datetime
+import cStringIO
+from PIL import Image
+from sys import maxint
+
+import openerp
+from openerp.osv import osv, orm
+from openerp.addons.web.http import request
+from openerp.tools import image_resize_and_sharpen, image_save_for_web
+
+
+class website(osv.osv):
+    _inherit = "website"
+
+    def _image(self, cr, uid, model, id, field, response, max_width=maxint, max_height=maxint, cache=None, context=None):
+        """ Fetches the requested field and ensures it does not go above
+        (max_width, max_height), resizing it if necessary.
+
+        Resizing is bypassed if the object provides a $field_big, which will
+        be interpreted as a pre-resized version of the base field.
+
+        If the record is not found or does not have the requested field,
+        returns a placeholder image via :meth:`~._image_placeholder`.
+
+        Sets and checks conditional response parameters:
+        * :mailheader:`ETag` is always set (and checked)
+        * :mailheader:`Last-Modified is set iif the record has a concurrency
+          field (``__last_update``)
+
+        The requested field is assumed to be base64-encoded image data in
+        all cases.
+        """
+        Model = self.pool[model]
+        id = int(id)
+
+        ids = Model.search(cr, uid,
+                           [('id', '=', id)], context=context)
+        if not ids and 'website_published' in Model._fields:
+            ids = Model.search(cr, openerp.SUPERUSER_ID,
+                               [('id', '=', id), ('website_published', '=', True)], context=context)
+        if not ids:
+            return self._image_placeholder(response)
+
+        concurrency = '__last_update'
+        [record] = Model.read(cr, openerp.SUPERUSER_ID, [id],
+                              [concurrency, field],
+                              context=context)
+
+        if concurrency in record:
+            server_format = openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT
+            try:
+                response.last_modified = datetime.datetime.strptime(
+                    record[concurrency], server_format + '.%f')
+            except ValueError:
+                # just in case we have a timestamp without microseconds
+                response.last_modified = datetime.datetime.strptime(
+                    record[concurrency], server_format)
+
+        # Field does not exist on model or field set to False
+        if not record.get(field):
+            # FIXME: maybe a field which does not exist should be a 404?
+            return self._image_placeholder(response)
+
+        response.set_etag(hashlib.sha1(record[field]).hexdigest())
+        response.make_conditional(request.httprequest)
+
+        if cache:
+            response.cache_control.max_age = cache
+            response.expires = int(time.time() + cache)
+
+        # conditional request match
+        if response.status_code == 304:
+            return response
+
+        if model == 'ir.attachment' and field == 'url':
+            path = record.get(field).strip('/')
+
+            # Check that we serve a file from within the module
+            if os.path.normpath(path).startswith('..'):
+                return self._image_placeholder(response)
+
+            # Check that the file actually exists
+            path = path.split('/')
+            resource = openerp.modules.get_module_resource(path[0], *path[1:])
+            if not resource:
+                return self._image_placeholder(response)
+
+            data = open(resource, 'rb').read()
+        else:
+            data = record[field].decode('base64')
+        image = Image.open(cStringIO.StringIO(data))
+        response.mimetype = Image.MIME[image.format]
+
+        filename = '%s_%s.%s' % (model.replace('.', '_'), id, str(image.format).lower())
+        response.headers['Content-Disposition'] = 'inline; filename="%s"' % filename
+
+        if (not max_width) and (not max_height):
+            response.data = data
+            return response
+
+        w, h = image.size
+        max_w = int(max_width) if max_width else maxint
+        max_h = int(max_height) if max_height else maxint
+
+        if w < max_w and h < max_h:
+            response.data = data
+        else:
+            size = (max_w, max_h)
+            img = image_resize_and_sharpen(image, size, preserve_aspect_ratio=True)
+            image_save_for_web(img, response.stream, format=image.format)
+            # invalidate content-length computed by make_conditional as
+            # writing to response.stream does not do it (as of werkzeug 0.9.3)
+            del response.headers['Content-Length']
+
+        return response
+
+class ir_http(orm.AbstractModel):
+    _inherit = 'ir.http'
+
+    def _serve_attachment(self):
+        response = super(ir_http, self)._serve_attachment()
+        if response and response.mimetype == 'application/octet-stream':
+            # Try to set mimetype via attachment mimetype field
+            attach = self.pool['ir.attachment'].search_read(request.cr, openerp.SUPERUSER_ID,
+                [('type', '=', 'binary'), ('url', '=', request.httprequest.path)],
+                ['mimetype'], context=request.context)
+            if attach and attach[0].get('mimetype'):
+                response.mimetype = attach[0].get('mimetype')
+        return response