diff options
94 files changed, 10087 insertions, 8926 deletions
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 2fce98dd145..37e68889f01 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1,4 +1,4 @@ -@charset "UTF-8"; +@charset "utf-8"; @font-face { font-family: "mnmliconsRegular"; src: url("/web/static/src/font/mnmliconsv21-webfont.eot") format("eot"); diff --git a/addons/web/static/src/js/framework/ajax.js b/addons/web/static/src/js/framework/ajax.js index dd1eee906c1..ce61f225524 100644 --- a/addons/web/static/src/js/framework/ajax.js +++ b/addons/web/static/src/js/framework/ajax.js @@ -275,11 +275,45 @@ function get_file(options) { }; +var loadXML = (function () { + var loading = false; + var urls = []; + var qwebs = []; + var templates_def = $.Deferred(); + + var load = function loadXML(url, qweb) { + if (url) { + urls.push(url); + qwebs.push(qweb); + } + + if (!loading && urls.length) { + if (templates_def.state() === "resolved") { + templates_def = $.Deferred(); + } + + loading = true; + qwebs.shift().add_template(urls.shift(), function () { + loading = false; + if (!urls.length) { + templates_def.resolve(); + } + load(null); + }); + } + + return templates_def; + }; + + return load; +})(); + return { jsonRpc: jsonRpc, jsonpRpc: jsonpRpc, loadCSS: loadCSS, loadJS: loadJS, + loadXML: loadXML, get_file: get_file, }; diff --git a/addons/web/static/src/js/tour.js b/addons/web/static/src/js/tour.js index 234b681f7d2..585626ab4a2 100644 --- a/addons/web/static/src/js/tour.js +++ b/addons/web/static/src/js/tour.js @@ -6,25 +6,6 @@ var core = require('web.core'); var _t = core._t; var qweb = core.qweb; -// make this work -// raise an error in test mode if openerp don't exist -// if (typeof openerp === "undefined") { -// var error = "openerp is undefined" -// + "\nhref: " + window.location.href -// + "\nreferrer: " + document.referrer -// + "\nlocalStorage: " + window.localStorage.getItem("tour"); -// if (typeof $ !== "undefined") { -// error += '\n\n' + $("body").html(); -// } -// throw new Error(error); -// } - - -// // don't rewrite T in test mode -// if (typeof openerp.Tour !== "undefined") { -// return; -// } - ///////////////////////////////////////////////// @@ -78,7 +59,7 @@ var Tour = { defaultDelay: 50, autoRunning: true, retryRunningDelay: 1000, - errorDelay: 5000, + errorDelay: 10000, state: null, $element: null, timer: null, @@ -700,7 +681,7 @@ var Tour = { }, scrollIntoView: function ($element){ // if element is not on window (screen) then it scroll down to get popup visible - if (!$element.is('body')){ + if (!$element.is('body') && !$element.parents().filter(function () { return $(this).css("position") === "fixed";}).length){ var element_top = $element.offset().top; var visible = ((element_top > $(window).scrollTop()) && (element_top < ($(window).scrollTop() + $(window).height()))); if (!visible) { @@ -709,7 +690,6 @@ var Tour = { } } }; -// openerp.Tour = Tour; ///////////////////////////////////////////////// diff --git a/addons/web/static/src/js/views/form_common.js b/addons/web/static/src/js/views/form_common.js index 19737746b4a..4d8a302882d 100644 --- a/addons/web/static/src/js/views/form_common.js +++ b/addons/web/static/src/js/views/form_common.js @@ -630,6 +630,12 @@ var FieldInterface = { the field to save its value before reading it using get_value(). Must return a promise. */ commit_value: function() {}, + /* + The form view call before_save before save data and if before_save return a deferred, + the form view wait that all deferred are resolve or fail. + If the deferred is rejected, the field is invalidate + */ + before_save: function() {}, }; /** diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index fd024ae0f75..3286f34eff4 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -837,68 +837,92 @@ var FormView = View.extend(common.FieldManagerMixin, { _process_save: function(save_obj) { var self = this; var prepend_on_create = save_obj.prepend_on_create; + var def_process_save = $.Deferred(); try { var form_invalid = false, values = {}, first_invalid_field = null, - readonly_values = {}; - for (var f in self.fields) { - if (!self.fields.hasOwnProperty(f)) { continue; } - f = self.fields[f]; - if (!f.is_valid()) { - form_invalid = true; - if (!first_invalid_field) { - first_invalid_field = f; - } - } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) { - // Special case 'id' field, do not save this field - // on 'create' : save all non readonly fields - // on 'edit' : save non readonly modified fields - if (!f.get("readonly")) { - values[f.name] = f.get_value(true); - } else { - readonly_values[f.name] = f.get_value(true); + readonly_values = {}, + deferred = []; + + _.each(self.fields, function (f) { + var res = f.before_save(); + if (res) { + deferred.push(res); + res.fail(function () { + form_invalid = true; + if (!first_invalid_field) { + first_invalid_field = f; + } + }); + } + }); + + $.when.apply($, deferred).always(function () { + + _.each(self.fields, function (f) { + if (!f.is_valid()) { + form_invalid = true; + if (!first_invalid_field) { + first_invalid_field = f; + } + } else if (f.name !== 'id' && (!self.datarecord.id || f._dirty_flag)) { + // Special case 'id' field, do not save this field + // on 'create' : save all non readonly fields + // on 'edit' : save non readonly modified fields + if (!f.get("readonly")) { + values[f.name] = f.get_value(true); + } else { + readonly_values[f.name] = f.get_value(true); + } } + + }); + + // Heuristic to assign a proper sequence number for new records that + // are added in a dataset containing other lines with existing sequence numbers + if (!self.datarecord.id && self.fields.sequence && + !_.has(values, 'sequence') && !_.isEmpty(self.dataset.cache)) { + // Find current max or min sequence (editable top/bottom) + var current = _[prepend_on_create ? "min" : "max"]( + _.map(self.dataset.cache, function(o){return o.values.sequence}) + ); + values['sequence'] = prepend_on_create ? current - 1 : current + 1; } - } - // Heuristic to assign a proper sequence number for new records that - // are added in a dataset containing other lines with existing sequence numbers - if (!self.datarecord.id && self.fields.sequence && - !_.has(values, 'sequence') && !_.isEmpty(self.dataset.cache)) { - // Find current max or min sequence (editable top/bottom) - var current = _[prepend_on_create ? "min" : "max"]( - _.map(self.dataset.cache, function(o){return o.values.sequence}) - ); - values['sequence'] = prepend_on_create ? current - 1 : current + 1; - } - if (form_invalid) { - self.set({'display_invalid_fields': true}); - first_invalid_field.focus(); - self.on_invalid(); - return $.Deferred().reject(); - } else { - self.set({'display_invalid_fields': false}); - var save_deferral; - if (!self.datarecord.id) { - // Creation save - save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) { - return self.record_created(r, prepend_on_create); - }, null); - } else if (_.isEmpty(values)) { - // Not dirty, noop save - save_deferral = $.Deferred().resolve({}).promise(); + if (form_invalid) { + self.set({'display_invalid_fields': true}); + first_invalid_field.focus(); + self.on_invalid(); + def_process_save.reject(); } else { - // Write save - save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) { - return self.record_saved(r); - }, null); + self.set({'display_invalid_fields': false}); + var save_deferral; + if (!self.datarecord.id) { + // Creation save + save_deferral = self.dataset.create(values, {readonly_fields: readonly_values}).then(function(r) { + return self.record_created(r, prepend_on_create); + }, null); + } else if (_.isEmpty(values)) { + // Not dirty, noop save + save_deferral = $.Deferred().resolve({}).promise(); + } else { + // Write save + save_deferral = self.dataset.write(self.datarecord.id, values, {readonly_fields: readonly_values}).then(function(r) { + return self.record_saved(r); + }, null); + } + save_deferral.then(function(result) { + def_process_save.resolve(result); + }).fail(function() { + def_process_save.reject(); + }); } - return save_deferral; - } + }); } catch (e) { console.error(e); - return $.Deferred().reject(); + return def_process_save.reject(); } + return def_process_save; }, on_invalid: function() { var warnings = _(this.fields).chain() @@ -1266,7 +1290,7 @@ var FormRenderingEngine = FormRenderingEngineInterface.extend({ } var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]); if (!obj) { - throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget'))); + throw new Error(_.str.sprintf(_t("Widget type '%s' is not implemented"), $elem.attr('widget') || self.fvg.fields[name].type )); } var w = new (obj)(self.view, utils.xml_to_json($elem[0])); var $label = self.labels[$elem.attr("name")]; diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 6082bca1363..94553687043 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -10,6 +10,8 @@ <!-- jQuery stuff --> <script type="text/javascript" src="/web/static/lib/jquery/jquery.js"></script> + <link rel="stylesheet" href="/web/static/lib/jquery.ui/jquery-ui.css"/> + <script type="text/javascript" src="/web/static/lib/jquery.ui/jquery-ui.js"></script> <script type="text/javascript" src="/web/static/lib/jquery/jquery.browser.js"></script> <script type="text/javascript" src="/web/static/lib/jquery.blockUI/jquery.blockUI.js"></script> <script type="text/javascript" src="/web/static/lib/jquery.hotkeys/jquery.hotkeys.js"></script> @@ -64,19 +66,6 @@ <script src="/web/static/lib/bootstrap/js/transition.js"></script> </template> - <template id="jqueryui_conflict" name="jquery.ui.conflict"> - <!-- TODO: get rid of this hack once jQuery.ui is removed --> - <script type="text/javascript" charset="utf-8"> - $.fn.bstooltip = $.fn.tooltip; - $.fn.bsbutton = $.fn.button; - </script> - <t t-raw="0"/> - <script type="text/javascript" charset="utf-8"> - $.fn.tooltip = $.fn.bstooltip; - $.fn.button = $.fn.bsbutton; - </script> - </template> - <template id="web.assets_backend"> <!-- jQuery addons --> <script type="text/javascript" src="/web/static/lib/jquery.validate/jquery.validate.js"></script> @@ -91,13 +80,6 @@ <t t-call="web.bootstrap"/> - <!-- jQuery ui --> - <link rel="stylesheet" href="/web/static/lib/jquery.ui/jquery-ui.css"/> - - <t t-call="web.jqueryui_conflict"> - <script type="text/javascript" src="/web/static/lib/jquery.ui/jquery-ui.js"></script> - </t> - <link rel="stylesheet" href="/web/static/lib/jquery.ui.notify/css/ui.notify.css"/> <script type="text/javascript" src="/web/static/lib/jquery.ui.notify/js/jquery.notify.js"></script> diff --git a/addons/web_editor/__init__.py b/addons/web_editor/__init__.py new file mode 100644 index 00000000000..03ab5da2fb6 --- /dev/null +++ b/addons/web_editor/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +import controllers +import models +import tests diff --git a/addons/web_editor/__openerp__.py b/addons/web_editor/__openerp__.py new file mode 100644 index 00000000000..66fdff08ce1 --- /dev/null +++ b/addons/web_editor/__openerp__.py @@ -0,0 +1,21 @@ +{ + 'name': 'Web Editor', + 'category': 'Hidden', + 'description': """ +Odoo Web Editor widget. +========================== + +""", + 'author': 'Odoo S.A.', + 'depends': ['web'], + 'data': [ + 'security/ir.model.access.csv', + 'views/editor.xml', + 'views/iframe.xml', + 'views/snippets.xml', + ], + 'qweb': [ + 'static/src/xml/*.xml', + ], + 'auto_install': True +} diff --git a/addons/web_editor/controllers/__init__.py b/addons/web_editor/controllers/__init__.py new file mode 100644 index 00000000000..8ee9bae18d9 --- /dev/null +++ b/addons/web_editor/controllers/__init__.py @@ -0,0 +1 @@ +import main diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py new file mode 100644 index 00000000000..7d9b6206ae7 --- /dev/null +++ b/addons/web_editor/controllers/main.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +from openerp.http import request, STATIC_CACHE +from openerp.addons.web import http +import json +import io +from PIL import Image, ImageFont, ImageDraw +from openerp import tools +import cStringIO +import werkzeug.wrappers +import time +import logging +logger = logging.getLogger(__name__) + + +class Web_Editor(http.Controller): + #------------------------------------------------------ + # Backend snippet + #------------------------------------------------------ + @http.route('/web_editor/snippets', type='json', auth="user") + def snippets(self, **kwargs): + return request.registry["ir.ui.view"].render(request.cr, request.uid, 'web_editor.snippets', None, context=request.context) + + #------------------------------------------------------ + # Backend html field + #------------------------------------------------------ + @http.route('/web_editor/field/html', type='http', auth="user") + def FieldTextHtml(self, model=None, res_id=None, field=None, callback=None, **kwargs): + cr, uid, context = request.cr, request.uid, request.context + + kwargs.update( + model=model, + res_id=res_id, + field=field, + datarecord=json.loads(kwargs['datarecord']), + debug='debug' in kwargs) + + for k in kwargs: + if isinstance(kwargs[k], basestring) and kwargs[k].isdigit(): + kwargs[k] = int(kwargs[k]) + + trans = dict( + lang=kwargs.get('lang', context.get('lang')), + translatable=kwargs.get('translatable'), + edit_translations=kwargs.get('edit_translations'), + editable=kwargs.get('enable_editor')) + + context.update(trans) + kwargs.update(trans) + + record = None + if model and kwargs.get('res_id'): + record = request.registry[model].browse(cr, uid, kwargs.get('res_id'), context) + + kwargs.update(content=record and getattr(record, field) or "") + + return request.render(kwargs.get("template") or "web_editor.FieldTextHtml", kwargs, uid=request.uid) + + #------------------------------------------------------ + # Backend html field in inline mode + #------------------------------------------------------ + @http.route('/web_editor/field/html/inline', type='http', auth="user") + def FieldTextHtmlInline(self, model=None, res_id=None, field=None, callback=None, **kwargs): + kwargs['inline_mode'] = True + kwargs['dont_load_assets'] = not kwargs.get('enable_editor') and not kwargs.get('edit_translations') + return self.FieldTextHtml(model, res_id, field, callback, **kwargs) + + #------------------------------------------------------ + # convert font into picture + #------------------------------------------------------ + @http.route([ + '/web_editor/font_to_img/<icon>', + '/web_editor/font_to_img/<icon>/<color>', + '/web_editor/font_to_img/<icon>/<color>/<int:size>', + '/web_editor/font_to_img/<icon>/<color>/<int:size>/<int:alpha>', + ], type='http', auth="none") + def export_icon_to_png(self, icon, color='#000', size=100, alpha=255, font='/web/static/lib/fontawesome/fonts/fontawesome-webfont.ttf'): + """ This method converts FontAwesom pictograms to Images and is Used only + for mass mailing becuase custom fonts are not supported in mail. + :param icon : character from FontAwesom cheatsheet + :param color : RGB code of the color + :param size : Pixels in integer + :param alpha : transparency of the image from 0 to 255 + :param font : font path + + :returns PNG image converted from given font + """ + # Initialize font + addons_path = http.addons_manifest['web']['addons_path'] + font_obj = ImageFont.truetype(addons_path + font, size) + + # Determine the dimensions of the icon + image = Image.new("RGBA", (size, size), color=(0, 0, 0, 0)) + draw = ImageDraw.Draw(image) + + boxw, boxh = draw.textsize(icon, font=font_obj) + draw.text((0, 0), icon, font=font_obj) + left, top, right, bottom = image.getbbox() + + # Create an alpha mask + imagemask = Image.new("L", (boxw, boxh), 0) + drawmask = ImageDraw.Draw(imagemask) + drawmask.text((-left, -top), icon, font=font_obj, fill=alpha) + + # Create a solid color image and apply the mask + if color.startswith('rgba'): + color = color.replace('rgba', 'rgb') + color = ','.join(color.split(',')[:-1])+')' + iconimage = Image.new("RGBA", (boxw, boxh), color) + iconimage.putalpha(imagemask) + + # Create output image + outimage = Image.new("RGBA", (boxw, size), (0, 0, 0, 0)) + outimage.paste(iconimage, (left, top)) + + # output image + output = io.BytesIO() + outimage.save(output, format="PNG") + response = werkzeug.wrappers.Response() + response.mimetype = 'image/png' + response.data = output.getvalue() + response.headers['Cache-Control'] = 'public, max-age=604800' + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, POST' + response.headers['Connection'] = 'close' + response.headers['Date'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime()) + response.headers['Expires'] = time.strftime("%a, %d-%b-%Y %T GMT", time.gmtime(time.time()+604800*60)) + + return response + + #------------------------------------------------------ + # image route for browse record + #------------------------------------------------------ + def placeholder(self, response): + return request.registry['ir.attachment']._image_placeholder(response) + + @http.route([ + '/web_editor/image', + '/web_editor/image/<xmlid>', + '/web_editor/image/<xmlid>/<int:max_width>x<int:max_height>', + '/web_editor/image/<xmlid>/<field>', + '/web_editor/image/<xmlid>/<field>/<int:max_width>x<int:max_height>', + '/web_editor/image/<model>/<id>/<field>', + '/web_editor/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>' + ], type='http', auth="public") + def 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.ref(xmlid) + model, id = record._name, record.id + except: + raise werkzeug.exceptions.NotFound() + if model == 'ir.attachment' and not field: + if record.sudo().type == "url": + field = "url" + else: + field = "datas" + + if not model or not id or not field: + raise werkzeug.exceptions.NotFound() + + try: + idsha = str(id).split('_') + id = idsha[0] + response = werkzeug.wrappers.Response() + return request.registry['ir.attachment']._image( + request.cr, request.uid, model, id, field, response, max_width, max_height, + cache=STATIC_CACHE if len(idsha) > 1 else None) + except Exception: + logger.exception("Cannot render image field %r of record %s[%s] at size(%s,%s)", + field, model, id, max_width, max_height) + response = werkzeug.wrappers.Response() + return self.placeholder(response) + + #------------------------------------------------------ + # add attachment (images or link) + #------------------------------------------------------ + @http.route('/web_editor/attachment/add', type='http', auth='user', methods=['POST']) + def attach(self, func, upload=None, url=None, disable_optimization=None, **kwargs): + # the upload argument doesn't allow us to access the files if more than + # one file is uploaded, as upload references the first file + # therefore we have to recover the files from the request object + Attachments = request.registry['ir.attachment'] # registry for the attachment table + + uploads = [] + message = None + if not upload: # no image provided, storing the link and the image name + uploads.append({'website_url': url}) + name = url.split("/").pop() # recover filename + attachment_id = Attachments.create(request.cr, request.uid, { + 'name': name, + 'type': 'url', + 'url': url, + 'res_model': 'ir.ui.view', + }, request.context) + else: # images provided + try: + attachment_ids = [] + for c_file in request.httprequest.files.getlist('upload'): + image_data = c_file.read() + image = Image.open(cStringIO.StringIO(image_data)) + w, h = image.size + if w*h > 42e6: # Nokia Lumia 1020 photo resolution + raise ValueError( + u"Image size excessive, uploaded images must be smaller " + u"than 42 million pixel") + + if not disable_optimization and image.format in ('PNG', 'JPEG'): + image_data = tools.image_save_for_web(image) + + attachment_id = Attachments.create(request.cr, request.uid, { + 'name': c_file.filename, + 'datas': image_data.encode('base64'), + 'datas_fname': c_file.filename, + 'res_model': 'ir.ui.view', + }, request.context) + attachment_ids.append(attachment_id) + + uploads = Attachments.read( + request.cr, request.uid, attachment_ids, ['website_url'], + context=request.context) + except Exception, e: + logger.exception("Failed to upload image to attachment") + message = unicode(e) + + return """<script type='text/javascript'> + window.parent['%s'](%s, %s); + </script>""" % (func, json.dumps(uploads), json.dumps(message)) + + #------------------------------------------------------ + # remove attachment (images or link) + #------------------------------------------------------ + @http.route('/web_editor/attachment/remove', type='json', auth='user') + def remove(self, ids, **kwargs): + """ Removes a web-based image attachment if it is used by no view (template) + + Returns a dict mapping attachments which would not be removed (if any) + mapped to the views preventing their removal + """ + cr, uid, context = request.cr, request.uid, request.context + Attachment = request.registry['ir.attachment'] + Views = request.registry['ir.ui.view'] + + attachments_to_remove = [] + # views blocking removal of the attachment + removal_blocked_by = {} + + for attachment in Attachment.browse(cr, uid, ids, context=context): + # in-document URLs are html-escaped, a straight search will not + # find them + url = tools.html_escape(attachment.local_url) + ids = Views.search(cr, uid, ["|", ('arch_db', 'like', '"%s"' % url), ('arch_db', 'like', "'%s'" % url)], context=context) + + if ids: + removal_blocked_by[attachment.id] = Views.read( + cr, uid, ids, ['name'], context=context) + else: + attachments_to_remove.append(attachment.id) + if attachments_to_remove: + Attachment.unlink(cr, uid, attachments_to_remove, context=context) + return removal_blocked_by diff --git a/addons/web_editor/models/__init__.py b/addons/web_editor/models/__init__.py new file mode 100644 index 00000000000..43522002bfc --- /dev/null +++ b/addons/web_editor/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +import ir_attachment +import ir_qweb +import ir_ui_view +import ir_http + +import test_models diff --git a/addons/web_editor/models/ir_attachment.py b/addons/web_editor/models/ir_attachment.py new file mode 100644 index 00000000000..3068807b2ab --- /dev/null +++ b/addons/web_editor/models/ir_attachment.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- +import openerp +from openerp.osv import osv, fields + +import contextlib +from sys import maxint +from openerp.addons.web.http import request +import datetime +import hashlib +import time +import os + +from openerp.tools import html_escape as escape, ustr, image_resize_and_sharpen, image_save_for_web +from PIL import Image +import cStringIO + +import logging +logger = logging.getLogger(__name__) + + +class ir_attachment(osv.osv): + + _inherit = "ir.attachment" + + def _local_url_get(self, cr, uid, ids, name, arg, context=None): + result = {} + for attach in self.browse(cr, uid, ids, context=context): + if attach.url: + result[attach.id] = attach.url + else: + result[attach.id] = self.image_url(cr, uid, attach, 'datas') + return result + + _columns = { + 'local_url': fields.function(_local_url_get, string="Attachment URL", type='char'), + } + + def _image_placeholder(self, response): + # file_open may return a StringIO. StringIO can be closed but are + # not context managers in Python 2 though that is fixed in 3 + with contextlib.closing(openerp.tools.misc.file_open( + os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), + mode='rb')) as f: + response.data = f.read() + return response.make_conditional(request.httprequest) + + def _image(self, cr, uid, model, id_or_ids, 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] + ids = isinstance(id_or_ids, (list, tuple)) and id_or_ids or [int(id_or_ids)] + ids = Model.search(cr, uid, [('id', 'in', ids)], context=context) + + if not ids: + return self._image_placeholder(response) + + concurrency = '__last_update' + [record] = Model.read(cr, openerp.SUPERUSER_ID, ids, [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' and field in record: + path = record[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) + 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('.', '_'), ids[0], 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 + + def image_url(self, cr, uid, record, field, size=None, context=None): + """Returns a local url that points to the image field of a given browse record.""" + model = record._name + sudo_record = record.sudo() + id = '%s_%s' % (record.id, hashlib.sha1(sudo_record.write_date or sudo_record.create_date or '').hexdigest()[0:7]) + size = '' if size is None else '/%s' % size + return '/web_editor/image/%s/%s/%s%s' % (model, id, field, size) diff --git a/addons/web_editor/models/ir_http.py b/addons/web_editor/models/ir_http.py new file mode 100644 index 00000000000..c72fee55e83 --- /dev/null +++ b/addons/web_editor/models/ir_http.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +from openerp.osv import orm +from openerp.http import request + + +class ir_http(orm.AbstractModel): + _inherit = 'ir.http' + + def _dispatch(self): + if 'editable' in request.httprequest.args and 'editable' not in request.context: + request.context['editable'] = True + if 'edit_translations' in request.httprequest.args and 'edit_translations' not in request.context: + request.context['edit_translations'] = True + if request.context.get('lang') != "en_US" and 'translatable' not in request.context: + request.context['translatable'] = True + return super(ir_http, self)._dispatch() diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py new file mode 100644 index 00000000000..9642a3c2881 --- /dev/null +++ b/addons/web_editor/models/ir_qweb.py @@ -0,0 +1,566 @@ +# -*- coding: utf-8 -*- +""" +Web_editor-context rendering needs to add some metadata to rendered and allow to edit fields, +as well as render a few fields differently. + +Also, adds methods to convert values back to openerp models. +""" + +import cStringIO +import datetime +import itertools +import logging +import os +import urllib2 +import urlparse +import re +import simplejson + +import pytz +from dateutil import parser +from lxml import etree, html +from PIL import Image as I +import openerp.modules + +import openerp +from openerp.osv import orm, fields +from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools import html_escape as escape +from openerp.addons.base.ir import ir_qweb + +REMOTE_CONNECTION_TIMEOUT = 2.5 + +logger = logging.getLogger(__name__) + + +class QWeb(orm.AbstractModel): + """ QWeb object for rendering editor stuff + """ + _name = 'ir.qweb' + _inherit = 'ir.qweb' + + re_remove_spaces = re.compile('\s+') + + def render_tag_snippet(self, element, template_attributes, generated_attributes, qwebcontext): + cr = qwebcontext['request'].cr + uid = qwebcontext['request'].uid + view_reg = self.pool['ir.ui.view'] + page_xmlid = self.eval_format(template_attributes["snippet"], qwebcontext) + view = view_reg.browse(cr, uid, view_reg.search(cr, uid, [('key', '=', page_xmlid)])) + generated_attributes += " " + "".join([ + 'data-oe-name="%s"' % view.name, + 'data-oe-type="snippet"', + 'data-oe-thumbnail="%s"' % template_attributes.get('thumbnail', "oe-thumbnail")]) + + template_attributes['call'] = template_attributes['snippet'] + return self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext) + + def get_converter_for(self, field_type): + return self.pool.get('ir.qweb.field.' + field_type, self.pool['ir.qweb.field']) + + +#------------------------------------------------------ +# QWeb fields +#------------------------------------------------------ + + +class Field(orm.AbstractModel): + _name = 'ir.qweb.field' + _inherit = 'ir.qweb.field' + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context, context=None): + if options is None: + options = {} + field = record._model._fields[field_name] + attrs = [] + + placeholder = options.get('placeholder') \ + or source_element.get('placeholder') \ + or getattr(field, 'placeholder', None) + if placeholder: + attrs.append(('placeholder', placeholder)) + + return itertools.chain( + super(Field, self).attributes(cr, uid, field_name, record, options, + source_element, g_att, t_att, + qweb_context, context=context), + attrs + ) + + def value_from_string(self, value): + return value + + def from_html(self, cr, uid, model, field, element, context=None): + return self.value_from_string(element.text_content().strip()) + + def qweb_object(self): + return self.pool['ir.qweb'] + + +class Integer(orm.AbstractModel): + _name = 'ir.qweb.field.integer' + _inherit = ['ir.qweb.field'] + + value_from_string = int + + +class Float(orm.AbstractModel): + _name = 'ir.qweb.field.float' + _inherit = ['ir.qweb.field', 'ir.qweb.field.float'] + + def from_html(self, cr, uid, model, field, element, context=None): + lang = self.user_lang(cr, uid, context=context) + + value = element.text_content().strip() + + return float(value.replace(lang.thousands_sep, '') + .replace(lang.decimal_point, '.')) + + +class ManyToOne(orm.AbstractModel): + _name = 'ir.qweb.field.many2one' + _inherit = ['ir.qweb.field.many2one'] + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context, + context=None): + attrs = super(ManyToOne, self).attributes( + cr, uid, field_name, record, options, source_element, g_att, t_att, + qweb_context, context=context) + many2one = getattr(record, field_name) + if many2one: + data = [('data-oe-many2one-id', many2one.id), + ('data-oe-many2one-model', many2one._name)] + return itertools.chain(attrs, data) + else: + return attrs + + def from_html(self, cr, uid, model, field, element, context=None): + Model = self.pool[element.get('data-oe-model')] + M2O = self.pool[field.comodel_name] + field_name = element.get('data-oe-field') + id = int(element.get('data-oe-id')) + many2one_id = int(element.get('data-oe-many2one-id')) + + if many2one_id and M2O.exists(cr, uid, [many2one_id]): + # save the new id of the many2one + Model.write(cr, uid, [id], { + field_name: many2one_id + }, context=context) + + # not necessary, but might as well be explicit about it + return None + + +class Contact(orm.AbstractModel): + _name = 'ir.qweb.field.contact' + _inherit = ['ir.qweb.field.contact', 'ir.qweb.field.many2one'] + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context, + context=None): + attrs = super(Contact, self).attributes( + cr, uid, field_name, record, options, source_element, g_att, t_att, + qweb_context, context=context) + if getattr(record, field_name): + return itertools.chain(attrs, [('data-oe-contact-options', simplejson.dumps(options))]) + else: + return attrs + + # helper to call the rendering of contact field + def get_record_to_html(self, cr, uid, ids, options=None, context=None): + node = self.record_to_html(cr, uid, 'record', { + 'record': self.pool['res.partner'].browse(cr, uid, ids[0], context=context)}, + options=options, context=context) + return node and node.__html__() + + +def parse_fuzzy(in_format, value): + day_first = in_format.find('%d') < in_format.find('%m') + + if '%y' in in_format: + year_first = in_format.find('%y') < in_format.find('%d') + else: + year_first = in_format.find('%Y') < in_format.find('%d') + + return parser.parse(value, dayfirst=day_first, yearfirst=year_first) + + +class Date(orm.AbstractModel): + _name = 'ir.qweb.field.date' + _inherit = ['ir.qweb.field', 'ir.qweb.field.date'] + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context, + context=None): + attrs = super(Date, self).attributes( + cr, uid, field_name, record, options, source_element, g_att, t_att, + qweb_context, context=None) + return itertools.chain(attrs, [('data-oe-original', record[field_name])]) + + def from_html(self, cr, uid, model, field, element, context=None): + value = element.text_content().strip() + if not value: + return False + + datetime.datetime.strptime(value, DEFAULT_SERVER_DATE_FORMAT) + return value + + +class DateTime(orm.AbstractModel): + _name = 'ir.qweb.field.datetime' + _inherit = ['ir.qweb.field', 'ir.qweb.field.datetime'] + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context, + context=None): + value = record[field_name] + if isinstance(value, basestring): + value = datetime.datetime.strptime( + value, DEFAULT_SERVER_DATETIME_FORMAT) + if value: + # convert from UTC (server timezone) to user timezone + value = fields.datetime.context_timestamp( + cr, uid, timestamp=value, context=context) + value = value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + + attrs = super(DateTime, self).attributes( + cr, uid, field_name, record, options, source_element, g_att, t_att, + qweb_context, context=None) + return itertools.chain(attrs, [ + ('data-oe-original', value) + ]) + + def from_html(self, cr, uid, model, field, element, context=None): + if context is None: + context = {} + value = element.text_content().strip() + if not value: + return False + + # parse from string to datetime + dt = datetime.datetime.strptime(value, DEFAULT_SERVER_DATETIME_FORMAT) + + # convert back from user's timezone to UTC + tz_name = context.get('tz') \ + or self.pool['res.users'].read(cr, openerp.SUPERUSER_ID, uid, ['tz'], context=context)['tz'] + if tz_name: + try: + user_tz = pytz.timezone(tz_name) + utc = pytz.utc + + dt = user_tz.localize(dt).astimezone(utc) + except Exception: + logger.warn( + "Failed to convert the value for a field of the model" + " %s back from the user's timezone (%s) to UTC", + model, tz_name, + exc_info=True) + + # format back to string + return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) + + +class Text(orm.AbstractModel): + _name = 'ir.qweb.field.text' + _inherit = ['ir.qweb.field', 'ir.qweb.field.text'] + + def from_html(self, cr, uid, model, field, element, context=None): + return html_to_text(element) + + +class Selection(orm.AbstractModel): + _name = 'ir.qweb.field.selection' + _inherit = ['ir.qweb.field', 'ir.qweb.field.selection'] + + def from_html(self, cr, uid, model, field, element, context=None): + record = self.browse(cr, uid, [], context=context) + value = element.text_content().strip() + selection = field.get_description(record.env)['selection'] + for k, v in selection: + if isinstance(v, str): + v = ustr(v) + if value == v: + return k + + raise ValueError(u"No value found for label %s in selection %s" % ( + value, selection)) + + +class HTML(orm.AbstractModel): + _name = 'ir.qweb.field.html' + _inherit = ['ir.qweb.field', 'ir.qweb.field.html'] + + def from_html(self, cr, uid, model, field, element, context=None): + content = [] + if element.text: + content.append(element.text) + content.extend(html.tostring(child) + for child in element.iterchildren(tag=etree.Element)) + return '\n'.join(content) + + +class Image(orm.AbstractModel): + """ + Widget options: + + ``class`` + set as attribute on the generated <img> tag + """ + _name = 'ir.qweb.field.image' + _inherit = ['ir.qweb.field', 'ir.qweb.field.image'] + + def to_html(self, cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context, context=None): + assert source_element.tag != 'img',\ + "Oddly enough, the root tag of an image field can not be img. " \ + "That is because the image goes into the tag, or it gets the " \ + "hose again." + + return super(Image, self).to_html( + cr, uid, field_name, record, options, + source_element, t_att, g_att, qweb_context, context=context) + + def record_to_html(self, cr, uid, field_name, record, options=None, context=None): + if options is None: + options = {} + aclasses = ['img', 'img-responsive'] + options.get('class', '').split() + classes = ' '.join(itertools.imap(escape, aclasses)) + + max_size = None + max_width, max_height = options.get('max_width', 0), options.get('max_height', 0) + if max_width or max_height: + max_size = '%sx%s' % (max_width, max_height) + + src = self.pool['ir.attachment'].image_url(cr, uid, record, field_name, max_size) + if options.get('resize'): + src = "%s/%s" % (src, options.get('resize')) + + img = '<img class="%s" src="%s" style="%s"/>' % (classes, src, options.get('style', '')) + return ir_qweb.HTMLSafe(img) + + local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$') + + def from_html(self, cr, uid, model, field, element, context=None): + url = element.find('img').get('src') + + url_object = urlparse.urlsplit(url) + if url_object.path.startswith('/web_editor/image'): + # url might be /web_editor/image/<model>/<id>[_<checksum>]/<field>[/<width>x<height>] + fragments = url_object.path.split('/') + query = dict(urlparse.parse_qsl(url_object.query)) + model = query.get('model', fragments[3]) + oid = query.get('id', fragments[4].split('_')[0]) + field = query.get('field', fragments[5]) + item = self.pool[model].browse(cr, uid, int(oid), context=context) + return item[field] + + if self.local_url_re.match(url_object.path): + return self.load_local_url(url) + + return self.load_remote_url(url) + + def load_local_url(self, url): + match = self.local_url_re.match(urlparse.urlsplit(url).path) + + rest = match.group('rest') + for sep in os.sep, os.altsep: + if sep and sep != '/': + rest.replace(sep, '/') + + path = openerp.modules.get_module_resource( + match.group('module'), 'static', *(rest.split('/'))) + + if not path: + return None + + try: + with open(path, 'rb') as f: + # force complete image load to ensure it's valid image data + image = I.open(f) + image.load() + f.seek(0) + return f.read().encode('base64') + except Exception: + logger.exception("Failed to load local image %r", url) + return None + + def load_remote_url(self, url): + try: + # should probably remove remote URLs entirely: + # * in fields, downloading them without blowing up the server is a + # challenge + # * in views, may trigger mixed content warnings if HTTPS CMS + # linking to HTTP images + # implement drag & drop image upload to mitigate? + + req = urllib2.urlopen(url, timeout=REMOTE_CONNECTION_TIMEOUT) + # PIL needs a seekable file-like image, urllib result is not seekable + image = I.open(cStringIO.StringIO(req.read())) + # force a complete load of the image data to validate it + image.load() + except Exception: + logger.exception("Failed to load remote image %r", url) + return None + + # don't use original data in case weird stuff was smuggled in, with + # luck PIL will remove some of it? + out = cStringIO.StringIO() + image.save(out, image.format) + return out.getvalue().encode('base64') + + +class Monetary(orm.AbstractModel): + _name = 'ir.qweb.field.monetary' + _inherit = ['ir.qweb.field', 'ir.qweb.field.monetary'] + + def from_html(self, cr, uid, model, field, element, context=None): + lang = self.user_lang(cr, uid, context=context) + + value = element.find('span').text.strip() + + return float(value.replace(lang.thousands_sep, '') + .replace(lang.decimal_point, '.')) + + +class Duration(orm.AbstractModel): + _name = 'ir.qweb.field.duration' + _inherit = [ + 'ir.qweb.field.duration', + 'ir.qweb.field.float', + ] + + def attributes(self, cr, uid, field_name, record, options, + source_element, g_att, t_att, qweb_context, + context=None): + attrs = super(Duration, self).attributes( + cr, uid, field_name, record, options, source_element, g_att, t_att, + qweb_context, context=None) + return itertools.chain(attrs, [('data-oe-original', record[field_name])]) + + def from_html(self, cr, uid, model, field, element, context=None): + value = element.text_content().strip() + + # non-localized value + return float(value) + + +class RelativeDatetime(orm.AbstractModel): + _name = 'ir.qweb.field.relative' + _inherit = [ + 'ir.qweb.field.relative', + 'ir.qweb.field.datetime', + ] + + # get formatting from ir.qweb.field.relative but edition/save from datetime + + +class QwebView(orm.AbstractModel): + _name = 'ir.qweb.field.qweb' + _inherit = ['ir.qweb.field.qweb'] + + +def html_to_text(element): + """ Converts HTML content with HTML-specified line breaks (br, p, div, ...) + in roughly equivalent textual content. + + Used to replace and fixup the roundtripping of text and m2o: when using + libxml 2.8.0 (but not 2.9.1) and parsing HTML with lxml.html.fromstring + whitespace text nodes (text nodes composed *solely* of whitespace) are + stripped out with no recourse, and fundamentally relying on newlines + being in the text (e.g. inserted during user edition) is probably poor form + anyway. + + -> this utility function collapses whitespace sequences and replaces + nodes by roughly corresponding linebreaks + * p are pre-and post-fixed by 2 newlines + * br are replaced by a single newline + * block-level elements not already mentioned are pre- and post-fixed by + a single newline + + ought be somewhat similar (but much less high-tech) to aaronsw's html2text. + the latter produces full-blown markdown, our text -> html converter only + replaces newlines by <br> elements at this point so we're reverting that, + and a few more newline-ish elements in case the user tried to add + newlines/paragraphs into the text field + + :param element: lxml.html content + :returns: corresponding pure-text output + """ + + # output is a list of str | int. Integers are padding requests (in minimum + # number of newlines). When multiple padding requests, fold them into the + # biggest one + output = [] + _wrap(element, output) + + # remove any leading or tailing whitespace, replace sequences of + # (whitespace)\n(whitespace) by a single newline, where (whitespace) is a + # non-newline whitespace in this case + return re.sub( + r'[ \t\r\f]*\n[ \t\r\f]*', + '\n', + ''.join(_realize_padding(output)).strip()) + +_PADDED_BLOCK = set('p h1 h2 h3 h4 h5 h6'.split()) +# https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements minus p +_MISC_BLOCK = set(( + 'address article aside audio blockquote canvas dd dl div figcaption figure' + ' footer form header hgroup hr ol output pre section tfoot ul video' +).split()) + + +def _collapse_whitespace(text): + """ Collapses sequences of whitespace characters in ``text`` to a single + space + """ + return re.sub('\s+', ' ', text) + + +def _realize_padding(it): + """ Fold and convert padding requests: integers in the output sequence are + requests for at least n newlines of padding. Runs thereof can be collapsed + into the largest requests and converted to newlines. + """ + padding = None + for item in it: + if isinstance(item, int): + padding = max(padding, item) + continue + + if padding: + yield '\n' * padding + padding = None + + yield item + # leftover padding irrelevant as the output will be stripped + + +def _wrap(element, output, wrapper=u''): + """ Recursively extracts text from ``element`` (via _element_to_text), and + wraps it all in ``wrapper``. Extracted text is added to ``output`` + + :type wrapper: basestring | int + """ + output.append(wrapper) + if element.text: + output.append(_collapse_whitespace(element.text)) + for child in element: + _element_to_text(child, output) + output.append(wrapper) + + +def _element_to_text(e, output): + if e.tag == 'br': + output.append(u'\n') + elif e.tag in _PADDED_BLOCK: + _wrap(e, output, 2) + elif e.tag in _MISC_BLOCK: + _wrap(e, output, 1) + else: + # inline + _wrap(e, output) + + if e.tail: + output.append(_collapse_whitespace(e.tail)) diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py new file mode 100644 index 00000000000..5946e7128bb --- /dev/null +++ b/addons/web_editor/models/ir_ui_view.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +import copy +import openerp +from openerp.exceptions import AccessError +from openerp.osv import osv +from lxml import etree, html + + +class view(osv.osv): + _inherit = 'ir.ui.view' + + def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None): + if not values: + values = {} + if values.get('editable'): + try: + if not isinstance(id_or_xml_id, (int, long)): + if '.' not in id_or_xml_id: + raise ValueError('Invalid template id: %r' % (id_or_xml_id,)) + id_or_xml_id = self.get_view_id(cr, uid, id_or_xml_id, context=context) + + self.check_access_rule(cr, uid, [id_or_xml_id], 'write', context=context) + + except AccessError: + values['editable'] = False + + return super(view, self).render(cr, uid, id_or_xml_id, values=values, engine=engine, context=context) + + #------------------------------------------------------ + # Save from html + #------------------------------------------------------ + + def extract_embedded_fields(self, cr, uid, arch, context=None): + return arch.xpath('//*[@data-oe-model != "ir.ui.view"]') + + def save_embedded_field(self, cr, uid, el, context=None): + Model = self.pool[el.get('data-oe-model')] + field = el.get('data-oe-field') + + converter = self.pool['ir.qweb'].get_converter_for(el.get('data-oe-type')) + value = converter.from_html(cr, uid, Model, Model._fields[field], el) + + if value is not None: + # TODO: batch writes? + Model.write(cr, uid, [int(el.get('data-oe-id'))], { + field: value + }, context=context) + + def _pretty_arch(self, arch): + # remove_blank_string does not seem to work on HTMLParser, and + # pretty-printing with lxml more or less requires stripping + # whitespace: http://lxml.de/FAQ.html#why-doesn-t-the-pretty-print-option-reformat-my-xml-output + # so serialize to XML, parse as XML (remove whitespace) then serialize + # as XML (pretty print) + arch_no_whitespace = etree.fromstring( + etree.tostring(arch, encoding='utf-8'), + parser=etree.XMLParser(encoding='utf-8', remove_blank_text=True)) + return etree.tostring( + arch_no_whitespace, encoding='unicode', pretty_print=True) + + def replace_arch_section(self, cr, uid, view_id, section_xpath, replacement, context=None): + # the root of the arch section shouldn't actually be replaced as it's + # not really editable itself, only the content truly is editable. + + [view] = self.browse(cr, uid, [view_id], context=context) + arch = etree.fromstring(view.arch.encode('utf-8')) + # => get the replacement root + if not section_xpath: + root = arch + else: + # ensure there's only one match + [root] = arch.xpath(section_xpath) + + root.text = replacement.text + root.tail = replacement.tail + # replace all children + del root[:] + for child in replacement: + root.append(copy.deepcopy(child)) + + return arch + + def to_field_ref(self, cr, uid, el, context=None): + # filter out meta-information inserted in the document + attributes = dict((k, v) for k, v in el.items() + if not k.startswith('data-oe-')) + attributes['t-field'] = el.get('data-oe-expression') + + out = html.html_parser.makeelement(el.tag, attrib=attributes) + out.tail = el.tail + return out + + def save(self, cr, uid, res_id, value, xpath=None, context=None): + """ Update a view section. The view section may embed fields to write + + :param str model: + :param int res_id: + :param str xpath: valid xpath to the tag to replace + """ + res_id = int(res_id) + + arch_section = html.fromstring( + value, parser=html.HTMLParser(encoding='utf-8')) + + if xpath is None: + # value is an embedded field on its own, not a view section + self.save_embedded_field(cr, uid, arch_section, context=context) + return + + for el in self.extract_embedded_fields(cr, uid, arch_section, context=context): + self.save_embedded_field(cr, uid, el, context=context) + + # transform embedded field back to t-field + el.getparent().replace(el, self.to_field_ref(cr, uid, el, context=context)) + + arch = self.replace_arch_section(cr, uid, res_id, xpath, arch_section, context=context) + self.write(cr, uid, res_id, { + 'arch': self._pretty_arch(arch) + }, context=context) + + view = self.browse(cr, openerp.SUPERUSER_ID, res_id, context=context) + if view.model_data_id: + view.model_data_id.write({'noupdate': True}) diff --git a/addons/website/models/test_models.py b/addons/web_editor/models/test_models.py index 52e614ade4a..100b49b6448 100644 --- a/addons/website/models/test_models.py +++ b/addons/web_editor/models/test_models.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- from openerp.osv import orm, fields + class test_converter(orm.Model): - _name = 'website.converter.test' + _name = 'web_editor.converter.test' # disable translation export for those brilliant field labels and values _translate = False @@ -12,7 +13,7 @@ class test_converter(orm.Model): 'integer': fields.integer(), 'float': fields.float(), 'numeric': fields.float(digits=(16, 2)), - 'many2one': fields.many2one('website.converter.test.sub'), + 'many2one': fields.many2one('web_editor.converter.test.sub'), 'binary': fields.binary(), 'date': fields.date(), 'datetime': fields.datetime(), @@ -35,7 +36,7 @@ class test_converter(orm.Model): class test_converter_sub(orm.Model): - _name = 'website.converter.test.sub' + _name = 'web_editor.converter.test.sub' _columns = { 'name': fields.char(), diff --git a/addons/web_editor/security/ir.model.access.csv b/addons/web_editor/security/ir.model.access.csv new file mode 100644 index 00000000000..a02dd5f1957 --- /dev/null +++ b/addons/web_editor/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_web_editor_converter_test,access_web_editor_converter_test,model_web_editor_converter_test,,1,1,1,1 +access_web_editor_converter_test_sub,access_web_editor_converter_test_sub,model_web_editor_converter_test_sub,,1,1,1,1 diff --git a/addons/web_editor/static/src/css/backend.css b/addons/web_editor/static/src/css/backend.css new file mode 100644 index 00000000000..decaed2d593 --- /dev/null +++ b/addons/web_editor/static/src/css/backend.css @@ -0,0 +1,73 @@ +.oe_form_field_html_text .note-editable { + min-height: 180px; + font: initial !important; + line-height: initial !important; + color: initial !important; +} +.oe_form_field_html_text .note-editable p { + margin: initial; +} +.oe_form_field_html_text .note-editable a, .oe_form_field_html_text .note-editable a:hover { + color: initial; +} + +.oe_form_field_html iframe { + display: block; + width: 100%; + border: 0; + padding: 0; + margin: 0; + min-height: 300px; + min-height: -webkit-calc(100vh - 170px); + min-height: calc(100vh - 170px); +} +.o_form_FieldTextHtml_fullscreen .oe_form_field_html.o_form_fullscreen_ancestor iframe { + position: absolute !important; + left: 0 !important; + right: 0 !important; + top: 0 !important; + bottom: 0 !important; + width: 100% !important; + min-height: 100% !important; + z-index: 1001 !important; + border: 0; +} + +.o_form_FieldTextHtml_fullscreen * { + display: none; +} +.o_form_FieldTextHtml_fullscreen .o_form_fullscreen_ancestor { + display: block !important; + position: static !important; + top: 0 !important; + left: 0 !important; + overflow: visible !important; + width: auto !important; + overflow: hidden !important; +} + + +.openerp .oe_form .oe_form_field_html_translate textarea { + min-width: 60px; + width: 100%; + color: #4c4c4c; + font: normal 13px "Lucida Grande", Helvetica, Verdana, Arial, sans-serif; +} +.openerp .oe_form_readonly .oe_form .oe_form_field_html_translate textarea { + border:0; + background: #fff; +} +.openerp .oe_form_editable .oe_form .oe_form_field_html_translate > button { + float: right; + margin: -6px -3px 0 0; +} +.openerp .oe_form_editable .oe_form .oe_form_field_html_translate > .note-editable { + border: 1px solid #ccc; + min-height: 22px; + border-radius: 3px; +} +.openerp .oe_form_editable .oe_form .oe_form_field_html_translate .note-editable [contentEditable='true']:hover { + -moz-box-shadow: #DDCC33 0 0 5px 2px inset; + -webkit-box-shadow: #DDCC33 0 0 5px 2px inset; + box-shadow: #DDCC33 0 0 5px 2px inset; +}
\ No newline at end of file diff --git a/addons/web_editor/static/src/css/common.sass b/addons/web_editor/static/src/css/common.sass new file mode 100644 index 00000000000..264f0166c49 --- /dev/null +++ b/addons/web_editor/static/src/css/common.sass @@ -0,0 +1,4 @@ +@import "compass/css3" +@import "compass/css3/user-interface" +@import "compass/css3/transition" +@import "compass/support" diff --git a/addons/web_editor/static/src/css/inline.css b/addons/web_editor/static/src/css/inline.css new file mode 100644 index 00000000000..61e995fa3fa --- /dev/null +++ b/addons/web_editor/static/src/css/inline.css @@ -0,0 +1,64 @@ +body { + background-color: #fff; +} +#editable_area p { + margin: initial; +} +#editable_area a { + color: inherit; +} +#editable_area a:hover { + color: inherit; +} +#editable_area { + margin-top: 0 !important; + font: inherit !important; + line-height: inherit !important; + color: inherit !important; +} +#oe_snippets ~ #wrapwrap #editable_area { + margin-bottom: 100px; +} + +#wrapwrap > main > div:first-child { + max-width: 600px; + margin-left: auto; + margin-right: auto; + position: relative; +} + +.editor_enable #wrapwrap > main > div:first-child { + border-top: 1px dashed #888; + margin-top: 50px; +} + +.editor_enable #wrapwrap > main > div:first-child:before, #wrapwrap > main > div:first-child:after { + content: " "; + display: block; + height: 100%; + vertical-align: top; + position: fixed; + top: -4px; + overflow: hidden; + z-index: -1; + margin-top: 54px; +} +.editor_enable #wrapwrap > main > div:first-child[data-oe-placeholder]:empty:before { + content: " \00a0 " attr(data-oe-placeholder); +} + +.editor_enable #wrapwrap > main > div:first-child:before { + border-left: 1px dashed #000; + opacity: 0.4; + margin-left: -5px; +} +.editor_enable #wrapwrap > main > div:first-child:after { + border-left: 1px dashed #000; + opacity: 0.4; + margin-left: 605px; + width: 0; +} + +.note-color-palette .colorpicker { + display: none; +} diff --git a/addons/web_editor/static/src/css/snippets.css b/addons/web_editor/static/src/css/snippets.css new file mode 100644 index 00000000000..ca2ba0c6deb --- /dev/null +++ b/addons/web_editor/static/src/css/snippets.css @@ -0,0 +1,490 @@ +/* ---- SNIPPET EDITOR ---- {{{ */ +#oe_snippets #o_left_bar, #oe_snippets #o_arrow { + position: fixed; + top: 34px; + left: 0px; + bottom: 0; + background: #282828; + -moz-box-shadow: 0px 10px 10px -10px black inset; + -webkit-box-shadow: 0px 10px 10px -10px black inset; + box-shadow: 0px 10px 10px -10px black inset; +} +#oe_snippets #o_arrow { + z-index: 1009; + width: 24px; + opacity: 0.75; +} +#oe_snippets #o_arrow > * { + position: absolute; + color: white; +} +#oe_snippets #o_arrow div { + -moz-transform: translateX(-36%) rotate(-90deg); + -ms-transform: translateX(-36%) rotate(-90deg); + -webkit-transform: translateX(-36%) rotate(-90deg); + transform: translateX(-36%) rotate(-90deg); + top: 50%; + white-space: nowrap; +} +#oe_snippets #o_arrow .fa { + width: 100%; + left: 40%; +} +#oe_snippets #o_left_bar { + left: -228px; + z-index: 1010; + width: 208px; + -moz-transition: all 500ms ease; + -o-transition: all 500ms ease; + -webkit-transition: all 500ms ease; + transition: all 500ms ease; +} +#oe_snippets #o_left_bar ul { + text-align: center; + margin: 0; + padding: 6px 0; + list-style-type: none; +} +#oe_snippets #o_left_bar ul li { + display: inline-block; + padding: 0 3px; +} +#oe_snippets #o_left_bar ul li a { + color: #aaa; +} +#oe_snippets #o_left_bar ul li .fa-stack { + overflow: hidden; +} +#oe_snippets #o_left_bar ul li.active a.fa, #oe_snippets #o_left_bar ul li.active .fa-stack-1x { + color: #428bca; +} +#oe_snippets.o_open #o_left_bar { + left: 0; +} +#oe_snippets .o_panel { + margin: 0; + border: 0; + width: 208px; +} +#oe_snippets .o_panel_header { + background: #dff0d8; + padding: 1px 8px; + color: #414141; + text-align: center; +} +#oe_snippets .o_panel_header .fa-stack { + overflow: hidden; + font-size: 1em; +} +#oe_snippets .o_panel_body { + white-space: normal; + background: black; + padding: 7px 3px; +} +#oe_snippets .o_panel_body > div { + background: black; +} +#oe_snippets .o_panel_body > div label { + width: 44px; + color: #fff; + padding-left: 10px; +} +#oe_snippets .o_panel_body > div label div { + width: 100px; + text-align: center; + -moz-transform: translate(-39px, 44px); + -webkit-transform: translate(-39px, 44px); + transform: translate(-39px, 44px); + -moz-transform-origin: 50% 50%; + -ms-transform-origin: 50% 50%; + -webkit-transform-origin: 50% 50%; + transform-origin: 50% 50%; +} + +.oe_snippet { + display: inline-block; + vertical-align: top; + width: 93px; + margin-left: 3px; + margin-right: 3px; + margin-top: 1px; + position: relative; + overflow: hidden; + -moz-user-select: -moz-none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; + cursor: move; +} +.oe_snippet .oe_snippet_thumbnail { + text-align: center; + height: 100%; + background: transparent; + color: white; + position: relative; +} +.oe_snippet .oe_snippet_thumbnail:hover .oe_snippet_thumbnail_img { + -moz-transform: scale(0.95, 0.95); + -ms-transform: scale(0.95, 0.95); + -webkit-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; + width: 92px; + background-size: cover; + -moz-transform: scale(1, 1); + -ms-transform: scale(1, 1); + -webkit-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_snippet.disable .oe_snippet_thumbnail { + background-color: rgba(255, 0, 0, 0.75); +} +.oe_snippet.disable .oe_snippet_thumbnail_img { + opacity: 0.8; +} + +#oe_snippets .oe_snippet_thumbnail { + background: #747474, url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjUwJSIgY3k9IjUwJSIgcj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIgc3RvcC1vcGFjaXR5PSIwLjI1Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuNCIvPjwvcmFkaWFsR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); + background: #747474, -moz-radial-gradient(rgba(0, 0, 0, 0.25), 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, 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; + -moz-transition: margin 250ms linear; + -o-transition: margin 250ms linear; + -webkit-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_vertical_text { + width: 48px; + float: none; + display: inline-block; + position: relative; + margin: 0px -24px !important; +} +.oe_drop_zone.oe_insert.oe_overlay { + -moz-border-radius: 3px; + -webkit-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); + -moz-border-radius: 4px; + -webkit-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: transparent; + z-index: 1001; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + -moz-transition: opacity 100ms linear; + -o-transition: opacity 100ms linear; + -webkit-transition: opacity 100ms linear; + transition: opacity 100ms linear; + -moz-box-sizing: border-box; + -webkit-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%; + -moz-box-sizing: border-box; + -webkit-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; + -moz-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset; + -webkit-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); + -moz-border-radius: 5px; + -webkit-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); + -moz-transition: background 100ms linear; + -o-transition: background 100ms linear; + -webkit-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: #fff; + -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7); + -webkit-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: ""; + 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: ""; + 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: #fff; + -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7); + -webkit-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-group > div { + float: left; +} +.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: "" !important; +} +.oe_overlay.block-w-right .w:before { + content: "" !important; +} +.oe_overlay.block-w-left.block-w-right .w:before { + display: none !important; +} +.oe_overlay.block-e-left .e:before { + content: "" !important; +} +.oe_overlay.block-e-right .e:before { + content: "" !important; +} +.oe_overlay.block-e-left.block-e-right .e:before { + display: none !important; +} +.oe_overlay.block-s-top .s:before { + content: "" !important; +} +.oe_overlay.block-s-bottom .s:before { + content: "" !important; +} +.oe_overlay.block-n-top .n:before { + content: "" !important; +} +.oe_overlay.block-n-bottom .n:before { + content: "" !important; +} +.oe_overlay.o_keypress { + opacity: 0; + -webkit-transition: 0.8s; + -moz-transition: 0.8s; + -o-transition: 0.8s; + -transition: 0.8s; +} + +.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; +}
\ No newline at end of file diff --git a/addons/web_editor/static/src/css/snippets.sass b/addons/web_editor/static/src/css/snippets.sass new file mode 100644 index 00000000000..752dabafc78 --- /dev/null +++ b/addons/web_editor/static/src/css/snippets.sass @@ -0,0 +1,376 @@ +@import "compass/css3" +@import "compass/css3/user-interface" +@import "compass/css3/transition" + + +/* ---- SNIPPET EDITOR ---- {{{ */ + +#oe_snippets + #o_left_bar, #o_arrow + position: fixed + top: 34px + left: 0px + bottom: 0 + background: rgb(40,40,40) + +box-shadow(0px 10px 10px -10px black inset) + #o_arrow + z-index: 1009 + width: 24px + opacity: 0.75 + > * + position: absolute + color: white + div + @include transform( translateX(-36%) rotate(-90deg) ) + top: 50% + white-space: nowrap + .fa + width: 100% + left: 40% + #o_left_bar + left: -228px + z-index: 1010 + width: 208px + @include transition(all 500ms ease) + ul + text-align: center + margin: 0 + padding: 6px 0 + list-style-type: none + li + display: inline-block + padding: 0 3px + a + color: #aaa + .fa-stack + overflow: hidden + li.active a.fa, li.active .fa-stack-1x + color: #428bca + + &.o_open #o_left_bar + left: 0 + .o_panel + margin: 0 + border: 0 + width: 208px + .o_panel_header + background: #dff0d8 + padding: 1px 8px + color: #414141 + text-align: center + .fa-stack + overflow: hidden + font-size: 1em + .o_panel_body + white-space: normal + background: black + padding: 7px 3px + > 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%) + +.oe_snippet + display: inline-block + vertical-align: top + width: 93px + margin-left: 3px + margin-right: 3px + margin-top: 1px + position: relative + overflow: hidden + +user-select(none) + cursor: move + .oe_snippet_thumbnail + text-align: center + height: 100% + background: transparent + 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 + width: 92px + background-size: cover + @include transform( scale(1,1)) + span, div + line-height: 18px + & > :not(.oe_snippet_thumbnail) + display: none !important + &.disable + .oe_snippet_thumbnail + background-color: rgba(255, 0, 0, 0.75) + .oe_snippet_thumbnail_img + opacity: 0.8 +#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_vertical_text + width: 48px + float: none + display: inline-block + 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: transparent + 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: "\f07e" + 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 + > div + float: left + .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:before + 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:before + 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 + + &.o_keypress + opacity: 0 + -webkit-transition: .8s + -moz-transition: .8s + -o-transition: .8s + -transition: .8s + + +.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 + +// }}} diff --git a/addons/web_editor/static/src/css/web_editor.css b/addons/web_editor/static/src/css/web_editor.css new file mode 100644 index 00000000000..e61e5244e40 --- /dev/null +++ b/addons/web_editor/static/src/css/web_editor.css @@ -0,0 +1,266 @@ +@charset "utf-8"; +/* ----- EDITOR ----- */ +.css_non_editable_mode_hidden { + display: none !important; +} + +/* ----- Editor (for public user, like forum) ----- */ +.note-toolbar { + margin-left: 0 !important; +} + +.note-popover .popover > .arrow { + display: none; +} + +/* ----- GENERIC LAYOUTING HELPERS ---- */ + +.hidden { + display: none !important; +} + +/* table */ +table.table { + table-layout: fixed; +} + +td { + min-width: 20px; +} + +/* Vertical Spacing */ +.mt128 { + margin-top: 128px !important; +} + +.mt92 { + margin-top: 92px !important; +} + +.mt64 { + margin-top: 64px !important; +} + +.mt48 { + margin-top: 48px !important; +} + +.mt32 { + margin-top: 32px !important; +} + +.mt16 { + margin-top: 16px !important; +} + +.mt8 { + margin-top: 8px !important; +} + +.mt4 { + margin-top: 4px !important; +} + +.mt0 { + margin-top: 0px !important; +} + +.mb128 { + margin-bottom: 128px !important; +} + +.mb92 { + margin-bottom: 92px !important; +} + +.mb64 { + margin-bottom: 64px !important; +} + +.mb48 { + margin-bottom: 48px !important; +} + +.mb32 { + margin-bottom: 32px !important; +} + +.mb16 { + margin-bottom: 16px !important; +} + +.mb8 { + margin-bottom: 8px !important; +} + +.mb4 { + margin-bottom: 4px !important; +} + +.mb0 { + margin-bottom: 0px !important; +} + +.ml8 { + margin-left: 8px !important; +} + +.ml4 { + margin-left: 4px !important; +} + +.ml0 { + margin-left: 0px !important; +} + +.mr8 { + margin-right: 8px !important; +} + +.mr4 { + margin-right: 4px !important; +} + +.mr0 { + margin-right: 0px !important; +} + +/* Media */ +img.shadow { + -moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); + -webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); + box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); +} + +img.padding-small, .img.padding-small, span.fa.padding-small, iframe.padding-small { + padding: 4px; +} +img.padding-medium, .img.padding-medium, span.fa.padding-medium, iframe.padding-medium { + padding: 8px; +} +img.padding-large, .img.padding-large, span.fa.padding-large, iframe.padding-large { + padding: 16px; +} +img.padding-xl, .img.padding-xl, span.fa.padding-xl, iframe.padding-xl { + padding: 32px; +} + +div.media_iframe_video.padding-small iframe { + padding: 4px; +} +div.media_iframe_video.padding-medium iframe { + padding: 8px; +} +div.media_iframe_video.padding-large iframe { + padding: 16px; +} +div.media_iframe_video.padding-xl iframe { + padding: 32px; +} + +/* font awsome */ +.fa-6x { + font-size: 6em; +} + +.fa-7x { + font-size: 7em; +} + +.fa-8x { + font-size: 8em; +} + +.fa-9x { + font-size: 9em; +} + +.fa-10x { + font-size: 10em; +} + +.fa.center-block { + text-align: center; +} + +/* video */ +div.media_iframe_video { + margin: 0 auto; + text-align: center; + position: relative; + overflow: hidden; +} +div.media_iframe_video iframe { + width: 100%; + height: 100%; + top: 0; + position: absolute; + margin: 0 auto; + margin-left: -50%; +} +div.media_iframe_video .media_iframe_video_size { + padding-bottom: 66.5%; + position: relative; + width: 100%; + height: 0; +} +div.media_iframe_video .css_editable_mode_display { + position: absolute; + width: 100%; + height: 100%; + display: none; + z-index: 2; +} + +html[data-browser^="msie"] div.media_iframe_video iframe { + margin-left: 0; +} + +/* Mobile view */ +@media (max-width: 768px) { + img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa { + -webkit-transform: none !important; + -moz-transform: none !important; + -ms-transform: none !important; + -o-transform: none !important; + transform: none !important; + } +} +/* ---- fields --- */ +address .fa.fa-mobile-phone { + margin: 0 3px 0 2px; +} +address .fa.fa-file-text-o { + margin-right: 1px; +} + +span[data-oe-type="monetary"] { + white-space: nowrap; +} + +/* ---- 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; +} diff --git a/addons/web_editor/static/src/css/web_editor.sass b/addons/web_editor/static/src/css/web_editor.sass new file mode 100644 index 00000000000..75b4ad6c781 --- /dev/null +++ b/addons/web_editor/static/src/css/web_editor.sass @@ -0,0 +1,195 @@ +@charset "utf-8" +@import "common" + +/* ----- EDITOR ----- */ + +.css_non_editable_mode_hidden + display: none !important + +/* ----- Editor (for public user, like forum) ----- */ + +.note-toolbar + margin-left: 0 !important + +.note-popover .popover > .arrow + display: none + +/* ----- GENERIC LAYOUTING HELPERS ---- */ + +.hidden + display: none !important + +/* table */ + +table.table + table-layout: fixed + +td + min-width: 20px + +/* Vertical Spacing */ + +.mt128 + margin-top: 128px !important +.mt92 + margin-top: 92px !important +.mt64 + margin-top: 64px !important +.mt48 + margin-top: 48px !important +.mt32 + margin-top: 32px !important +.mt16 + margin-top: 16px !important +.mt8 + margin-top: 8px !important +.mt4 + margin-top: 4px !important +.mt0 + margin-top: 0px !important + +.mb128 + margin-bottom: 128px !important +.mb92 + margin-bottom: 92px !important +.mb64 + margin-bottom: 64px !important +.mb48 + margin-bottom: 48px !important +.mb32 + margin-bottom: 32px !important +.mb16 + margin-bottom: 16px !important +.mb8 + margin-bottom: 8px !important +.mb4 + margin-bottom: 4px !important +.mb0 + margin-bottom: 0px !important + +.ml8 + margin-left: 8px !important +.ml4 + margin-left: 4px !important +.ml0 + margin-left: 0px !important + +.mr8 + margin-right: 8px !important +.mr4 + margin-right: 4px !important +.mr0 + margin-right: 0px !important + +/* Media */ + +img.shadow + +box-shadow(0px 3px 8px rgba(0, 0, 0, 0.2)) + +img, .img, span.fa, iframe + &.padding-small + padding: 4px + &.padding-medium + padding: 8px + &.padding-large + padding: 16px + &.padding-xl + padding: 32px +div.media_iframe_video + &.padding-small iframe + padding: 4px + &.padding-medium iframe + padding: 8px + &.padding-large iframe + padding: 16px + &.padding-xl iframe + padding: 32px + +/* font awsome */ + +.fa-6x + font-size: 6em +.fa-7x + font-size: 7em +.fa-8x + font-size: 8em +.fa-9x + font-size: 9em +.fa-10x + font-size: 10em +.fa.center-block + text-align: center + +/* video */ + +div.media_iframe_video + margin: 0 auto + text-align: center + position: relative + overflow: hidden + iframe + width: 100% + height: 100% + top: 0 + position: absolute + margin: 0 auto + margin-left: -50% + .media_iframe_video_size + padding-bottom: 66.5% + position: relative + width: 100% + height: 0 + .css_editable_mode_display + position: absolute + width: 100% + height: 100% + display: none + z-index: 2 + +html[data-browser^="msie"] div.media_iframe_video iframe + margin-left: 0 + +/* Mobile view */ + +@media (max-width: 768px) + img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa + -webkit-transform: none !important + -moz-transform: none !important + -ms-transform: none !important + -o-transform: none !important + transform: none !important + +/* ---- fields --- */ + +address + .fa.fa-mobile-phone + margin: 0 3px 0 2px + .fa.fa-file-text-o + margin-right: 1px + +span[data-oe-type="monetary"] + white-space: nowrap + +/* ---- 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 + +// }}} diff --git a/addons/website/static/src/img/drag_here.png b/addons/web_editor/static/src/img/drag_here.png Binary files differindex 335c3a571bb..335c3a571bb 100644 --- a/addons/website/static/src/img/drag_here.png +++ b/addons/web_editor/static/src/img/drag_here.png diff --git a/addons/website/static/src/img/edit_here.png b/addons/web_editor/static/src/img/edit_here.png Binary files differindex 77beff11c7b..77beff11c7b 100644 --- a/addons/website/static/src/img/edit_here.png +++ b/addons/web_editor/static/src/img/edit_here.png diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js new file mode 100644 index 00000000000..5725cfc39f6 --- /dev/null +++ b/addons/web_editor/static/src/js/backend.js @@ -0,0 +1,322 @@ +odoo.define('web_editor.backend', function (require) { +'use strict'; + +var core = require('web.core'); +var session = require('web.session'); +var Model = require('web.DataModel'); +var common = require('web.form_common'); +var base = require('web_editor.base'); +var editor = require('web_editor.editor'); +var summernote = require('web_editor.summernote'); +var transcoder = require('web_editor.transcoder'); + +var QWeb = core.qweb; +var _t = core._t; + +/** + * FieldTextHtml Widget + * Intended for FieldText widgets meant to display HTML content. This + * widget will instantiate an iframe with the editor summernote improved by odoo + */ + +var widget = common.AbstractField.extend(common.ReinitializeFieldMixin); + +var FieldTextHtmlSimple = widget.extend({ + template: 'web_editor.FieldTextHtmlSimple', + _config: function () { + var self = this; + return { + 'focus': false, + 'height': 180, + 'toolbar': [ + ['style', ['style']], + ['font', ['bold', 'italic', 'underline', 'clear']], + ['fontsize', ['fontsize']], + ['color', ['color']], + ['para', ['ul', 'ol', 'paragraph']], + ['table', ['table']], + ['insert', ['link', 'picture']], + ['history', ['undo', 'redo']] + ], + 'styleWithSpan': false, + 'inlinemedia': ['p'], + 'lang': "odoo", + 'onChange': function (value) { + self.internal_set_value(value); + self.trigger('changed_value'); + } + }; + }, + initialize_content: function() { + var self = this; + this.$textarea = this.$("textarea").val(this.get('value') || "<p><br/></p>"); + + if (this.get("effective_readonly")) { + this.$textarea.hide().after('<div class="note-editable"/>'); + } else { + this.$textarea.summernote(this._config()); + + if (this.field.translate && this.view) { + $(QWeb.render('web_editor.FieldTextHtml.button.translate', this)) + .appendTo(this.$('.note-toolbar')) + .on('click', this.on_translate); + } + + var reset = _.bind(this.reset_history, this); + this.view.on('load_record', this, reset); + setTimeout(reset, 0); + } + this.$content = this.$('.note-editable:first'); + + $(".oe-view-manager-content").on("scroll", function () { + $('.o_table_handler').remove(); + }); + this._super(); + }, + reset_history: function () { + var history = this.$content.data('NoteHistory'); + if (history) { + history.reset(); + self.$('.note-toolbar').find('button[data-event="undo"]').attr('disabled', true); + } + }, + text_to_html: function (text) { + var value = text || ""; + if (value.match(/^\s*$/)) { + value = '<p><br/></p>'; + } else { + value = "<p>"+value.split(/<br\/?>/).join("</p><p>")+"</p>"; + value = value.replace('<p><p>', '<p>').replace('</p></p>', '</p>'); + } + return value; + }, + render_value: function() { + var value = this.get('value'); + this.$textarea.val(value || ''); + this.$content.html(this.text_to_html(value)); + this.$content.focusInEnd(); + var history = this.$content.data('NoteHistory'); + if (history && history.recordUndo()) { + this.$('.note-toolbar').find('button[data-event="undo"]').attr('disabled', false); + } + }, + is_false: function() { + return !this.get('value') || this.get('value') === "<p><br/></p>" || !this.get('value').match(/\S/); + }, + before_save: function() { + if (this.options['style-inline']) { + transcoder.class_to_style(this.$content); + transcoder.font_to_img(this.$content); + this.internal_set_value(this.$content.html()); + } + }, + destroy_content: function () { + $(".oe-view-manager-content").off("scroll"); + this.$textarea.destroy(); + this._super(); + } +}); + +var FieldTextHtml = widget.extend({ + template: 'web_editor.FieldTextHtml', + willStart: function () { + var self = this; + return new Model('res.lang').call("search_read", [[['code', '!=', 'en_US']], ["name", "code"]]).then(function (res) { + self.languages = res; + }); + }, + start: function () { + var self = this; + + this.callback = _.uniqueId('FieldTextHtml_'); + window.odoo[this.callback+"_editor"] = function (EditorBar) { + setTimeout(function () { + self.on_editor_loaded(EditorBar); + },0); + }; + window.odoo[this.callback+"_content"] = function (EditorBar) { + self.on_content_loaded(); + }; + window.odoo[this.callback+"_updown"] = null; + window.odoo[this.callback+"_downup"] = function (value) { + self.internal_set_value(value); + self.trigger('changed_value'); + self.resize(); + }; + + // init jqery objects + this.$iframe = this.$el.find('iframe'); + this.document = null; + this.$body = $(); + this.$content = $(); + + this.$iframe.css('min-height', 'calc(100vh - 360px)'); + + // init resize + this.resize = function resize() { + if (self.get('effective_readonly')) { return; } + if ($("body").hasClass("o_form_FieldTextHtml_fullscreen")) { + self.$iframe.css('height', (document.body.clientHeight - self.$iframe.offset().top) + 'px'); + } else { + self.$iframe.css("height", (self.$body.find("#oe_snippets").length ? 500 : 300) + "px"); + } + }; + $(window).on('resize', self.resize); + + return this._super(); + }, + get_url: function (_attr) { + var src = this.options.editor_url ? this.options.editor_url+"?" : "/web_editor/field/html?"; + var datarecord = this.view.get_fields_values(); + + var attr = { + 'model': this.view.model, + 'field': this.name, + 'res_id': datarecord.id || '', + 'callback': this.callback + }; + _attr = _attr || {}; + + if (this.options['style-inline']) { + attr.inline_mode = 1; + } + if (this.options.snippets) { + attr.snippets = this.options.snippets; + } + if (!this.get("effective_readonly")) { + attr.enable_editor = 1; + } + if (this.field.translate) { + attr.translatable = 1; + } + if (session.debug) { + attr.debug = 1; + } + + attr.lang = attr.enable_editor ? 'en_US' : this.session.user_context.lang; + + for (var k in _attr) { + attr[k] = _attr[k]; + } + + for (var k in attr) { + if (attr[k] !== null) { + src += "&"+k+"="+(_.isBoolean(attr[k]) ? +attr[k] : attr[k]); + } + } + + delete datarecord[this.name]; + src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord)); + + return src; + }, + initialize_content: function() { + this.$el.closest('.modal-body').css('max-height', 'none'); + this.$iframe = this.$el.find('iframe'); + this.document = null; + this.$body = $(); + this.$content = $(); + this.editor = false; + window.odoo[this.callback+"_updown"] = null; + this.$iframe.attr("src", this.get_url()); + }, + on_content_loaded: function () { + var self = this; + this.document = this.$iframe.contents()[0]; + this.$body = $("body", this.document); + this.$content = this.$body.find("#editable_area"); + this.lang = this.$iframe.attr('src').match(/[?&]lang=([^&]+)/); + this.lang = this.lang ? this.lang[1] : this.view.dataset.context.lang; + this._dirty_flag = false; + this.render_value(); + setTimeout(function () { + self.add_button(); + setTimeout(self.resize,0); + }, 0); + }, + on_editor_loaded: function (EditorBar) { + var self = this; + this.editor = EditorBar; + if (this.get('value') && window.odoo[self.callback+"_updown"] && !(this.$content.html()||"").length) { + this.render_value(); + } + setTimeout(function () { + setTimeout(self.resize,0); + }, 0); + }, + add_button: function () { + var self = this; + var $to = this.$body.find("#web_editor-top-edit, #wrapwrap").first(); + + $(QWeb.render('web_editor.FieldTextHtml.translate', {'widget': this})) + .appendTo($to) + .on('change', 'select', function () { + var lang = $(this).val(); + var edit = !self.get("effective_readonly"); + var trans = lang !== 'en_US'; + self.$iframe.attr("src", self.get_url({ + 'edit_translations': edit && trans, + 'enable_editor': edit && !trans, + 'lang': lang + })); + }); + + $(QWeb.render('web_editor.FieldTextHtml.fullscreen')) + .appendTo($to) + .on('click', '.o_fullscreen', function () { + $("body").toggleClass("o_form_FieldTextHtml_fullscreen"); + var full = $("body").hasClass("o_form_FieldTextHtml_fullscreen"); + self.$iframe.parents().toggleClass('o_form_fullscreen_ancestor', full); + self.resize(); + }); + + this.$body.on('click', '[data-action="cancel"]', function (event) { + event.preventDefault(); + self.initialize_content(); + }); + }, + render_value: function() { + if (this.lang !== this.view.dataset.context.lang || this.$iframe.attr('src').match(/[?&]edit_translations=1/)) { + return; + } + var value = (this.get('value') || "").replace(/^<p[^>]*>(\s*|<br\/?>)<\/p>$/, ''); + if (!this.$content) { + return; + } + if (!this.get("effective_readonly")) { + if(window.odoo[this.callback+"_updown"]) { + window.odoo[this.callback+"_updown"](value, this.view.get_fields_values(), this.name); + this.resize(); + } + } else { + this.$content.html(value); + this.$iframe.css("height", (this.$body.height()+20) + "px"); + } + }, + is_false: function() { + return this.get('value') === false || !this.$content.html() || !this.$content.html().match(/\S/); + }, + before_save: function () { + if (this.lang !== 'en_US' && this.$body.find('.o_dirty').length) { + this.internal_set_value( this.view.datarecord[this.name] ); + this._dirty_flag = false; + return this.editor.save(); + } else if (this._dirty_flag && this.editor && this.editor.buildingBlock) { + this.editor.buildingBlock.clean_for_save(); + this.internal_set_value( this.$content.html() ); + } + }, + destroy: function () { + $(window).off('resize', self.resize); + delete window.odoo[this.callback+"_editor"]; + delete window.odoo[this.callback+"_content"]; + delete window.odoo[this.callback+"_updown"]; + delete window.odoo[this.callback+"_downup"]; + } +}); + +core.form_widget_registry + .add('html', FieldTextHtmlSimple) + .add('html_frame', FieldTextHtml); + +}); diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/base.js new file mode 100644 index 00000000000..28b35722edd --- /dev/null +++ b/addons/web_editor/static/src/js/base.js @@ -0,0 +1,85 @@ +odoo.define('web_editor.base', function (require) { +"use strict"; + +var core = require('web.core'); +var ajax = require('web.ajax'); +var qweb = core.qweb; +var _t = core._t; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +var get_context = function (dict) { + var html = document.documentElement; + return _.extend({ + 'lang': (html.getAttribute('lang') || '').replace('-', '_'), + }, dict); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* ---------------------------------------------------- + Async Ready and Template loading + ---------------------------------------------------- */ +var dom_ready = $.Deferred(); +$(document).ready(function () { + dom_ready.resolve(); + // fix for ie + if($.fn.placeholder) $('input, textarea').placeholder(); +}); + + +// todo: remove and load a bundle of translated templates +var all_ready; +var ready = function() { + if (!all_ready) { + all_ready = $.when(dom_ready, ajax.loadXML()).then(translations); + } else if(all_ready.state() === "resolved") { // can add async template + all_ready = $.when(dom_ready, ajax.loadXML()); + } + return all_ready; +}; + +function translations() { + function translate_node(node){ + if(node.nodeType === 3) { // TEXT_NODE + if(node.nodeValue.match(/\S/)){ + var space = node.nodeValue.match(/^([\s]*)([\s\S]*?)([\s]*)$/); + node.nodeValue = space[1] + $.trim(_t(space[2])) + space[3]; + } + } + else if(node.nodeType === 1 && node.hasChildNodes()) { // ELEMENT_NODE + _.each(node.childNodes, translate_node); + } + } + return ajax.jsonRpc(data.url_translations, 'call', { + 'mods': ['web_editor'], + 'lang': data.get_context().lang + }) + .then(function(trans) { + _t.database.set_bundle(trans); + }).then(function () { + var keys = _.keys(qweb.templates); + for (var i = 0; i < keys.length; i++){ + translate_node(qweb.templates[keys[i]]); + } + }).promise(); +} + +var reload = function () { + location.hash = "scrollTop=" + window.document.body.scrollTop; + if (location.search.indexOf("enable_editor") > -1) { + window.location.href = window.location.href.replace(/enable_editor(=[^&]*)?/g, ''); + } else { + window.location.reload(); + } +}; + +var data = { + 'get_context': get_context, + 'url_translations': '/web/webclient/translations', + 'dom_ready': dom_ready, + 'ready': ready, + 'reload': reload +}; +return data; + +}); diff --git a/addons/web_editor/static/src/js/editor.js b/addons/web_editor/static/src/js/editor.js new file mode 100644 index 00000000000..b09a69e3818 --- /dev/null +++ b/addons/web_editor/static/src/js/editor.js @@ -0,0 +1,164 @@ +odoo.define('web_editor.editor', function (require) { +"use strict"; + +var ajax = require('web.ajax'); +var Widget = require('web.Widget'); +var core = require('web.core'); +var base = require('web_editor.base'); +var rte = require('web_editor.rte'); + +var qweb = core.qweb; +var _t = core._t; +var editor = {}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +editor.dummy = function () {return true;}; // used for snippets, options... + +editor.editable = !!$('html').data('editable'); + +ajax.loadXML('/web_editor/static/src/xml/editor.xml', qweb); + +base.dom_ready.done(function () { + $(document).on('click', '.note-editable', function (ev) { + ev.preventDefault(); + }); + + $(document).on('submit', '.note-editable form .btn', function (ev) { + // Disable form submition in editable mode + ev.preventDefault(); + }); + + $(document).on('hide.bs.dropdown', '.dropdown', function (ev) { + // Prevent dropdown closing when a contenteditable children is focused + if (ev.originalEvent + && $(ev.target).has(ev.originalEvent.target).length + && $(ev.originalEvent.target).is('[contenteditable]')) { + ev.preventDefault(); + } + }); +}); + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +editor.reload = function () { + location.hash = "scrollTop=" + window.document.body.scrollTop; + if (location.search.indexOf("enable_editor") > -1) { + window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, ''); + } else { + window.location.reload(); + } +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/* ----- TOP EDITOR BAR FOR ADMIN ---- */ + +base.ready().then(function () { + if (editor.editable && location.search.indexOf("enable_editor") >= 0 && !editor.no_editor) { + editor.editor_bar = new editor.Class(); + editor.editor_bar.prependTo(document.body); + } +}); + +editor.Class = Widget.extend({ + template: 'web_editor.editorbar', + events: { + 'click button[data-action=save]': 'save', + 'click a[data-action=cancel]': 'cancel', + }, + init: function(parent) { + var self = this; + var res = this._super.apply(this, arguments); + this.parent = parent; + this.rte = new rte.Class(this); + this.rte.on('rte:start', this, function () { + self.trigger('rte:start'); + }); + return res; + }, + start: function() { + var self = this; + + this.$('button[data-action=save]').prop('disabled', true); + $('.dropdown-toggle').dropdown(); + + this.display_placeholder(); + + this.rte.on('change', this, this.proxy('rte_changed')); + this.rte.start(); + + var flag = false; + window.onbeforeunload = function(event) { + if ($('.o_editable.o_dirty').length && !flag) { + flag = true; + setTimeout(function () {flag=false;},0); + return _t('This document is not saved!'); + } + }; + return this._super(); + }, + display_placeholder: function () { + var $area = $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]") + .addClass("oe_empty") + .attr("data-oe-placeholder", _t("Press The Top-Left Edit Button")); + + this.on('rte:start', this, function () { + $area.attr("data-oe-placeholder", _t("Write Your Text Here")); + }); + this.on("snippets:ready", this, function () { + if ($("body").hasClass("editor_has_snippets")) { + $area.attr("data-oe-placeholder", _t("Write Your Text or Drag a Block Here")); + } + }); + + $(document).on("keyup", function (event) { + if((event.keyCode === 8 || event.keyCode === 46)) { + var $target = $(event.target).closest(".o_editable"); + if(!$target.is(":has(*:not(p):not(br))") && !$target.text().match(/\S/)) { + $target.empty(); + } + } + }); + }, + rte_changed: function () { + // todo: use !rte.history.getEditableHasUndo().length (but phantomjs unknow error) + this.$('button[data-action=save]').prop('disabled', !$('.o_dirty').length); + }, + save: function () { + return this.rte.save().then(function () { + editor.reload(); + }); + }, + /** + * Saves an RTE content, which always corresponds to a view section (?). + */ + save_without_reload: function () { + return this.rte.save(); + }, + cancel: function () { + new $.Deferred(function (d) { + if (!rte.history.getEditableHasUndo().length) { + return d.resolve(); + } + var $dialog = $(qweb.render('web_editor.discard')).appendTo(document.body); + $dialog.on('click', '.btn-danger', function () { + d.resolve(); + }).on('hidden.bs.modal', function () { + d.reject(); + }); + d.always(function () { + $dialog.remove(); + }); + $dialog.modal('show'); + }).then(function () { + window.onbeforeunload = null; + editor.reload(); + }); + }, +}); + +return editor; + +}); + diff --git a/addons/web_editor/static/src/js/iframe.js b/addons/web_editor/static/src/js/iframe.js new file mode 100644 index 00000000000..8b287064b7c --- /dev/null +++ b/addons/web_editor/static/src/js/iframe.js @@ -0,0 +1,78 @@ +odoo.define('web_editor.iframe', function (require) { +'use strict'; + +var editor = require('web_editor.editor'); +var translator = require('web_editor.translate'); +var rte = require('web_editor.rte'); +var snippet_editor = require('web_editor.snippet.editor'); + +window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name) { + var $editable = $("#editable_area"); + if(value !== $editable.prop("innerHTML")) { + if ($('body').hasClass('editor_enable')) { + if (value !== fields_values[field_name]) { + rte.history.recordUndo($editable); + } + snippet_editor.instance.make_active(false); + } + + $editable.html(value); + + if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) { + $editable.trigger("content_changed"); + } + } +}; + +editor.Class.include({ + start: function () { + this.on('rte:start', this, function () { + this.$('form').hide(); + + if (window.top.odoo[callback+"_editor"]) { + window.top.odoo[callback+"_editor"](this); + } + + var $editable = $("#editable_area"); + setTimeout(function () { + $($editable.find("*").filter(function () {return !this.children.length;}).first()[0] || $editable) + .focusIn().trigger("mousedown").trigger("keyup"); + },0); + + $editable.on('content_changed', this, function () { + if (window.top.odoo[callback+"_downup"]) { + window.top.odoo[callback+"_downup"]($editable.prop('innerHTML')); + } + }); + }); + + this.on("snippets:ready", this, function () { + $(window.top).trigger("resize"); + }); + return this._super(); + } +}); + +snippet_editor.Class.include({ + _get_snippet_url: function () { + return snippets_url; + } +}); + +translator.Class.include({ + start: function () { + var self = this; + return this._super().then(function () { + $('button[data-action=save]').hide(); + if (window.top.odoo[callback+"_editor"]) { + window.top.odoo[callback+"_editor"](self); + } + }); + } +}); + +}); + +$(function () { + odoo.init(); +}); diff --git a/addons/web_editor/static/src/js/inline.js b/addons/web_editor/static/src/js/inline.js new file mode 100644 index 00000000000..c5fd2cfd924 --- /dev/null +++ b/addons/web_editor/static/src/js/inline.js @@ -0,0 +1,77 @@ +odoo.define('web_editor.inline', function (require) { +'use strict'; + +var editor = require('web_editor.editor'); +var rte = require('web_editor.rte'); +var widget = require('web_editor.widget'); +var transcoder = require('web_editor.transcoder'); +var snippet_editor = require('web_editor.snippet.editor'); + +widget.MediaDialog.include({ + start: function () { + this._super(); + this.$('[href="#editor-media-video"]').addClass('hidden'); + } +}); + +editor.Class.include({ + start: function () { + var self = this; + if (location.search.indexOf("enable_editor") !== -1) { + this.on('rte:start', this, function () { + // move the caret at the end of the text when click after all content + $("#wrapwrap").on('click', function (event) { + if ($(event.target).is("#wrapwrap") || $(event.target).is("#editable_area:empty")) { + setTimeout(function () { + var node = $("#editable_area *") + .filter(function () { return this.textContent.match(/\S|\u00A0/); }) + .add($("#editable_area")) + .last()[0]; + $.summernote.core.range.create(node, $.summernote.core.dom.nodeLength(node)).select(); + },0); + } + }); + }); + } + return this._super.apply(this, arguments); + }, +}); + +snippet_editor.Class.include({ + start: function () { + var self = this; + this._super(); + setTimeout(function () { + var $editable = $("#editable_area"); + transcoder.img_to_font($editable); + transcoder.style_to_class($editable); + }); + }, + clean_for_save: function () { + this._super(); + var $editable = $("#editable_area"); + transcoder.class_to_style($editable); + transcoder.font_to_img($editable); + }, + +}); + +window.top.odoo[callback+"_updown"] = function (value, fields_values) { + var $editable = $("#editable_area"); + value = value || ""; + if (value.indexOf('on_change_model_and_list') === -1 && value !== $editable.html()) { + rte.history.recordUndo($editable, null, true); + if (snippet_editor.instance) { + snippet_editor.instance.make_active(false); + } + + $editable.html(value); + + transcoder.img_to_font($editable); + transcoder.style_to_class($editable); + } else { + $editable.trigger("content_changed"); + } +}; + +});
\ No newline at end of file diff --git a/addons/web_editor/static/src/js/rte.js b/addons/web_editor/static/src/js/rte.js new file mode 100644 index 00000000000..c8d5655cbd6 --- /dev/null +++ b/addons/web_editor/static/src/js/rte.js @@ -0,0 +1,597 @@ +odoo.define('web_editor.rte', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var utils = require('web.utils'); +var Widget = require('web.Widget'); +var summernote = require('web_editor.summernote'); +var base = require('web_editor.base'); + +var _t = core._t; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* Summernote Lib (neek change to make accessible: method and object) */ + +var dom = summernote.core.dom; +var range = summernote.core.range; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* Change History to have a global History for all summernote instances */ + +var History = function History ($editable) { + var aUndo = []; + var pos = 0; + + this.makeSnap = function (event, rng) { + rng = rng || range.create(); + var elEditable = $(rng.sc).closest('.o_editable')[0]; + if (!elEditable) { + return false; + } + return { + event: event, + editable: elEditable, + contents: elEditable.innerHTML, + bookmark: rng && rng.bookmark(elEditable), + scrollTop: $(elEditable).scrollTop() + }; + }; + + this.applySnap = function (oSnap) { + var $editable = $(oSnap.editable); + + if (!!document.documentMode) { + $editable.removeAttr("contentEditable").removeProp("contentEditable"); + } + + $editable.html(oSnap.contents).scrollTop(oSnap.scrollTop); + $(".oe_overlay").remove(); + $(".note-control-selection").hide(); + + $editable.trigger("content_changed"); + + if (!oSnap.bookmark || oSnap.event === "blur") { + return; + } + + try { + var r = oSnap.editable.innerHTML === "" ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark); + r.select(); + } catch(e) { + console.error(e); + return; + } + + $(document).trigger("click"); + $(".o_editable *").filter(function () { + var $el = $(this); + if($el.data('snippet-editor')) { + $el.removeData(); + } + }); + + + setTimeout(function () { + var target = dom.isBR(r.sc) ? r.sc.parentNode : dom.node(r.sc); + if (!target) { + return; + } + + $editable.trigger("applySnap"); + + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target); + target.dispatchEvent(evt); + + $editable.trigger("keyup"); + },0); + }; + + this.undo = function () { + if (!pos) { return; } + var _toSnap = toSnap; + if (_toSnap) { + this.saveSnap(); + } + if (!aUndo[pos] && (!aUndo[pos] || aUndo[pos].event !== 'undo')) { + var temp = this.makeSnap('undo'); + if (temp && (!pos || temp.contents !== aUndo[pos-1].contents)) { + aUndo[pos] = temp; + } else { + pos--; + } + } else if (_toSnap) { + pos--; + } + this.applySnap(aUndo[Math.max(--pos,0)]); + while (pos && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate")) { + this.applySnap(aUndo[--pos]); + } + }; + this.hasUndo = function () { + return (toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo")) || + !!_.find(aUndo.slice(0, pos+1), function (undo) { + return undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo"; + }); + }; + + this.getEditableHasUndo = function () { + var editable = []; + if ((toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo"))) { + editable.push(toSnap.editable); + } + _.each(aUndo.slice(0, pos+1), function (undo) { + if (undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo") { + editable.push(undo.editable); + } + }); + return _.uniq(editable); + }; + + this.redo = function () { + if (!aUndo[pos+1]) { return; } + this.applySnap(aUndo[++pos]); + while (aUndo[pos+1] && (aUndo[pos].event === "blur" || aUndo[pos].event === "activate" || aUndo[pos].event === "undo")) { + this.applySnap(aUndo[Math.max(++pos,aUndo.length-1)]); + } + }; + this.hasRedo = function () { + return aUndo.length > pos+1; + }; + + var toSnap, split; + this.recordUndo = function ($editable, event, internal_history) { + var self = this; + if (!$editable) { + $editable = $(range.create().sc).closest(".o_editable"); + } + + if (aUndo[pos] && (event === "applySnap" || event === "activate")) { + return; + } + + if (!internal_history) { + if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === "activate") { // don't trigger change for all keypress + setTimeout(function () { + $editable.trigger("content_changed"); + },0); + } + } + + if (aUndo[pos]) { + pos = Math.min(pos, aUndo.length); + aUndo.splice(Math.max(pos,1), aUndo.length); + } + + // => make a snap when the user change editable zone (because: don't make snap for each keydown) + if (toSnap && (toSnap.split || !event || toSnap.event !== event || toSnap.editable !== $editable[0])) { + this.saveSnap(); + } + + if (pos && aUndo[pos-1].editable !== $editable[0]) { + var snap = this.makeSnap('blur', range.create(aUndo[pos-1].editable, 0)); + pos++; + aUndo.push(snap); + } + + if (range.create()) { + toSnap = self.makeSnap(event); + } else { + toSnap = false; + } + }; + + this.splitNext = function () { + if (toSnap) { + toSnap.split = true; + } + }; + + this.saveSnap = function () { + if (toSnap) { + if (!aUndo[pos]) { + pos++; + } + aUndo.push(toSnap); + delete toSnap.split; + toSnap = null; + } + }; +}; +var history = new History(); + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +// add focusIn to jQuery to allow to move caret into a div of a contentEditable area + +$.extend($.expr[':'],{ + o_editable: function(node,i,m){ + while (node) { + if (node.className) { + if (node.className.indexOf('o_not_editable')!==-1 ) { + return false; + } + if (node.className.indexOf('o_editable')!==-1 ) { + return true; + } + } + node = node.parentNode; + } + return false; + } +}); + +$.fn.extend({ + focusIn: function () { + if (this.length) { + range.create(dom.firstChild(this[0]), 0).select(); + } + return this; + }, + focusInEnd: function () { + if (this.length) { + var last = dom.lastChild(this[0]); + range.create(last, dom.nodeLength(last)).select(); + } + return this; + }, + selectContent: function () { + if (this.length) { + var next = dom.lastChild(this[0]); + range.create(dom.firstChild(this[0]), 0, next, next.textContent.length).select(); + } + return this; + } +}); + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/* ----- RICH TEXT EDITOR ---- */ + +var RTE = Widget.extend({ + init: function (EditorBar, config) { + var self = this; + this.EditorBar = EditorBar; + data.rte = this; + this._super.apply(this, arguments); + + this.init_bootstrap_carousel = $.fn.carousel; + this.edit_bootstrap_carousel = function () { + var res = self.init_bootstrap_carousel.apply(this, arguments); + // off bootstrap keydown event to remove event.preventDefault() + // and allow to change cursor position + $(this).off('keydown.bs.carousel'); + return res; + }; + + if (config) { + this.config = config; + } + }, + /** + * Add a record undo to history + * @param {DOM} target where the dom is changed is editable zone + */ + historyRecordUndo: function ($target, event, internal_history) { + var rng = range.create(); + var $editable = $(rng && rng.sc).closest(".o_editable"); + if (!rng || !$editable.length) { + $editable = $($target).closest(".o_editable"); + rng = range.create($target.closest("*")[0],0); + } else { + rng = $editable.data('range') || rng; + } + rng.select(); + history.recordUndo($editable, event, internal_history); + }, + /** + * Makes the page editable + * + * @param {Boolean} [restart=false] in case the edition was already set + * up once and is being re-enabled. + * @returns {$.Deferred} deferred indicating when the RTE is ready + */ + start: function () { + var self = this; + + this.saving_mutex = new utils.Mutex(); + + $.fn.carousel = this.edit_bootstrap_carousel; + + $(document).on('keydown', this, this.onKeydown); + $(document).on('mousedown activate', this, this.onMousedown); + $(document).on('mouseup', this, this.onMouseup); + + $('.o_not_editable').attr("contentEditable", false); + + var $editable = this.editable(); + + $editable.addClass('o_editable').data('rte', self); + + $editable.each(function () { + var $node = $(this); + + // add class to display inline-block for empty t-field + if(window.getComputedStyle(this).display === "inline" && $node.data('oe-type') !== "image") { + $node.addClass('o_is_inline_editable'); + } + + // start element observation + $node.one('content_changed', function () { + $(this).addClass('o_dirty'); + }); + + $node.data('initInnerHTML', $node.html()); + }); + + $(document).on('content_changed', '.o_editable', function () { + self.trigger('change', this); + }); + + $('#wrapwrap, .o_editable').on('click', '*', this, this.onClick); + + $('body').addClass("editor_enable"); + + $(document) + .tooltip({ + selector: '[data-oe-readonly]', + container: 'body', + trigger: 'hover', + delay: { "show": 1000, "hide": 100 }, + placement: 'bottom', + title: _t("Readonly field") + }) + .on('click', function () { + $(this).tooltip('hide'); + }); + + $(document).trigger('mousedown'); + self.trigger('rte:start'); + }, + + save: function (context) { + var self = this; + + this.__saved = {}; // list of allready saved views and data + + var editables = history.getEditableHasUndo(); + + var defs = $('.o_dirty') + .removeAttr('contentEditable') + .removeClass('o_dirty oe_carlos_danger o_is_inline_editable') + .map(function () { + var $el = $(this); + + $el.find('[class]').filter(function () { + if (!this.className.match(/\S/)) { + this.removeAttribute("class"); + } + }); + + // TODO: Add a queue with concurrency limit in webclient + // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js + return self.saving_mutex.exec(function () { + return self.saveElement($el, context) + .then(undefined, function (thing, response) { + // because ckeditor regenerates all the dom, + // we can't just setup the popover here as + // everything will be destroyed by the DOM + // regeneration. Add markings instead, and + // returns a new rejection with all relevant + // info + var id = _.uniqueId('carlos_danger_'); + $el.addClass('o_dirty oe_carlos_danger ' + id); + return $.Deferred().reject({ + id: id, + error: response.data, + }); + }); + }); + }).get(); + + return $.when.apply(null, defs).then(function () { + window.onbeforeunload = null; + }, function (failed) { + // If there were errors, re-enable edition + self.stop(); + self.start(); + // jquery's deferred being a pain in the ass + if (!_.isArray(failed)) { failed = [failed]; } + + _(failed).each(function (failure) { + var html = failure.error.exception_type === "except_osv"; + if (html) { + var msg = $("<div/>").text(failure.error.message).html(); + var data = msg.substring(3,msg.length-2).split(/', u'/); + failure.error.message = '<b>' + data[0] + '</b>' + data[1]; + } + $('.o_editable.' + failure.id) + .removeClass(failure.id) + .popover({ + html: html, + trigger: 'hover', + content: failure.error.message, + placement: 'auto top', + }) + // Force-show popovers so users will notice them. + .popover('show'); + }); + }); + }, + + saveElement: function ($el, context) { + // remove multi edition + if ($el.data('oe-model')) { + var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression'); + if (this.__saved[key]) return true; + this.__saved[key] = true; + } + + return ajax.jsonRpc('/web/dataset/call', 'call', { + model: 'ir.ui.view', + method: 'save', + args: [ + $el.data('oe-id'), + $el.prop('outerHTML'), + $el.data('oe-xpath') || null, + context || base.get_context() + ], + }); + }, + + stop: function () { + if (this.$last) { + this.$last.destroy(); + this.$last = null; + } + + $.fn.carousel = this.init_bootstrap_carousel; + + $(document).off('keydown', this.onKeydown); + $(document).off('mousedown applySnap', this.onMousedown); + $(document).off('mouseup', this.onMouseup); + $('.o_not_editable').removeAttr("contentEditable"); + $(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte'); + $('#wrapwrap, .o_editable').off('click', this.onClick); + $(document).tooltip('destroy'); + $('body').removeClass("editor_enable"); + this.trigger('rte:stop'); + }, + + cancel: function () { + $('.o_editable').each(function () { + var $node = $(this); + $node.data('initInnerHTML', $node.html()); + }); + this.stop(); + }, + + onClick: function (event) { + event.preventDefault(); + }, + + // handler for cancel editor + onKeydown: function (event) { + if (event.keyCode === 27 && !$('.modal-content:visible').length) { + setTimeout(function () { + $('#editor-top-navbar [data-action="cancel"]').click(); + var $modal = $('.modal-content > .modal-body').parents(".modal:first"); + $modal.off('keyup.dismiss.bs.modal'); + setTimeout(function () { + $modal.on('keyup.dismiss.bs.modal', function () { + $(this).modal('hide'); + }); + },500); + },0); + } + }, + + // activate editor + onMousedown: function (event) { + var $target = $(event.target); + var $editable = $target.closest('.o_editable'); + + if (!$editable.size()) { + return; + } + + var rte = $editable.data('rte') || event.data; + + if (rte && rte.$last && (!$editable.size() || rte.$last[0] != $editable[0])) { + var $destroy = rte.$last; + history.splitNext(); + + setTimeout(function () { + var id = $destroy.data('note-id'); + $destroy.destroy().removeData('note-id').removeAttr('data-note-id'); + $('#note-popover-'+id+', #note-handle-'+id+', #note-dialog-'+id+'').remove(); + },150); // setTimeout to remove flickering when change to editable zone (re-create an editor) + rte.$last = null; + } + if ($editable.size() && (!rte.$last || rte.$last[0] != $editable[0]) && + ($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') { + + $editable.summernote(rte.config($editable)); + + $editable.data('NoteHistory', history); + rte.$last = $editable; + + // firefox & IE fix + try { + document.execCommand('enableObjectResizing', false, false); + document.execCommand('enableInlineTableEditing', false, false); + document.execCommand( '2D-position', false, false); + } catch (e) {} + document.body.addEventListener('resizestart', function (evt) {evt.preventDefault(); return false;}); + document.body.addEventListener('movestart', function (evt) {evt.preventDefault(); return false;}); + document.body.addEventListener('dragstart', function (evt) {evt.preventDefault(); return false;}); + + if (!range.create()) { + $editable.focusIn(); + } + + if (dom.isImg($target[0])) { + $target.trigger('mousedown'); // for activate selection on picture + } + } + }, + + onMouseup: function (event) { + var $target = $(event.target); + var $editable = $target.closest('.o_editable'); + + if (!$editable.size()) { + return; + } + + var rte = event.data; + setTimeout(function () { + rte.historyRecordUndo($target, 'activate', true); + },0); + }, + + editable: function () { + return $('#wrapwrap [data-oe-model]') + .not('.o_not_editable') + .filter(function () { + return !$(this).closest('.o_not_editable').length; + }) + .not('link, script') + .not('[data-oe-readonly]') + .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]') + .not('.oe_snippet_editor') + .add('.o_editable'); + }, + + config: function ($editable) { + return { + 'airMode' : true, + 'focus': false, + 'airPopover': [ + ['style', ['style']], + ['font', ['bold', 'italic', 'underline', 'clear']], + ['fontsize', ['fontsize']], + ['color', ['color']], + ['para', ['ul', 'ol', 'paragraph']], + ['table', ['table']], + ['insert', ['link', 'picture']], + ['history', ['undo', 'redo']], + ], + 'styleWithSpan': false, + 'inlinemedia' : ['p'], + 'lang': "odoo", + 'onChange': function (html, $editable) { + $editable.trigger("content_changed"); + } + }; + }, +}); + + +var data = { + 'history': history, + 'Class': RTE +}; +return data; + +}); diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js new file mode 100644 index 00000000000..1086f4b369c --- /dev/null +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -0,0 +1,781 @@ +odoo.define('web_editor.rte.summernote', function (require) { +'use strict'; + +var core = require('web.core'); +var ajax = require('web.ajax'); +var base = require('web_editor.base'); +var widgets = require('web_editor.widget'); +var rte = require('web_editor.rte'); + +var QWeb = core.qweb; +var _t = core._t; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +ajax.jsonRpc('/web/dataset/call', 'call', { + 'model': 'ir.ui.view', + 'method': 'read_template', + 'args': ['web_editor.colorpicker', base.get_context()] +}).done(function (data) { + QWeb.add_template(data); +}); + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* Summernote Lib (neek change to make accessible: method and object) */ + +var dom = $.summernote.core.dom; +var range = $.summernote.core.range; +var eventHandler = $.summernote.eventHandler; +var renderer = $.summernote.renderer; +var options = $.summernote.options; + +var tplButton = renderer.getTemplate().button; +var tplIconButton = renderer.getTemplate().iconButton; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* update and change the popovers content, and add history button */ + +var fn_createPalette = renderer.createPalette; +renderer.createPalette = function ($container, options) { + fn_createPalette.call(this, $container, options); + + if (!QWeb.has_template('web_editor.colorpicker')) { + return; + } + + var $color = $container.find('.note-color'); + var html = QWeb.render('web_editor.colorpicker'); + $color.find('.note-color-palette').prepend(html); + var $bg = $color.find('.colorpicker:first button'); + var $fore = $color.find('.colorpicker:last button'); + + $bg.each(function () { $(this).attr('data-event', 'backColor').attr('data-value', $(this).attr('class')); }); + $fore.each(function () { $(this).attr('data-event', 'foreColor').attr('data-value', $(this).attr('class').replace(/bg-/, 'text-')); }); +}; + +var fn_tplPopovers = renderer.tplPopovers; +renderer.tplPopovers = function (lang, options) { + var $popover = $(fn_tplPopovers.call(this, lang, options)); + + var $imagePopover = $popover.find('.note-image-popover'); + var $linkPopover = $popover.find('.note-link-popover'); + var $airPopover = $popover.find('.note-air-popover'); + + if (window === window.top) { + $popover.children().addClass("hidden-xs"); + } + + //////////////// image popover + + // add center button for images + var $centerbutton = $(tplIconButton('fa fa-align-center', { + title: _t('Center'), + event: 'floatMe', + value: 'center' + })).insertAfter($imagePopover.find('[data-event="floatMe"][data-value="left"]')); + $imagePopover.find('button[data-event="removeMedia"]').parent().remove(); + $imagePopover.find('button[data-event="floatMe"][data-value="none"]').remove(); + + var $alt = $('<div class="btn-group"/>'); + $alt.prependTo($imagePopover.find('.popover-content')); + $alt.append('<button class="btn btn-default btn-sm btn-small" data-event="alt"><strong>Alt: </strong><span class="o_image_alt"></span></button>'); + + // padding button + var $padding = $('<div class="btn-group"/>'); + $padding.insertBefore($imagePopover.find('.btn-group:first')); + var $button = $(tplIconButton('fa fa-plus-square-o', { + title: _t('Padding'), + dropdown: true + })).appendTo($padding); + var $ul = $('<ul class="dropdown-menu"/>').insertAfter($button); + $ul.append('<li><a data-event="padding" href="#" data-value="">'+_t('None')+'</a></li>'); + $ul.append('<li><a data-event="padding" href="#" data-value="small">'+_t('Small')+'</a></li>'); + $ul.append('<li><a data-event="padding" href="#" data-value="medium">'+_t('Medium')+'</a></li>'); + $ul.append('<li><a data-event="padding" href="#" data-value="large">'+_t('Large')+'</a></li>'); + $ul.append('<li><a data-event="padding" href="#" data-value="xl">'+_t('Xl')+'</a></li>'); + + // circle, boxed... options became toggled + $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove(); + var $button = $(tplIconButton('fa fa-sun-o', { + title: _t('Shadow'), + event: 'imageShape', + value: 'shadow' + })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-circle"]')); + + // add spin for fa + var $spin = $('<div class="btn-group hidden only_fa"/>').insertAfter($button.parent()); + $(tplIconButton('fa fa-refresh', { + title: _t('Spin'), + event: 'imageShape', + value: 'fa-spin' + })).appendTo($spin); + + // resize for fa + var $resizefa = $('<div class="btn-group hidden only_fa"/>') + .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])')); + for (var size=1; size<=5; size++) { + $(tplButton('<span class="note-fontsize-10">'+size+'x</span>', { + title: size+"x", + event: 'resizefa', + value: size+'' + })).appendTo($resizefa); + } + var $colorfa = $airPopover.find('.note-color').clone(); + $colorfa.find(".btn-group:first").remove(); + $colorfa.find("ul.dropdown-menu").css('min-width', '172px'); + $colorfa.find('button[data-event="color"]').attr('data-value', '{"foreColor": "#f00"}') + .find("i").css({'background': '', 'color': '#f00'}); + $resizefa.after($colorfa); + + // show dialog box and delete + var $imageprop = $('<div class="btn-group"/>'); + $imageprop.appendTo($imagePopover.find('.popover-content')); + $(tplIconButton('fa fa-picture-o', { + title: _t('Edit'), + event: 'showImageDialog' + })).appendTo($imageprop); + $(tplIconButton('fa fa-trash-o', { + title: _t('Remove'), + event: 'delete' + })).appendTo($imageprop); + + $imagePopover.find('.popover-content').append($airPopover.find(".note-history").clone()); + + $imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone()); + + //////////////// link popover + + $linkPopover.find('.popover-content').append($airPopover.find(".note-history").clone()); + + $linkPopover.find('button[data-event="showLinkDialog"] i').attr("class", "fa fa-link"); + $linkPopover.find('button[data-event="unlink"]').before($airPopover.find('button[data-event="showImageDialog"]').clone()); + + //////////////// text/air popover + + //// highlight the text format + $airPopover.find('.note-style').on('mousedown', function () { + var $format = $airPopover.find('[data-event="formatBlock"]'); + var node = range.create().sc; + var formats = $format.map(function () { return $(this).data("value"); }).get(); + while (node && (!node.tagName || (!node.tagName || formats.indexOf(node.tagName.toLowerCase()) === -1))) { + node = node.parentNode; + } + $format.parent().removeClass('active'); + $format.filter('[data-value="'+(node ? node.tagName.toLowerCase() : "p")+'"]') + .parent().addClass("active"); + }); + + //////////////// tooltip + + $airPopover.add($linkPopover).add($imagePopover).find("button") + .tooltip('destroy') + .tooltip({ + container: 'body', + trigger: 'hover', + placement: 'bottom' + }).on('click', function () {$(this).tooltip('hide');}); + + return $popover; +}; + +var fn_boutton_update = eventHandler.popover.button.update; +eventHandler.popover.button.update = function ($container, oStyle) { + // stop animation when edit content + var previous = $(".note-control-selection").data('target'); + if (previous) { + $(previous).css({"-webkit-animation-play-state": "", "animation-play-state": "", "-webkit-transition": "", "transition": "", "-webkit-animation": "", "animation": ""}); + } + if (oStyle.image) { + $(oStyle.image).css({"-webkit-animation": "none", "animation": "none"}); + } + // end + + fn_boutton_update.call(this, $container, oStyle); + + if (oStyle.image) { + $container.find('[data-event]').parent().removeClass("active"); + + $container.find('a[data-event="padding"][data-value="small"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-small")); + $container.find('a[data-event="padding"][data-value="medium"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-medium")); + $container.find('a[data-event="padding"][data-value="large"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-large")); + $container.find('a[data-event="padding"][data-value="xl"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-xl")); + $container.find('a[data-event="padding"][data-value=""]').parent().toggleClass("active", !$container.find('.active a[data-event="padding"]').length); + + if (dom.isImgFont(oStyle.image)) { + + $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-event="imageShape"])').addClass("hidden"); + $container.find('.only_fa').removeClass("hidden"); + $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x")); + $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x")); + $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x")); + $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x")); + $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length); + + $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin")); + + $container.find('.note-color').removeClass("hidden"); + + } else { + + $container.find('.hidden:not(.only_fa)').removeClass("hidden"); + $container.find('.only_fa').addClass("hidden"); + var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/); + if (width) { + width = width[2]; + } + $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%"); + $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%"); + $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%"); + + $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow")); + + if (!$(oStyle.image).is("img")) { + $container.find('.btn-group:has(button[data-event="imageShape"])').addClass("hidden"); + } + + $container.find('.note-color').addClass("hidden"); + + } + + $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("pull-left")); + $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("center-block")); + $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("pull-right")); + + $(oStyle.image).trigger('attributes_change'); + } +}; + +var fn_popover_update = eventHandler.popover.update; +eventHandler.popover.update = function ($popover, oStyle, isAirMode) { + var $imagePopover = $popover.find('.note-image-popover'); + var $linkPopover = $popover.find('.note-link-popover'); + var $airPopover = $popover.find('.note-air-popover'); + + fn_popover_update.call(this, $popover, oStyle, isAirMode); + + if (!isAirMode || $(oStyle.range.sc).closest('[data-oe-model]:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"])').length) { + $imagePopover.hide(); + $linkPopover.hide(); + $airPopover.hide(); + return; + } + + if (oStyle.image) { + if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) { + oStyle.image = oStyle.image.parentNode; + } + var alt = $(oStyle.image).attr("alt"); + + $imagePopover.find('.o_image_alt').text( (alt || "").replace(/"/g, '"') ).parent().toggle(oStyle.image.tagName === "IMG"); + $imagePopover.show(); + + range.createFromNode(dom.firstChild(oStyle.image)).select(); + } else { + $(".note-control-selection").hide(); + } + + if (oStyle.image || (!oStyle.range.isCollapsed() || (oStyle.range.sc.tagName && !dom.isAnchor(oStyle.range.sc)) || (oStyle.image && !$(oStyle.image).closest('a').length))) { + $linkPopover.hide(); + oStyle.anchor = false; + } + + if (oStyle.image || oStyle.anchor || !$(oStyle.range.sc).closest('.note-editable').length) { + $airPopover.hide(); + } else { + $airPopover.show(); + } +}; + +eventHandler.handle.update = function ($handle, oStyle, isAirMode) { + $handle.toggle(!!oStyle.image); + if (oStyle.image) { + var $selection = $handle.find('.note-control-selection'); + var $image = $(oStyle.image); + var szImage = { + w: parseInt($image.outerWidth(true), 10), + h: parseInt($image.outerHeight(true), 10) + }; + $selection.data('target', oStyle.image); // save current image element. + var sSizing = szImage.w + 'x' + szImage.h; + $selection.find('.note-control-selection-info').text(szImage.h > 50 ? sSizing : ""); + + $selection.find('.note-control-sizing').toggleClass('note-control-sizing note-control-holder').css({ + 'border-top': 0, + 'border-left': 0 + }); + } +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* hack for image and link editor */ + +function getImgTarget () { + return $(".note-control-selection").data('target'); +} +eventHandler.editor.padding = function ($editable, sValue) { + var $target = $(getImgTarget()); + var paddings = "small medium large xl".split(/\s+/); + $editable.data('NoteHistory').recordUndo(); + if (sValue.length) { + paddings.splice(paddings.indexOf(sValue),1); + $target.toggleClass('padding-'+sValue); + } + $target.removeClass("padding-" + paddings.join(" padding-")); +}; +eventHandler.editor.resize = function ($editable, sValue) { + var $target = $(getImgTarget()); + $editable.data('NoteHistory').recordUndo(); + var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/); + if (width) { + width = width[2]/100; + } + $(getImgTarget()).css('width', width != sValue ? (sValue * 100) + '%' : ''); +}; +eventHandler.editor.resizefa = function ($editable, sValue) { + var $target = $(getImgTarget()); + $editable.data('NoteHistory').recordUndo(); + $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, '')); + if (+sValue > 1) { + $target.addClass('fa-'+sValue+'x'); + } +}; +eventHandler.editor.floatMe = function ($editable, sValue) { + var $target = $(getImgTarget()); + $editable.data('NoteHistory').recordUndo(); + switch (sValue) { + case 'center': $target.toggleClass('center-block').removeClass('pull-right pull-left'); break; + case 'left': $target.toggleClass('pull-left').removeClass('pull-right center-block'); break; + case 'right': $target.toggleClass('pull-right').removeClass('pull-left center-block'); break; + } +}; +eventHandler.editor.imageShape = function ($editable, sValue) { + var $target = $(getImgTarget()); + $editable.data('NoteHistory').recordUndo(); + $target.toggleClass(sValue); +}; + +eventHandler.dialog.showLinkDialog = function ($editable, $dialog, linkInfo) { + $editable.data('range').select(); + $editable.data('NoteHistory').recordUndo(); + + var editor = new widgets.LinkDialog($editable, linkInfo); + editor.appendTo(document.body); + + var def = new $.Deferred(); + editor.on("save", this, function (linkInfo) { + linkInfo.range.select(); + $editable.data('range', linkInfo.range); + def.resolve(linkInfo); + $editable.trigger('keyup'); + $('.note-popover .note-link-popover').show(); + }); + editor.on("cancel", this, function () { + def.reject(); + }); + return def; +}; +eventHandler.dialog.showImageDialog = function ($editable) { + var r = $editable.data('range'); + if (r.sc.tagName && r.sc.childNodes.length) { + r.sc = r.sc.childNodes[r.so]; + } + var editor = new widgets.MediaDialog($editable, dom.isImg(r.sc) ? r.sc : null); + editor.appendTo(document.body); + return new $.Deferred().reject(); +}; +$.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) { + var $editable = layoutInfo.editable(); + var $selection = layoutInfo.handle().find('.note-control-selection'); + var media = $selection.data('target'); + new widgets.alt($editable, media).appendTo(document.body); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +dom.isVoid = function (node) { + return node && /^BR|^IMG|^HR/.test(node.nodeName.toUpperCase()) || dom.isImg(node); +}; +dom.isImg = function (node) { + return dom.isImgFont(node) || (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i)) )); +}; +dom.isForbiddenNode = function (node) { + return $(node).is(".media_iframe_video, .fa, img"); +}; + +dom.isImgFont = function (node) { + var nodeName = node && node.nodeName.toUpperCase(); + var className = (node && node.className || ""); + if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) { + var classNames = className.split(/\s+/); + for (var k=0; k<widgets.fontIcons.length; k++) { + if (_.intersection(widgets.fontIcons[k].icons, classNames).length) { + return true; + } + } + } + return false; +}; +// re-overwrite font to include theme icons +var isFont = dom.isFont; +dom.isFont = function (node) { + return dom.isImgFont(node) || isFont(node); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +var fn_visible = $.summernote.pluginEvents.visible; +$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) { + var res = fn_visible.call(this, event, editor, layoutInfo); + var $node = $(dom.node(range.create().sc)); + if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) && $node.hasClass("o_editable") && !$node[0].children.length) { + var p = $('<p><br/></p>')[0]; + $node.append( p ); + range.createFromNode(p.firstChild).select(); + } + return res; +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* fix ie and re-range to don't break snippet*/ + +var initial_data = {}; +function reRangeSelectKey (event) { + initial_data.range = null; + if (event.shiftKey && event.keyCode >= 37 && event.keyCode <= 40 && !$(event.target).is("input, textarea, select")) { + var r = range.create(); + if (r) { + var rng = r.reRange(event.keyCode <= 38); + if (r !== rng) { + rng.select(); + } + } + } +} +function reRangeSelect (event, dx, dy) { + var r = range.create(); + if (!r || r.isCollapsed()) return; + + // check if the user move the caret on up or down + var data = r.reRange(dy < 0 || (dy === 0 && dx < 0)); + + if (data.sc !== r.sc || data.so !== r.so || data.ec !== r.ec || data.eo !== r.eo) { + setTimeout(function () { + data.select(); + $(data.sc.parentNode).closest('.note-popover'); + },0); + } + + $(data.sc).closest('.o_editable').data('range', r); + return r; +} +function summernote_mouseup (event) { + if ($(event.target).closest("#web_editor-top-navbar, .note-popover").length) { + return; + } + // don't rerange if simple click + if (initial_data.event) { + var dx = event.clientX - (event.shiftKey ? initial_data.rect.left : initial_data.event.clientX); + var dy = event.clientY - (event.shiftKey ? initial_data.rect.top : initial_data.event.clientY); + if (10 < Math.pow(dx, 2)+Math.pow(dy, 2)) { + reRangeSelect(event, dx, dy); + } + } + + if (!$(event.target).closest(".o_editable").length) { + return; + } + if (!initial_data.range || !event.shiftKey) { + setTimeout(function () { + initial_data.range = range.create(); + },0); + } +} +var remember_selection; +function summernote_mousedown (event) { + rte.history.splitNext(); + + var $editable = $(event.target).closest(".o_editable, .note-editor"); + + if (!!document.documentMode) { + summernote_ie_fix(event, function (node) { return node.tagName === "DIV" || node.tagName === "IMG" || (node.dataset && node.dataset.oeModel); }); + } else if (last_div && event.target !== last_div) { + if (last_div.tagName === "A") { + summernote_ie_fix(event, function (node) { return node.dataset && node.dataset.oeModel; }); + } else if ($editable.length) { + if (summernote_ie_fix(event, function (node) { return node.tagName === "A"; })) { + r = range.create(); + r.select(); + } + } + } + + // remember_selection when click on non editable area + var r = range.create(); + if ($(r ? dom.node(r.sc) : event.srcElement || event.target).closest('#web_editor-top-navbar, #oe_main_menu_navbar, .note-popover, .note-toolbar, .modal').length) { + if (!$(event.target).is('input, select, label, button, a')) { + if (!remember_selection && $editable[0]) { + remember_selection = range.create(dom.firstChild($editable[0]), 0); + } + if (!remember_selection && $editable[0]) { + try { + remember_selection.select(); + } catch (e) { + console.warn(e); + } + } + } + } else if (r && $(dom.node(r.sc)).closest('.o_editable, .note-editable').length) { + remember_selection = r; + } + + initial_data.event = event; + + // keep selection when click with shift + if (event.shiftKey && $editable.length) { + if (initial_data.range) { + initial_data.range.select(); + } + var rect = r && r.getClientRects(); + initial_data.rect = rect && rect.length ? rect[0] : { top: 0, left: 0 }; + } +} + +var last_div; +var last_div_change; +var last_editable; +function summernote_ie_fix (event, pred) { + var editable; + var div; + var node = event.target; + while(node.parentNode) { + if (!div && pred(node)) { + div = node; + } + if(last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) { + break; + } + if (node.className && node.className.indexOf('o_editable') !== -1) { + if (!div) { + div = node; + } + editable = node; + break; + } + node = node.parentNode; + } + + if (!editable) { + $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable"); + $(last_editable).attr("contentEditable", "true").prop("contentEditable", "true"); + last_div_change = null; + last_editable = null; + return; + } + + if (div === last_div) { + return; + } + + last_div = div; + + $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable"); + + if (last_editable !== editable) { + if ($(editable).is("[contentEditable='true']")) { + $(editable).removeAttr("contentEditable").removeProp("contentEditable"); + last_editable = editable; + } else { + last_editable = null; + } + } + if (!$(div).attr("contentEditable") && !$(div).is("[data-oe-type='many2one'], [data-oe-type='contact']")) { + $(div).attr("contentEditable", "true").prop("contentEditable", "true"); + last_div_change = div; + } else { + last_div_change = null; + } + return editable !== div ? div : null; +} + +var fn_attach = eventHandler.attach; +eventHandler.attach = function (oLayoutInfo, options) { + fn_attach.call(this, oLayoutInfo, options); + oLayoutInfo.editor.on('dragstart', 'img', function (e) { e.preventDefault(); }); + $(document).on('mousedown', summernote_mousedown); + $(document).on('mouseup', summernote_mouseup); + oLayoutInfo.editor.off('click').on('click', function (e) {e.preventDefault();}); // if the content editable is a link + oLayoutInfo.editor.on('dblclick', 'img, .media_iframe_video, span.fa, i.fa, span.fa', function (event) { + if (!$(event.target).closest(".note-toolbar").length) { // prevent icon edition of top bar for default summernote + new widgets.MediaDialog(oLayoutInfo.editor, event.target).appendTo(document.body); + } + }); + $(document).on("keyup", reRangeSelectKey); + + var clone_data = false; + var $node = oLayoutInfo.editor; + if ($node.data('oe-model')) { + $node.on('content_changed', function () { + + var $nodes = $('[data-oe-model]') + .filter(function () { return this != $node[0];}) + .filter('[data-oe-model="'+$node.data('oe-model')+'"]') + .filter('[data-oe-id="'+$node.data('oe-id')+'"]') + .filter('[data-oe-field="'+$node.data('oe-field')+'"]'); + if ($node.data('oe-type')) $nodes = $nodes.filter('[data-oe-type="'+$node.data('oe-type')+'"]'); + if ($node.data('oe-expression')) $nodes = $nodes.filter('[data-oe-expression="'+$node.data('oe-expression')+'"]'); + if ($node.data('oe-xpath')) $nodes = $nodes.filter('[data-oe-xpath="'+$node.data('oe-xpath')+'"]'); + if ($node.data('oe-contact-options')) $nodes = $nodes.filter('[data-oe-contact-options="'+$node.data('oe-contact-options')+'"]'); + + var nodes = $node.get(); + + if ($node.data('oe-type') === "many2one") { + $nodes = $nodes.add($('[data-oe-model]') + .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; }) + .filter('[data-oe-many2one-model="'+$node.data('oe-many2one-model')+'"]') + .filter('[data-oe-many2one-id="'+$node.data('oe-many2one-id')+'"]') + .filter('[data-oe-type="many2one"]')); + + $nodes = $nodes.add($('[data-oe-model]') + .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; }) + .filter('[data-oe-model="'+$node.data('oe-many2one-model')+'"]') + .filter('[data-oe-id="'+$node.data('oe-many2one-id')+'"]') + .filter('[data-oe-field="name"]')); + } + + if (!clone_data) { + clone_data = true; + $nodes.html(this.innerHTML); + clone_data = false; + } + }); + } + + var $toolbar = $(oLayoutInfo.popover).add(oLayoutInfo.toolbar); + $('button[data-event="undo"], button[data-event="redo"]', $toolbar).attr('disabled', true); + + $(oLayoutInfo.editor) + .add(oLayoutInfo.handle) + .add(oLayoutInfo.popover) + .add(oLayoutInfo.toolbar) + .on('click keyup', function () { + $('button[data-event="undo"]', $toolbar).attr('disabled', !oLayoutInfo.editable.data('NoteHistory').hasUndo()); + $('button[data-event="redo"]', $toolbar).attr('disabled', !oLayoutInfo.editable.data('NoteHistory').hasRedo()); + }); +}; +var fn_detach = eventHandler.detach; +eventHandler.dettach = function (oLayoutInfo, options) { + fn_detach.call(this, oLayoutInfo, options); + oLayoutInfo.editor.off("dragstart"); + $(document).off('mousedown', summernote_mousedown); + $(document).off('mouseup', summernote_mouseup); + oLayoutInfo.editor.off("dblclick"); + $(document).off("keyup", reRangeSelectKey); +}; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// +/* Translation for odoo */ + +$.summernote.lang.odoo = { + font: { + bold: _t('Bold'), + italic: _t('Italic'), + underline: _t('Underline'), + strikethrough: _t('Strikethrough'), + subscript: _t('Subscript'), + superscript: _t('Superscript'), + clear: _t('Remove Font Style'), + height: _t('Line Height'), + name: _t('Font Family'), + size: _t('Font Size') + }, + image: { + image: _t('Picture'), + insert: _t('Insert Image'), + resizeFull: _t('Resize Full'), + resizeHalf: _t('Resize Half'), + resizeQuarter: _t('Resize Quarter'), + floatLeft: _t('Float Left'), + floatRight: _t('Float Right'), + floatNone: _t('Float None'), + dragImageHere: _t('Drag an image here'), + selectFromFiles: _t('Select from files'), + url: _t('Image URL'), + remove: _t('Remove Image') + }, + link: { + link: _t('Link'), + insert: _t('Insert Link'), + unlink: _t('Unlink'), + edit: _t('Edit'), + textToDisplay: _t('Text to display'), + url: _t('To what URL should this link go?'), + openInNewWindow: _t('Open in new window') + }, + video: { + video: _t('Video'), + videoLink: _t('Video Link'), + insert: _t('Insert Video'), + url: _t('Video URL?'), + providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)') + }, + table: { + table: _t('Table') + }, + hr: { + insert: _t('Insert Horizontal Rule') + }, + style: { + style: _t('Style'), + normal: _t('Normal'), + blockquote: _t('Quote'), + pre: _t('Code'), + h1: _t('Header 1'), + h2: _t('Header 2'), + h3: _t('Header 3'), + h4: _t('Header 4'), + h5: _t('Header 5'), + h6: _t('Header 6') + }, + lists: { + unordered: _t('Unordered list'), + ordered: _t('Ordered list') + }, + options: { + help: _t('Help'), + fullscreen: _t('Full Screen'), + codeview: _t('Code View') + }, + paragraph: { + paragraph: _t('Paragraph'), + outdent: _t('Outdent'), + indent: _t('Indent'), + left: _t('Align left'), + center: _t('Align center'), + right: _t('Align right'), + justify: _t('Justify full') + }, + color: { + recent: _t('Recent Color'), + more: _t('More Color'), + background: _t('Background Color'), + foreground: _t('Foreground Color'), + transparent: _t('Transparent'), + setTransparent: _t('Set transparent'), + reset: _t('Reset'), + resetToDefault: _t('Reset to default') + }, + shortcut: { + shortcuts: _t('Keyboard shortcuts'), + close: _t('Close'), + textFormatting: _t('Text formatting'), + action: _t('Action'), + paragraphFormatting: _t('Paragraph formatting'), + documentStyle: _t('Document Style') + }, + history: { + undo: _t('Undo'), + redo: _t('Redo') + } +}; + +}); diff --git a/addons/web_editor/static/src/js/snippets.animation.js b/addons/web_editor/static/src/js/snippets.animation.js new file mode 100644 index 00000000000..b3fccc5584a --- /dev/null +++ b/addons/web_editor/static/src/js/snippets.animation.js @@ -0,0 +1,82 @@ +odoo.define('web_editor.snippets.animation', function (require) { +'use strict'; + +var Class = require('web.Class'); +var base = require('web_editor.base'); + +var registry = []; +var ready = []; + +var Animation = Class.extend({ + selector: false, + $: function () { + return this.$el.find.apply(this.$el, arguments); + }, + init: function (dom, editable_mode) { + this.$el = this.$target = $(dom); + this.start(editable_mode); + }, + /* + * start + * This method is called after init + */ + start: function (editable_mode) { + }, + /* + * stop + * This method is called to stop the animation (e.g.: when rte is launch) + */ + stop: function () { + }, +}); + +var start = function (editable_mode, $target) { + for (var k in registry) { + var Animation = registry[k]; + var selector = ""; + if (Animation.prototype.selector) { + if (selector != "") selector += ", " + selector += Animation.prototype.selector; + } + if ($target) { + if ($target.is(selector)) selector = $target; + else continue; + } + + $(selector).each(function() { + var $snipped_id = $(this); + if ( !$snipped_id.parents("#oe_snippets").length && + !$snipped_id.parent("body").length && + !$snipped_id.data("snippet-view")) { + ready.push($snipped_id); + $snipped_id.data("snippet-view", new Animation($snipped_id, editable_mode)); + } else if ($snipped_id.data("snippet-view")) { + $snipped_id.data("snippet-view").start(editable_mode); + } + }); + } +}; + +var stop = function () { + $(ready).each(function() { + var $snipped_id = $(this); + if ($snipped_id.data("snippet-view")) { + $snipped_id.data("snippet-view").stop(); + } + }); +}; + +base.ready(function () { + setTimeout(function () { + start(); + },0); +}); + +return { + 'Class': Class, + 'registry': registry, + 'start': start, + 'stop': stop +}; + +});
\ No newline at end of file diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js new file mode 100644 index 00000000000..9d37a9f84c8 --- /dev/null +++ b/addons/web_editor/static/src/js/snippets.editor.js @@ -0,0 +1,1137 @@ +odoo.define('web_editor.snippet.editor', function (require) { +'use strict'; + +var Class = require('web.Class'); +var ajax = require('web.ajax'); +var core = require('web.core'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var editor = require('web_editor.editor'); +var animation = require('web_editor.snippets.animation'); +var options = require('web_editor.snippets.options'); + +var qweb = core.qweb; +var dummy = editor.dummy; + +/* ----- SNIPPET SELECTOR ---- */ + +ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb); + +editor.Class.include({ + init: function() { + var self = this; + var res = this._super.apply(this, arguments); + var $editable = this.rte.editable(); + this.buildingBlock = new BuildingBlock(this, $editable); + this.buildingBlock.on("snippets:ready", this, function () { + self.trigger("snippets:ready"); + }); + return res; + }, + start: function () { + var self = this; + animation.stop(); + this.buildingBlock.insertAfter(this.$el); + animation.start(true); + + this.rte.editable().find("*").off('mousedown mouseup click'); + return this._super(); + }, + save: function () { + this.buildingBlock.clean_for_save(); + this._super(); + }, +}); + +/* ----- SNIPPET SELECTOR ---- */ + +$.extend($.expr[':'],{ + hasData: function(node,i,m){ + return !!_.toArray(node.dataset).length; + }, + data: function(node,i,m){ + return $(node).data(m[3]); + } +}); + +var globalSelector = { + closest: function () { return $(); }, + all: function () { return $(); }, + is: function () { return false; }, +}; + +/* ----- Jquery activate block ---- */ + +$.fn.extend({ + activateBlock: function () { + var target = globalSelector.closest($(this))[0] || (dom.isBR(this) ? this.parentNode : dom.node(this)); + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target); + target.dispatchEvent(evt); + return this; + } +}); + +/* ----- BuildingBlock (managment of drag&drop and focus/blur snippets) ---- */ + +var BuildingBlock = Widget.extend({ + template: 'web_editor.snippets', + activeSnippets: [], + init: function (parent, $editable) { + this.parent = parent; + this.$editable = $editable; + + this._super.apply(this, arguments); + if(!$('#oe_manipulators').length){ + $("<div id='oe_manipulators' class='o_css_editor'></div>").appendTo('body'); + } + this.$active_snipped_id = false; + this.snippets = []; + + data.instance = this; + }, + start: function() { + var self = this; + this.$snippet = $("#oe_snippets"); + + this.$el + .on("mouseenter", function () { self.show(); }) + .on("mouseleave", function (event) { if (event.clientX>0 && event.clientY>0) self.hide(); }); + + $(window).resize(function () { + setTimeout('$(document).click()',0); + }); + + this.fetch_snippet_templates(); + this.bind_snippet_click_editor(); + + $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) { + e.preventDefault(); + }); + + var _isNotBreakable = $.summernote.core.dom.isNotBreakable; + $.summernote.core.dom.isNotBreakable = function (node) { + return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node)); + }; + + $(window).on('resize', function () { + if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { + self.cover_target(self.$active_snipped_id.data("snippet-editor").$overlay, self.$active_snipped_id); + } + }); + + $(document).on('mousemove', function () { + if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { + self.$active_snipped_id.data("snippet-editor").$overlay.removeClass('o_keypress'); + } + }); + $(document).on('keydown', function (event) { + if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { + self.$active_snipped_id.data("snippet-editor").$overlay.addClass('o_keypress'); + } + if ((event.metaKey || (event.ctrlKey && !event.altKey)) && event.shiftKey && event.keyCode >= 48 && event.keyCode <= 57) { + self.$snippet.find('.scroll:first > ul li:eq('+(event.keyCode-49)+') a').trigger("click"); + self.show(); + event.preventDefault(); + } + }); + }, + + _get_snippet_url: function () { + return '/web_editor/snippets'; + }, + _add_check_selector : function (selector, no_check) { + var self = this; + var selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)"; + + if (no_check) { + return { + closest: function ($from, parentNode) { + return $from.closest(selector, parentNode); + }, + all: function ($from) { + return $from ? $from.find(selector) : $(selector); + }, + is: function ($from) { + return $from.is(selector); + } + }; + } else { + var selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable"; + return { + closest: function ($from, parentNode) { + var parents = self.$editable.get(); + return $from.closest(selector, parentNode).filter(function () { + var node = this; + while (node.parentNode) { + if (parents.indexOf(node)!==-1) { + return true; + } + node = node.parentNode; + } + return false; + }); + }, + all: function ($from) { + return $from ? $from.find(selector) : self.$editable.filter(selector).add(self.$editable.find(selector)); + }, + is: function ($from) { + return $from.is(selector); + } + }; + } + }, + + fetch_snippet_templates: function () { + var self = this; + var url = this._get_snippet_url(); + if (!url || !url.length) { + this.$snippet.detach(); + return; + } + this.$el.find("#o_left_bar").addClass("hidden"); + return ajax.jsonRpc(url, 'call', {}).then(function (html) { + self.compute_snippet_templates(html); + self.trigger("snippets:ready"); + }); + }, + compute_snippet_templates: function (html) { + var self = this; + var $html = $(html); + var $left_bar = this.$el.find("#o_left_bar"); + var $ul = $html.siblings("ul"); + var $scroll = $html.siblings("#o_scroll"); + + if (!$scroll.length) { + throw new Error("Wrong snippets xml definition"); + } + + $ul.children().tooltip({ + delay: { "show": 500, "hide": 100 }, + container: 'body', + title: function () { + return (navigator.appVersion.indexOf('Mac') > -1 ? 'CMD' : 'CTRL')+'+SHIFT+'+($(this).index()+1); + }, + trigger: 'hover', + placement: 'top' + }).on('click', function () {$(this).tooltip('hide');}); + + // t-snippet + $html.find('[data-oe-type="snippet"][data-oe-name]').each(function () { + var $div = $('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name')); + }); + // end + + self.templateOptions = []; + 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() + }; + self.templateOptions.push(option); + selector.push(option.selector); + }); + + $styles.addClass("hidden"); + globalSelector = { + closest: function ($from) { + var $temp; + var $target; + var len = selector.length; + for (var i = 0; i<len; i++) { + $temp = selector[i].closest($from, $target && $target[0]); + if (!$target || $temp.length) { + $target = $temp; + } + } + return $target; + }, + all: function ($from) { + var $target; + var len = selector.length; + for (var i = 0; i<len; i++) { + if (!$target) $target = selector[i].all($from); + else $target = $target.add(selector[i].all($from)); + } + return $target; + }, + is: function ($from) { + var len = selector.length; + for (var i = 0; i<len; i++) { + if (selector[i].is($from)) { + return true; + } + } + return false; + }, + }; + + var number = 0; + + // oe_snippet_body + self.$snippets = $scroll.find(".o_panel_body").children() + .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'); + } + number++; + }); + + // hide scroll if no snippets defined + if (!number) { + this.$snippet.detach(); + } else { + this.$el.find("#o_left_bar").removeClass("hidden"); + } + $("body").toggleClass("editor_has_snippets", !!number); + + // select all default text to edit (if snippet default text) + /*self.$snippets.find('.oe_snippet_body, .oe_snippet_body *') + .contents() + .filter(function() { + return this.nodeType === 3 && this.textContent.match(/\S/); + }).parent().addClass("o_default_snippet_text");*/ + $(document).on("mouseup", ".o_default_snippet_text", function (event) { + $(event.target).selectContent(); + }); + $(document).on("keyup", function (event) { + var r = $.summernote.core.range.create(); + $(r && r.sc).closest(".o_default_snippet_text").removeClass("o_default_snippet_text"); + }); + // end + + // 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 + + $html.find('.o_not_editable').attr("contentEditable", false); + + $left_bar.append($html); + + // animate for list of snippet blocks + $left_bar.on('click', '.scroll-link', function (event) { + event.preventDefault(); + var targetOffset = $($(this).attr("href")).position().top - $ul.outerHeight() + $scroll[0].scrollTop; + $scroll.animate({'scrollTop': targetOffset}, 750); + }); + $scroll.on('scroll', function () { + var middle = $scroll.height()/4; + var $li = $ul.find("a").parent().removeClass('active'); + var last; + for (var k=0; k<$li.length; k++) { + var li = $($li[k]); + if (!li.data('target')) { + li.data('target', $($("a", li).attr("href"))); + } + if (li.data('target').position().top > middle) { + break; + } + last = $li[k]; + } + $(last).addClass("active"); + }); + // end + + // display scrollbar + $(window).on('resize', function () { + $scroll.css("overflow", ""); + var height = $left_bar.height() - $ul.outerHeight(); + $scroll.css("height", height); + var $last = $scroll.children(":visible").last().children(".o_panel_body"); + $last.css({'min-height': (height-$last.prev().outerHeight())+'px'}); + if ($scroll[0].scrollHeight + $ul[0].scrollHeight > document.body.clientHeight) { + $scroll.css("overflow", "auto").css("width", "226px"); + } else { + $scroll.css("width", ""); + } + }).trigger('resize'); + // end + + self.make_snippet_draggable(self.$snippets); + }, + + cover_target: function ($el, $target){ + if($el.data('not-cover_target')) return; + var pos = $target.offset(); + var mt = parseInt($target.css("margin-top") || 0); + var mb = parseInt($target.css("margin-bottom") || 0); + var width = $target.outerWidth(); + var bigger = pos.left+width > $("body").outerWidth()-8; + $el.css({ + 'width': width, + 'top': pos.top - mt - 5, + 'left': pos.left-1 + }); + $el.find(".oe_handle.e,.oe_handle.w").css({'height': $target.outerHeight() + mt + mb+1}); + if (bigger) { + $el.find(".oe_handle.e").css({right: 0, margin: 0}) + .find("div").css({right: 0, left: 'auto'}); + } else { + $el.find(".oe_handle.e").css({right: "", margin: ""}) + .find("div").css({right: "", left: ""}); + } + $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': width-2}); + }, + + show_blocks: function () { + var self = this; + var cache = {}; + this.$snippet.find(".o_panel").each(function () { + var catcheck = false; + var $category = $(this); + $category.find(".oe_snippet_body").each(function () { + var $snippet = $(this); + + var check = false; + + for (var k in self.templateOptions) { + var option = self.templateOptions[k]; + if ($snippet.is(option.base_selector)) { + + cache[k] = cache[k] || { + 'drop-near': option['drop-near'] ? option['drop-near'].all() : [], + 'drop-in': option['drop-in'] ? option['drop-in'].all() : [] + }; + + if (cache[k]['drop-near'].length || cache[k]['drop-in'].length) { + catcheck = true; + check = true; + break; + } + } + } + + if (check) { + $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(catcheck); + }); + }, + show: function () { + var self = this; + this.make_active(false); + this.$el.addClass("o_open"); + this.show_blocks(); + }, + hide: function () { + this.$el.removeClass("o_open"); + }, + bind_snippet_click_editor: function () { + var self = this; + var snipped_event_flag; + $(document).on('click', '*', function (event) { + var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target) || event.target); + if (self.editor_busy || snipped_event_flag===srcElement || !srcElement) { + return; + } + snipped_event_flag = srcElement; + + setTimeout(function () {snipped_event_flag = false;}, 0); + var $target = $(srcElement); + + if ($target.closest(".oe_overlay, .note-popover").length) { + return; + } + + if (!globalSelector.is($target)) { + $target = globalSelector.closest($target); + } + + 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 opt = options.registry; + var template = self.templateOptions; + for (var k in template) { + var Option = opt[template[k]['option']]; + if (Option && Option.prototype.clean_for_save !== dummy) { + template[k].selector.all().filter(".o_dirty").each(function () { + new Option(self, null, $(this), k).clean_for_save(); + }); + } + } + this.$editable.find("*[contentEditable], *[attributeEditable]") + .removeAttr('contentEditable') + .removeProp('contentEditable') + .removeAttr('attributeEditable') + .removeProp('attributeEditable'); + }, + 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 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_siblings = $(); + var $selector_children = $(); + var vertical = false; + var temp = self.templateOptions; + for (var k in temp) { + if ($base_body.is(temp[k].base_selector)) { + if (temp[k]['drop-near']) { + if (!$selector_siblings) $selector_siblings = temp[k]['drop-near'].all(); + else $selector_siblings = $selector_siblings.add(temp[k]['drop-near'].all()); + } + if (temp[k]['drop-in']) { + if (!$selector_children) $selector_children = temp[k]['drop-in'].all(); + else $selector_children = $selector_children.add(temp[k]['drop-in'].all()); + } + } + } + + $toInsert = $base_body.clone(); + + 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($selector_siblings, $selector_children); + + $('.oe_drop_zone').droppable({ + over: function(){ + dropped = true; + $(this).first().after($toInsert); + }, + out: function(){ + var prev = $toInsert.prev(); + if(this === prev[0]){ + dropped = false; + $toInsert.detach(); + } + } + }); + }, + stop: function(ev, ui){ + $toInsert.removeClass('oe_snippet_body'); + + if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3) { + var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first(); + if (el.length) { + el.after($toInsert); + dropped = true; + } + } + + self.$editable.find('.oe_drop_zone').droppable('destroy').remove(); + + if (dropped) { + + var prev = $toInsert.first()[0].previousSibling; + var next = $toInsert.last()[0].nextSibling; + var rte = self.parent.rte; + + if (prev) { + $toInsert.detach(); + rte.historyRecordUndo($(prev)); + $toInsert.insertAfter(prev); + } else if (next) { + $toInsert.detach(); + rte.historyRecordUndo($(next)); + $toInsert.insertBefore(next); + } else { + var $parent = $toInsert.parent(); + $toInsert.detach(); + rte.historyRecordUndo($parent); + $parent.prepend($toInsert); + } + + $toInsert.closest(".o_editable").trigger("content_changed"); + + var $target = false; + $target = $toInsert; + + setTimeout(function () { + self.$snippet.trigger('snippet-dropped', $target); + + animation.start(true, $target); + + // 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 self.templateOptions) { + self.templateOptions[k].selector.all($target).each(function () { + var $snippet = $(this); + self.create_overlay($snippet); + if ($snippet.data("snippet-editor")) { + $snippet.data("snippet-editor").drop_and_build_snippet(); + } + }); + } + $target.closest(".o_editable").trigger("content_changed"); + // 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_siblings, $selector_children){ + var self = this; + var zone_template = $("<div class='oe_drop_zone oe_insert'></div>"); + + if ($selector_children) { + $selector_children.each(function (){ + var $zone = $(this); + var css = window.getComputedStyle(this); + var float = css.float || css.cssFloat; + var $drop = zone_template.clone(); + + $zone.append($drop); + var node = $drop[0].previousSibling; + var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); + if (test) { + $drop.addClass("oe_vertical oe_vertical_text").css({ + 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), + 'float': 'none', + 'display': 'inline-block' + }); + } else if (float === "left" || float === "right") { + $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30)); + } + + $drop = $drop.clone(); + + $zone.prepend($drop); + var node = $drop[0].nextSibling; + var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); + if (test) { + $drop.addClass("oe_vertical oe_vertical_text").css({ + 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), + 'float': 'none', + 'display': 'inline-block' + }); + } else if (float === "left" || float === "right") { + $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30)); + } + if (test) { + $drop.css({'float': 'none', 'display': 'inline-block'}); + } + }); + + // add children near drop zone + $selector_siblings = $(_.uniq(($selector_siblings || $()).add($selector_children.children()).get())); + } + + if ($selector_siblings) { + $selector_siblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function (){ + var $zone = $(this); + var $drop; + var css = window.getComputedStyle(this); + var float = css.float || css.cssFloat; + + if($zone.prev('.oe_drop_zone:visible').length === 0){ + $drop = zone_template.clone(); + if (float === "left" || float === "right") { + $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30)); + } + $zone.before($drop); + } + if($zone.next('.oe_drop_zone:visible').length === 0){ + $drop = zone_template.clone(); + if (float === "left" || float === "right") { + $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30)); + } + $zone.after($drop); + } + }); + } + + var count; + do { + count = 0; + $zones = self.$editable.find('.oe_drop_zone > .oe_drop_zone').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.$editable.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 (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) { + 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($targets){ + 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 = $(qweb.render('web_editor.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); + + var timer; + $target.closest('.o_editable').on("content_changed", function (event) { + if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) { + clearTimeout(timer); + timer = setTimeout(function () { + if ($target.data('overlay')) { + self.cover_target($target.data('overlay'), $target); + } + },50); + } + }); + + 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; + } +}); + +/* ----- Editor (object who contain overlay and the option list) ---- */ + +var Editor = 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"); + + var $selector_siblings; + for (var i=0; i<self.selector_siblings.length; i++) { + if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all(); + else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all()); + } + var $selector_children; + for (var i=0; i<self.selector_children.length; i++) { + if (!$selector_children) $selector_children = self.selector_children[i].all(); + else $selector_children = $selector_children.add(self.selector_children[i].all()); + } + + self.buildingBlock.activate_insertion_zones($selector_siblings, $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; + var $dropzone = this.$target.prev(); + var prev = $dropzone.length && $dropzone[0].previousSibling; + var next = this.$target.last()[0].nextSibling; + var $parent = this.$target.parent(); + + $(".oe_drop_clone").after(this.$target); + + this.$overlay.removeClass("hidden"); + $("body").removeClass('move-important'); + $('.oe_drop_zone').droppable('destroy').remove(); + $(".oe_drop_clone, .oe_drop_to_remove").remove(); + + if (this.dropped) { + this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + + if (prev) { + this.$target.insertAfter(prev); + } else if (next) { + this.$target.insertBefore(next); + } else { + $parent.prepend(this.$target); + } + + for (var i in this.styles){ + this.styles[i].on_move(); + } + } + + 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(this.buildingBlock.templateOptions, function (val, option_id) { + if (!val.selector.is(self.$target)) { + 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 = options.registry[option] || options.Class; + var editor = self.styles[option] = new Editor(self.buildingBlock, self, self.$target, option_id); + $ul.append(editor.$el.addClass("snippet-option-" + option)); + editor.start(); + }); + + if (!this.selector_siblings.length && !this.selector_children.length) { + 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 = globalSelector.closest(this.$target.parent()); + 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; + if (!this.$target.parent().is(':o_editable')) { + this.$overlay.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove(); + } else { + 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 (event) { + event.preventDefault(); + var $clone = this.$target.clone(false); + + this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + + this.$target.after($clone); + for (var i in this.styles){ + this.styles[i].on_clone($clone); + } + this.buildingBlock.create_overlay(this.$target); + return false; + }, + + on_remove: function (event) { + event.preventDefault(); + this.on_blur(); + + this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + + 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]; + + var $editable = this.$target.closest(".o_editable"); + + // remove node and his empty + var node = this.$target.parent()[0]; + + this.$target.remove(); + this.$overlay.remove(); + + if (node && node.firstChild) { + $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1); + if (!node.firstChild.tagName && node.firstChild.textContent === " ") { + node.firstChild.parentNode.removeChild(node.firstChild); + } + } + + // clean editor if they are image or table in deleted content + $(".note-control-selection").hide(); + $('.o_table_handler').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'); + }, +}); + +var data = { + 'Class': BuildingBlock, + 'globalSelector': globalSelector, +}; +return data; + +}); diff --git a/addons/web_editor/static/src/js/snippets.options.js b/addons/web_editor/static/src/js/snippets.options.js new file mode 100644 index 00000000000..a46c00baa25 --- /dev/null +++ b/addons/web_editor/static/src/js/snippets.options.js @@ -0,0 +1,534 @@ +odoo.define('web_editor.snippets.options', function (require) { +'use strict'; + +var Class = require('web.Class'); +var ajax = require('web.ajax'); +var core = require('web.core'); +var base = require('web_editor.base'); +var editor = require('web_editor.editor'); +var widget = require('web_editor.widget'); +var animation = require('web_editor.snippets.animation'); + +var qweb = core.qweb; + +/* ----- Editor option (object link the the xml with data-js) ---- */ + +var Option = Class.extend({ + // initialisation (don't overwrite) + init: function (BuildingBlock, editor, $target, option_id) { + this.buildingBlock = BuildingBlock; + this.editor = editor; + this.$target = $target; + var option = this.buildingBlock.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(); + }, + + // helper for this.$target.find + $: function (selector) { + return this.$target(selector); + }, + + _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.reset, this)); + + this.$el + .off('mouseleave', "ul") + .on('mouseleave', "ul", _.bind(this.reset, this)); + + this.reset_methods = []; + }, + + /** + * 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.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"); + }, + + start: function () { + this.set_active(); + this.$target.on('snippet-option-reset', _.bind(this.set_active, this)); + this._bind_li_menu(); + }, + + on_focus : function () { + this._bind_li_menu(); + }, + + on_blur : function () { + }, + + on_clone: function ($clone) { + }, + + on_move: function () { + }, + + on_remove: function () { + }, + + drop_and_build_snippet: function () { + }, + + reset: function () { + 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.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 (data-only can be used) + select: function (type, $li) { + var self = this, + $methods = [], + el = $li[0]; + + if ($li.data('only') && type !== $li.data('only')) { + return; + } + + if (type==="click") { + this.reset(); + this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + } + + function filter (k) { return k !== 'oeId' && k !== 'oeModel' && k !== 'oeField' && k !== 'oeXpath' && k !== 'oeSourceId' && k !== 'only';} + 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.error("'"+self.option+"' snippet have not 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: editor.dummy +}); + +/* ----- default options ---- */ + +var media = Option.extend({ + start: function () { + var self =this; + this._super(); + animation.start(true, this.$target); + this.$target.closest(".o_editable").on("content_changed", function () { + if (!self.$target.parent().length) { + self.$target.remove(); + self.$overlay.remove(); + } + }); + }, + edition: function (type, value) { + if(type !== "click") return; + var self = this; + var _editor = new widget.MediaDialog(this.$target.closest('.o_editable'), this.$target[0]); + _editor.appendTo(document.body); + _editor.on('saved', this, function (item, old) { + self.editor.on_blur(); + self.buildingBlock.make_active(false); + if (self.$target.parent().data("oe-field") !== "image") { + setTimeout(function () { + self.buildingBlock.make_active($(item)); + },0); + } + }); + }, + on_focus : function () { + var self = this; + var $parent = this.$target.parent(); + + if ($parent.data("oe-field") === "image" && $parent.hasClass('o_editable')) { + this.$overlay.addClass("hidden"); + self.edition('click', null); + self.buildingBlock.make_active(false); + } + } +}); + +var colorpicker = Option.extend({ + start: function () { + var self = this; + var res = this._super(); + + if (!this.$el.find('.colorpicker').length) { + this.$el.find('li').append( qweb.render('web_editor.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(".colorpicker button").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(".colorpicker td"); + var $colors = this.$el.find(".colorpicker button"); + $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"); + }); + } +}); + +var background = Option.extend({ + start: function ($change_target) { + this.$bg = $change_target || this.$target; + this._super(); + var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, ""); + if (this.$bg.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.$bg.attr("style", 'background-image: url(' + value + ')' + (this.$bg.attr("style") || '').replace(/background-image:[^;]+/, '') ); + this.$bg.addClass("oe_img_bg"); + } else { + this.$bg.css("background-image", ""); + this.$bg.removeClass("oe_img_bg").removeClass("oe_custom_bg"); + } + }, + 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.$bg); + + var _editor = new widget.MediaDialog(null, $image[0]); + _editor.appendTo(document.body); + _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); + + _editor.on('saved', self, function () { + var value = $image.attr("src"); + $image.remove(); + self.$el.find('li[data-choose_image]').data("background", value).attr("data-background", value); + self.background(type, value,$li); + self.$bg.addClass('oe_custom_bg'); + self.$bg.trigger("snippet-option-change", [self]); + self.set_active(); + }); + _editor.on('cancel', self, function () { + $image.remove(); + }); + }, + set_active: function () { + var self = this; + var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, ""); + this._super(); + + 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[data-background].active').size()) { + this.$el.find('li[data-background=""]:not([data-choose_image])').addClass("active"); + } else { + this.$el.find('li[data-background=""]:not([data-choose_image])').removeClass("active"); + } + } +}); + +/* t-field options */ + +var many2one = Option.extend({ + start: function () { + var self = this; + + this.Model = this.$target.data('oe-many2one-model'); + this.ID = +this.$target.data('oe-many2one-id'); + + // create search button and bind search bar + this.$btn = $(qweb.render("web_editor.many2one.button")) + .insertAfter(this.$overlay.find('.oe_options')); + + this.$ul = this.$btn.find("ul"); + this.$search = this.$ul.find('li:first'); + this.$search.find('input').on('mousedown click mouseup keyup keydown', function (e) { + e.stopPropagation(); + }); + + // move menu item + setTimeout(function () { + if (self.$overlay.find('.oe_options').hasClass('hidden')) { + self.$btn.css('height', '0').find('> a').addClass('hidden'); + self.$ul.show().css({ + 'top': '-24px', 'margin': '0', 'padding': '2px 0', 'position': 'relative' + }); + } else { + self.$btn.find('a').on('click', function (e) { + self.clear(); + }); + } + },0); + + // bind search input + this.$search.find('input') + .focus() + .on('keyup', function(e) { + self.find_existing($(this).val()); + }); + + // bind result + this.$ul.on('click', "li:not(:first) a", function (e) { + self.select_record(this); + }); + }, + + on_focus: function () { + this.$target.attr('contentEditable', 'false'); + this.clear(); + this._super(); + }, + + clear: function () { + var self = this; + this.$search.siblings().remove(); + self.$search.find('input').val(""); + setTimeout(function () { + self.$search.find('input').focus(); + },0); + }, + + find_existing: function (name) { + var self = this; + var domain = []; + if (!name || !name.length) { + self.$search.siblings().remove(); + return; + } + if (isNaN(+name)) { + if (this.Model === "res.partner") { + domain.push(['name', 'ilike', name]); + } else { + domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]); + } + } else { + domain.push(['id', '=', name]); + } + + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: this.Model, + method: 'search_read', + args: [domain, this.Model === "res.partner" ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']], + kwargs: { + order: 'name DESC', + limit: 5, + context: base.get_context(), + } + }).then(function (result){ + self.$search.siblings().remove(); + self.$search.after(qweb.render("web_editor.many2one.search",{contacts:result})); + }); + }, + + get_contact_rendering: function (options) { + return ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'ir.qweb.field.contact', + method: 'get_record_to_html', + args: [[this.ID]], + kwargs: { + options: options, + context: base.get_context(), + } + }); + }, + + select_record: function (li) { + var self = this; + + this.ID = +$(li).data("id"); + this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID); + + this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + + if (self.$target.data('oe-type') === "contact") { + $('[data-oe-contact-options]') + .filter('[data-oe-model="'+self.$target.data('oe-model')+'"]') + .filter('[data-oe-id="'+self.$target.data('oe-id')+'"]') + .filter('[data-oe-field="'+self.$target.data('oe-field')+'"]') + .filter('[data-oe-contact-options!="'+self.$target.data('oe-contact-options')+'"]') + .add(self.$target) + .attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID) + .each(function () { + var $node = $(this); + self.get_contact_rendering($node.data('oe-contact-options')) + .then(function (html){ + $node.html(html); + }); + }); + } else { + self.$target.html($(li).data("name")); + } + + setTimeout(function () { + self.buildingBlock.make_active(false); + },0); + } +}); + +/* end*/ + +return { + 'registry': { + 'media': media, + 'colorpicker': colorpicker, + 'background': background, + 'many2one': many2one, + }, + 'Class': Option +}; + +}); diff --git a/addons/website/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js index 5303837b26a..56dd3249a6c 100644 --- a/addons/website/static/src/js/summernote.js +++ b/addons/web_editor/static/src/js/summernote.js @@ -1,7 +1,8 @@ -odoo.define('website.summernote', function (require) { +odoo.define('web_editor.summernote', function (require) { 'use strict'; var core = require('web.core'); +require('summernote/summernote'); // wait that summernote is loaded var _t = core._t; @@ -502,7 +503,7 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) { if (sc.tagName) { sc = sc.childNodes[so] || dom.firstChild(ec); so = 0; - if (!dom.hasContentBefore(sc) && towrite) { + if (!dom.hasContentBefore(sc)) { sc.parentNode.insertBefore(document.createTextNode('\u00A0'), sc); } } @@ -557,19 +558,18 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) { sc = before ? dom.lastChild(before) : dom.firstChild(after); so = sc.textContent.length; - if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) { - sc = $("<br/>")[0]; - so = 0; - node.appendChild(br); - } else if (!ancestor.children.length && !ancestor.textContent.match(/\S|\u00A0/)) { - sc = $("<br/>")[0]; - so = 0; - $(ancestor).prepend(sc); - } else if (before) { + if (before) { var text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0'); so -= sc.textContent.length - text.length; sc.textContent = text; } + if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) { + var br = $("<br/>")[0]; + node.appendChild(br); + sc = br; + so = 0; + } + dom.autoMerge(sc, false); } else { @@ -690,9 +690,9 @@ dom.listBetween = function (sc, ec) { }); return list.unique(nodes); }; -dom.isNotBreakable = function (node, sc, so, ec, eo) { +dom.isNotBreakable = function (node) { // avoid triple click => crappy dom - return node === ec && !dom.isText(ec) && !dom.isBR(dom.firstChild(ec)); + return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node)); }; dom.isContentEditable = function (node) { @@ -705,7 +705,7 @@ dom.isFont = function (node) { (nodeName === "SPAN" && ( node.className.match(/(^|\s)fa(\s|$)/i) || node.className.match(/(^|\s)(text|bg)-/i) || - (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color|font-size):/i)))) ); + (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color):/i)))) ); }; dom.isVisibleText = function (textNode) { return !!textNode.textContent.match(/\S|\u00A0/); @@ -1025,13 +1025,17 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) { var node = dom.node(r.sc); var last = node; - while (node && dom.isSplitable(node) && !dom.isList(node) && !dom.isEditable(node)) { + while (node && dom.isSplitable(node) && !dom.isList(node)) { last = node; node = node.parentNode; } if (last === node && !dom.isBR(node)) { node = r.insertNode(br, true); + if (isFormatNode(last.firstChild) && $(last).closest(options.styleTags.join(',')).length) { + dom.moveContent(last.firstChild, last); + last.removeChild(last.firstChild); + } do { node = dom.hasContentAfter(node); } while (node && dom.isBR(node)); @@ -1159,6 +1163,8 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) { } r = r.deleteContents(); r.select(); + event.preventDefault(); + return false; } var target = r.ec; @@ -1283,6 +1289,8 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) { } r = r.deleteContents(); r.select(); + event.preventDefault(); + return false; } var target = r.sc; @@ -1726,19 +1734,12 @@ $.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTa $.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, value) { var $editable = layoutInfo.editable(); $editable.data('NoteHistory').recordUndo($editable); - var node = range.create().sc.parentNode; document.execCommand('removeFormat'); - document.execCommand('removeFormat'); // 2 times because some browser don't clean all in one time + document.execCommand('removeFormat'); var r = range.create(); r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true); - r = range.create(r.sc, r.so, r.ec, r.eo); - r.select(); - - $.summernote.pluginEvents.justify(event, editor, layoutInfo, ""); - - r.clean().select(); - + range.create(r.sc, r.so, r.ec, r.eo).select(); event.preventDefault(); return false; }; @@ -1770,9 +1771,6 @@ eventHandler.toolbar.button.updateRecentColor = function (elBtn, sEvent, sValue) options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62]; $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) { - var $editable = layoutInfo.editable(); - $editable.data('NoteHistory').recordUndo($editable); - var rng = range.create(); var startPoint = rng.getStartPoint(); var endPoint = rng.getEndPoint(); @@ -1962,7 +1960,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) { className2 = font.getAttribute('class'); style2 = font.getAttribute('style'); - if (node !== font && className == className2 && style == style2 && (node.parentNode === font.parentNode || node.parentNode === font)) { + if (node !== font && className == className2 && style == style2) { remove(node, font); nodes.splice(i,1); i--; @@ -1975,8 +1973,6 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color }; $.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value) { var $editable = layoutInfo.editable(); - $editable.data('NoteHistory').recordUndo($editable); - event.preventDefault(); $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, value); editor.afterCommand($editable); @@ -2010,60 +2006,6 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC } $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, backColor, null); editor.afterCommand($editable); - }; - - $.summernote.pluginEvents.justify = function (event, editor, layoutInfo, align) { - var rng = range.create(); - var startPoint = rng.getStartPoint(); - var endPoint = rng.getEndPoint(); - - // get list of nodes to change - var nodes = []; - nodes.push(dom.ancestor(startPoint.node, dom.isPara) || dom.ancestor(startPoint.node, dom.isCell)); - nodes.push(dom.ancestor(endPoint.node, dom.isPara) || dom.ancestor(endPoint.node, dom.isCell)); - dom.walkPoint(startPoint, endPoint, function (point) { - var node = point.node; - if (dom.isPara(node) || dom.isCell(node)) { - nodes.push(point.node); - } - }); - nodes = list.unique(nodes); - - $(nodes).not(":has(div,p,li,h1,h2,h3,h4,h5,h6,h7,td,th)").each(function () { - var $node = $(this).css('text-align', ''); - var css = window.getComputedStyle(this); - var textAlign = css.textAlign; - if (textAlign === "start") { - textAlign = css.direction === "ltr" ? "left" : "right"; - } - if(textAlign !== align) { - $node.css('text-align', align); - } - }).find("*").css('text-align', ""); - }; - $.summernote.pluginEvents.justifyLeft = function (event, editor, layoutInfo) { - var $editable = layoutInfo.editable(); - $editable.data('NoteHistory').recordUndo($editable); - $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'left'); - editor.afterCommand($editable); - }; - $.summernote.pluginEvents.justifyCenter = function (event, editor, layoutInfo) { - var $editable = layoutInfo.editable(); - $editable.data('NoteHistory').recordUndo($editable); - $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'center'); - editor.afterCommand($editable); - }; - $.summernote.pluginEvents.justifyRight = function (event, editor, layoutInfo) { - var $editable = layoutInfo.editable(); - $editable.data('NoteHistory').recordUndo($editable); - $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'right'); - editor.afterCommand($editable); - }; - $.summernote.pluginEvents.justifyFull = function (event, editor, layoutInfo) { - var $editable = layoutInfo.editable(); - $editable.data('NoteHistory').recordUndo($editable); - $.summernote.pluginEvents.justify(event, editor, layoutInfo, 'justify'); - editor.afterCommand($editable); }; ////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2245,4 +2187,6 @@ eventHandler.dettach = function (oLayoutInfo, options) { $editable.off("scroll", summernote_table_scroll); }; +return $.summernote; + }); diff --git a/addons/website/static/src/js/website.tour.rte.js b/addons/web_editor/static/src/js/tour.rte.js index b068c11f604..c257176fd6f 100644 --- a/addons/website/static/src/js/website.tour.rte.js +++ b/addons/web_editor/static/src/js/tour.rte.js @@ -1,11 +1,15 @@ -odoo.define('website.tour.rte', function (require) { +odoo.define('web_editor.tour', function (require) { 'use strict'; var core = require('web.core'); var Tour = require('web.Tour'); +var base = require('web_editor.base'); +var snippet_editor = require('web_editor.snippet.editor'); var _t = core._t; +base.ready().done(function () { + var click_event = function(el, type) { var evt = document.createEvent("MouseEvents"); evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); @@ -13,47 +17,36 @@ var click_event = function(el, type) { }; Tour.register({ - id: 'website_rte', - name: "Test website RTE", - path: '/page/homepage', + id: 'rte', + name: "Test RTE", mode: 'test', + path: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D', steps: [ { - element: 'button[data-action=edit]', - title: "Edit this page", - wait: 250 - }, - { - snippet: '#snippet_structure .oe_snippet:has(.s_text_image)', - title: "Drag & Drop a Text-Image Block", - }, - { - element: '.oe_overlay_options:visible .oe_options a:first', - title: "Customize", - onload: function () { - $(".oe_overlay_options:visible .snippet-option-background > ul").show(); - } - }, - { - element: '.oe_overlay_options:visible .snippet-option-background > ul li[data-background*="quote"]:first a', - title: "Chose a background image", - }, - { title: "Change html for this test", - waitFor: '#wrapwrap > main > div > section:first[style*="background-image"]', - element: '#wrapwrap > main > div > section .row > div:first', onload: function () { var $el = $(this.element); - var html = '<h1 id="text_title_id">Batnae municipium in Anthemusia</h1> '+ - '\n <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>'+ - '\n <p> Quam <img style="width: 25%" src="/website/static/src/img/text_image.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>'+ - '\n <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+ - '<p>Harum trium sententiarum nulli prorsus assentior.</p>'; - $el.html(html); + var html = '\n'+ + '<section>\n'+ + ' <div class="container">\n'+ + ' <div class="row">\n'+ + ' <div class="col-md-6 mt16">\n'+ + '<h1 id="text_title_id">Batnae municipium in Anthemusia</h1> \n'+ + ' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+ + ' <p> Quam <img style="width: 25%" src="/web_editor/static/src/img/drag_here.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+ + ' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+ + '<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+ + ' </div>\n'+ + ' <div class="col-md-6 mt16">\n'+ + ' <img class="img img-responsive shadow mb16" src="/web_editor/static/src/img/drag_here.png" alt="Odoo text and image block">\n'+ + ' </div>\n'+ + ' </div>\n'+ + '</section>\n'; + $("#editable_area").html(html); } }, { - element: '#wrapwrap > main > div > section .row > div:first', + element: '#editable_area > section .row > div:first', title: "simulate triple click and change text bg-color", onload: function () { var $el = $(this.element); @@ -63,16 +56,16 @@ Tour.register({ } }, { - waitFor: '#wrapwrap > main > div > section .row > div:first', + waitFor: '#editable_area > section .row > div:first', element: '.note-popover button[data-event="color"]:visible', title: "change text bg-color after triple click", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1 font', + waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1 font', element: '.note-color button.dropdown-toggle:visible', title: "change selection to change text color", onload: function () { - var $el = $('#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1 font'); + var $el = $('#editable_area > section .row > div:first:not(:has(p font)) h1 font'); $.summernote.core.range.create($el[0].firstChild, 5, $el[0].firstChild, 10).select(); click_event($el[0], 'mouseup'); } @@ -82,11 +75,11 @@ Tour.register({ title: "change text color", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first h1 font:eq(2)', + waitFor: '#editable_area > section .row > div:first h1 font:eq(2)', element: '.note-color button.dropdown-toggle:visible', title: "change selection to change text bg-color again", onload: function () { - var $el = $('#wrapwrap > main > div > section .row > div:first h1 font:eq(2)'); + var $el = $('#editable_area > section .row > div:first h1 font:eq(2)'); $.summernote.core.range.create($el.prev()[0].firstChild, 3, $el[0].firstChild, 10).select(); click_event($el.prev()[0], 'mouseup'); } @@ -96,11 +89,11 @@ Tour.register({ title: "change text backColor again", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first h1 font:eq(4)', + waitFor: '#editable_area > section .row > div:first h1 font:eq(4)', element: '.note-color button.dropdown-toggle:visible', title: "change selection (h1 and p) to change text color with class", onload: function () { - var $el = $('#wrapwrap > main > div > section .row > div:first h1 font:eq(4)'); + var $el = $('#editable_area > section .row > div:first h1 font:eq(4)'); $.summernote.core.range.create($el.prev()[0].firstChild, 3, $el.parent("h1").next("p")[0].firstChild, 30).select(); click_event($el.prev()[0], 'mouseup'); } @@ -110,13 +103,13 @@ Tour.register({ title: "change text foreColor again", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first p font', + waitFor: '#editable_area > section .row > div:first p font', element: '.o_editable.note-editable.o_dirty', title: "delete selection", keydown: 46 // delete }, { - waitFor: '#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1', + waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1', element: '.o_editable.note-editable.o_dirty', title: "clean and delete (backspace) an other selection", onload: function () { @@ -128,7 +121,7 @@ Tour.register({ keydown: 8 // backspace }, { - waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1', + waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1', element: '.o_editable.note-editable.o_dirty', title: "delete an other selection", onload: function () { @@ -139,24 +132,24 @@ Tour.register({ keydown: 46 }, { - waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(Bat) )', - element: '.note-image-popover button[data-event="undo"]', + waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )', + element: '.note-air-popover button[data-event="undo"]', title: "undo", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(i) )', + waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )', element: '.note-air-popover button[data-event="undo"]', title: "undo adain", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first:not(:has(p font)) h1', + waitFor: '#editable_area > section .row > div:first:not(:has(p font)) h1', element: '.o_editable.note-editable.o_dirty', title: "delete (backspace) after undo", keydown: 8 // backspace }, { - waitFor: '#wrapwrap > main > div > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )', - element: '#wrapwrap > main > div > section .row > div:first img[style*="25%"]', + waitFor: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )', + element: '#editable_area > section .row > div:first img[style*="25%"]', title: "click on image", }, { @@ -164,7 +157,7 @@ Tour.register({ title: "Click on resize half", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first img[style*="50%"]', + waitFor: '#editable_area > section .row > div:first img[style*="50%"]', element: '.note-image-popover:visible button[data-event="showImageDialog"]', title: "Click on edit picture", }, @@ -192,7 +185,7 @@ Tour.register({ title: "click on float right", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first span.fa.pull-right', + waitFor: '#editable_area > section .row > div:first span.fa.pull-right', element: '.note-image-popover:visible button[data-event="showLinkDialog"]', title: "click on create link", }, @@ -205,39 +198,45 @@ Tour.register({ element: '.modal .dropdown ul label.btn-success', title: "choose success style", }, - { - waitFor: '.modal a#link-preview.btn:containsRegex(/^<span [^>]+><\\/span>$/)', - element: '.modal .select2-container.url-source a.select2-choice', - title: "click to choose an internal page", - }, - { - element: '.select2-drop:visible .select2-search input', - title: "search 'contact'", - sampleText: "contact", - }, - { - element: '.select2-drop:visible .select2-results .select2-result div:contains(/page/)', - title: "select /page/contactus", - }, - { - waitNot: '.select2-drop:visible', + // { + // waitFor: '.modal a#link-preview.btn:containsRegex(/^<span [^>]+><\\/span>$/)', + // element: '.modal .select2-container.url-source a.select2-choice', + // title: "click to choose an internal page", + // }, + // { + // element: '.select2-drop:visible .select2-search input', + // title: "search 'contact'", + // sampleText: "contact", + // }, + // { + // element: '.select2-drop:visible .select2-results .select2-result div:contains(/page/)', + // title: "select /page/contactus", + // }, + { + // waitNot: '.select2-drop:visible', element: '#link-text', title: "change text label", sampleText: "ABC[IMG] DEF", }, { + waitFor: '.modal a#link-preview.btn', + element: '#link-external', + sampleText: "http://www.odoo.com", + title: "insert a link url", + }, + { waitFor: '.modal a#link-preview.btn:containsRegex(/^ABC<span [^>]+><\\/span> DEF$/)', element: '.modal button.save', title: "save link", }, { waitNot: '#link-preview', - waitFor: 'a.btn[href^="/"]:has(span.fa.fa-3x.pull-right)', - element: '#wrapwrap > main > div > section .row > div:last img', + waitFor: 'a.btn[href^="http://"]:has(span.fa.fa-3x.pull-right)', + element: '#editable_area > section .row > div:last img', title: "click on other picture", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first span.fa.pull-right', + waitFor: '#editable_area > section .row > div:first span.fa.pull-right', element: '.note-image-popover:visible button[data-event="showLinkDialog"]', title: "click on create link again", }, @@ -263,8 +262,8 @@ Tour.register({ }, { waitNot: '#link-preview', - waitFor: '#wrapwrap > main > div > section .row > div:eq(1) > a > img', - element: '#wrapwrap > main > div > section .row > div:first p:eq(2)', + waitFor: '#editable_area > section .row > div:eq(1) > a > img', + element: '#editable_area > section .row > div:first p:eq(2)', title: "triple enter then double backspace", keydown: [66, 13, 66, 13, 13, 8, 8], // B enter B enter enter backspace backspace onload: function () { @@ -274,11 +273,11 @@ Tour.register({ }, }, { - waitNot: '#wrapwrap > main > div > section .row > div:first p:eq(4), #wrapwrap > main > div > section .row > div:eq(3)', - waitFor: '#wrapwrap > main > div > section .row > div:first p:eq(3)', + waitNot: '#editable_area > section .row > div:first p:eq(4), #editable_area > section .row > div:eq(3)', + waitFor: '#editable_area > section .row > div:first p:eq(3)', title: "add ul content", onload: function () { - var $el = $('#wrapwrap > main > div > section .row > div:first'); + var $el = $('#editable_area > section .row > div:first'); var html = ' <ul> '+ '\n <li> <p>Batnae municipium. </p></li>'+ '\n <li> Seleucia praeter.</li>'+ @@ -291,7 +290,7 @@ Tour.register({ } }, { - waitFor: '#wrapwrap > main > div > section .row > div:first ul li p:first', + waitFor: '#editable_area > section .row > div:first ul li p:first', element: '.note-air-popover .note-style button.dropdown-toggle', title: "click on style dropdown", }, @@ -299,24 +298,24 @@ Tour.register({ element: '.note-air-popover .note-style ul:visible a[data-value="h3"]', title: "select h3", onload: function () { - var node = $('#wrapwrap > main > div > section .row > div:first ul li p:last')[0].firstChild; + var node = $('#editable_area > section .row > div:first ul li p:last')[0].firstChild; $.summernote.core.range.create(node, 0).select(); click_event(node, 'mouseup'); } }, { - element: '#wrapwrap > main > div > section .row > div:first > ul > li > h3', + element: '#editable_area > section .row > div:first > ul > li > h3', title: "double tabulation", keydown: [9, 9] // tabulation }, { - waitFor: '#wrapwrap > main > div > section .row > div:first ul > li > ul > li > ul > li > h3', + waitFor: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3', element: '.note-air-popover button[data-event="insertOrderedList"]', title: "click on order list", }, { - waitFor: '#wrapwrap > main > div > section .row > div:first ul > li > ul > li > ol > li > h3', - element: '#wrapwrap > main > div > section .row > div:first ul li > p:last', + waitFor: '#editable_area > section .row > div:first ul > li > ul > li > ol > li > h3', + element: '#editable_area > section .row > div:first ul li > p:last', title: "enter in ul", keydown: [66, 13], // enter onload: function () { @@ -326,15 +325,74 @@ Tour.register({ } }, { - element: '#wrapwrap > main > div > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)', + element: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)', title: "backspace in list", keydown: 8 }, { - waitFor: '#wrapwrap > main > div > section .row > div:first ul li p:eq(1)', + waitFor: '#editable_area > section .row > div:first ul li p:eq(1)', title: "end", }, ] }); + +Tour.register({ + id: 'rte_inline', + name: "Test RTE Inline", + mode: 'test', + path: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D', + steps: [ + { + title: "Change html for this test", + onload: function () { + var $el = $(this.element); + var html = '\n'+ + '<div>\n'+ + ' <table cellspacing="0" cellpadding="0" width="100%">\n'+ + ' <tbody>\n'+ + ' <tr>\n'+ + ' <td valign="center" width="270">\n'+ + ' <img src="/logo.png" alt="Your Logo" class="img-circle img-thumbnail">\n'+ + ' </td>\n'+ + ' <td valign="center" width="270">\n'+ + ' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+ + ' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square pull-right"></span>\n'+ + ' </td>\n'+ + ' </tr>\n'+ + ' </tbody>\n'+ + ' </table>\n'+ + '</div>'; + $("#editable_area").html(html); + } + }, + { + title: "call clean for save", + element: '#wrapwrap table', + onload: function () { + snippet_editor.instance.clean_for_save(); + } + }, + { + waitFor: '#wrapwrap img:first[style*="border-bottom-left-radius:50%"][style*="border-top-width"][style*="padding-top"]', + title: "check the image style", + }, + { + waitFor: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(66,139,202)/27"]', + title: "check the font image src", + }, + { + waitFor: '#wrapwrap img:eq(1)[style*="height:2em"]', + title: "check the font class to css", + }, + { + waitFor: '#wrapwrap img:eq(2)[style*="float:right"]', + title: "check the second font class to css", + }, + ] +}); + + +}); + }); diff --git a/addons/web_editor/static/src/js/transcoder.js b/addons/web_editor/static/src/js/transcoder.js new file mode 100644 index 00000000000..c7bcd14107f --- /dev/null +++ b/addons/web_editor/static/src/js/transcoder.js @@ -0,0 +1,172 @@ +odoo.define('web_editor.transcoder', function (require) { +'use strict'; + +var widget = require('web_editor.widget'); + +var cache = {}; +var rulesCache = []; +var getMatchedCSSRules = function (a) { + if(cache[a.tagName + "." +a.className]) { + return cache[a.tagName + "." +a.className]; + } + if (!rulesCache.length) { + var sheets = document.styleSheets; + for(var i = sheets.length-1; i >= 0 ; i--) { + var rules = sheets[i].rules || sheets[i].cssRules; + if (rules) { + for(var r = rules.length-1; r >= 0; r--) { + var selectorText = rules[r].selectorText; + if (selectorText && + rules[r].cssText && + selectorText.indexOf(".") !== -1 && + selectorText.indexOf(".note-") === -1 && + + selectorText.indexOf(":hover") === -1 && + selectorText.indexOf(":before") === -1 && + selectorText.indexOf(":after") === -1 && + selectorText.indexOf(":active") === -1 && + selectorText.indexOf(":link") === -1 && + selectorText.indexOf("::") === -1) { + var st = selectorText.split(/\s*,\s*/); + for (var k=0; k<st.length; k++) { + rulesCache.push({ 'selector': st[k], 'style': rules[r].style }); + } + } + } + } + } + rulesCache.reverse(); + } + + var css = []; + a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector; + for(var r = 0; r < rulesCache.length; r++) { + if (a.matches(rulesCache[r].selector)) { + var style = rulesCache[r].style; + if (style.parentRule) { + var style_obj = {}; + for (var k=0, len=style.length; k<len; k++) { + style_obj[style[k]] = style[style[k]]; + } + rulesCache[r].style = style = style_obj; + } + css.push([rulesCache[r].selector, style]); + } + } + + function specificity (selector) { + // http://www.w3.org/TR/css3-selectors/#specificity + var a = 0; + selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ""; }); + var b = 0; + selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ""; }); + var c = 0; + selector.replace(/(\s+|:+)[a-z0-9_-]+/gi, function (a) { if(a.indexOf(':not(')===-1) c++; return ""; }); + return a*100 + b*10 + c; + } + css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); }); + + var style = {}; + _.each(css, function (v,k) { + _.each(v[1], function (v,k) { + if (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1) { + style[k] = v; + } + }); + }); + return cache[a.tagName + "." +a.className] = style; +}; + +// convert font awsome into image +var font_to_img = function ($editable) { + $(".fa", $editable).each(function () { + var $font = $(this); + var content; + _.find(widget.fontIcons, function (font) { + return _.find(widget.getCssSelectors(font.parser), function (css) { + if ($font.is(css[0].replace(/::?before$/, ''))) { + content = css[1].match(/content:\s*['"]?(.)['"]?/)[1]; + return true; + } + }); + }); + if (content) { + var size = parseInt(parseFloat($font.css("font-size"))/parseFloat($font.parent().css("font-size")),10); + var color = $font.css("color").replace(/\s/g, ''); + var src = _.str.sprintf('/web_editor/font_to_img/%s/%s/'+$font.width(), window.encodeURI(content), window.encodeURI(color)); + var style = $font.attr("style"); + style = (style ? style.replace(/\s/g, '').replace(/(^|;)height:[^;]*/, '$1').replace(/(^|;)font-size:[^;]*/, '$1') : "") + "height:"+size+"em;"; + var $img = $("<img/>").attr("src", src).attr("data-class", $font.attr("class")).attr("style", style); + $font.replaceWith($img); + } else { + $font.remove(); + } + }); +}; +// convert image into font awsome +var img_to_font = function ($editable) { + $("img[src*='/web_editor/font_to_img/']", $editable).each(function () { + var $img = $(this); + var $font = $("<span/>").attr("class", $img.data("class")).attr("style", $img.attr("style")).css("height", ""); + $img.replaceWith($font); + }); +}; + +// convert class into inline style to send by mail +var class_to_style = function ($editable) { + if (!rulesCache.length) { + getMatchedCSSRules($editable[0]); + } + var selector = _.map(rulesCache, function (a) { return a.selector;}).join(","); + $editable.find(selector).each(function () { + var $target = $(this); + var css = getMatchedCSSRules(this); + var style = $target.attr("style") || ""; + _.each(css, function (v,k) { + if (style.indexOf(k) === -1) { + style = k+":"+v+";"+style; + } + }); + $target.attr("style", style); + }); +}; +// convert style into inline class from mail +var style_to_class = function ($editable) { + getMatchedCSSRules($editable[0]); + var classes = []; + $editable.find("[class]").each(function () { + classes = classes.concat(this.className.split(/\s+/)); + }); + + var maybe_selector = _.filter(rulesCache, function (a) { return !!_.find(classes, function (b) { return a.selector.indexOf("."+b) !== -1; } ); }); + var selector = _.map(maybe_selector, function (a) { return a.selector;}).join(","); + + var $c = $('<span/>').appendTo("body"); + + $editable.find(selector).each(function () { + var $target = $(this); + var css = getMatchedCSSRules(this); + var style = ""; + _.each(css, function (v,k) { + if (style.indexOf(k) === -1) { + style = k+":"+v+";"+style; + } + }); + css = $c.attr("style", style).attr("style").split(/\s*;\s*/); + style = $target.attr("style") || ""; + _.each(css, function (v) { + style = style.replace(v, ''); + }); + $target.attr("style", style.replace(/;+(\s;)*/g, ';').replace(/^;/g, '')); + }); + $c.remove(); +}; + +return { + 'font_to_img': font_to_img, + 'img_to_font': img_to_font, + 'class_to_style': class_to_style, + 'style_to_class': style_to_class, +}; + +});
\ No newline at end of file diff --git a/addons/web_editor/static/src/js/translator.js b/addons/web_editor/static/src/js/translator.js new file mode 100644 index 00000000000..190706a111c --- /dev/null +++ b/addons/web_editor/static/src/js/translator.js @@ -0,0 +1,214 @@ +odoo.define('web_editor.translate', function (require) { +'use strict'; + +var core = require('web.core'); +var Model = require('web.Model'); +var ajax = require('web.ajax'); +var Class = require('web.Class'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var rte = require('web_editor.rte'); + +var qweb = core.qweb; + +ajax.loadXML('/web_editor/static/src/xml/translator.xml', qweb); + + +var translatable = !!$('html').data('translatable'); +var edit_translations = !!$('html').data('edit_translations'); + +function unbind_click(event) { + if (event.ctrlKey || !$(event.target).is(':o_editable')) { + return; + } + event.preventDefault(); + event.stopPropagation(); +} + + +var RTE_Translate = rte.Class.extend({ + saveElement: function ($el, context) { + // remove multi edition + if ($el.data('oe-translation-id')) { + var key = 'translation:'+$el.data('oe-translation-id'); + if (this.__saved[key]) return true; + this.__saved[key] = true; + + return ajax.jsonRpc('/web/dataset/call', 'call', { + model: 'ir.translation', + method: 'write', + args: [ + [+$el.data('oe-translation-id')], + {value: $el.html(), state: 'translated'}, + context || base.get_context() + ], + }); + } + + return this._super.apply(this, arguments); + }, +}); + +var Translate = Widget.extend({ + events: { + 'click [data-action="save"]': 'save_and_reload', + 'click [data-action="cancel"]': 'cancel', + }, + template: 'web_editor.translator', + init: function (parent, $target, lang) { + this.parent = parent; + this.ir_translation = new Model('ir.translation'); + this.lang = lang || base.get_context().lang; + this.setTarget($target); + this._super(); + + this.rte = new RTE_Translate(this, this.config); + this.rte.on('change', this, this.onChange); + }, + start: function () { + this._super(); + return this.edit(); + }, + setTarget: function ($target) { + this.$target = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]'); + }, + find: function (selector) { + return selector ? this.$target.find(selector).addBack().filter(selector) : this.$target; + }, + edit: function () { + var flag = false; + window.onbeforeunload = function(event) { + if ($('.o_editable.o_dirty').length && !flag) { + flag = true; + setTimeout(function () {flag=false;},0); + return _t('This document is not saved!'); + } + }; + this.$target.addClass("o_editable"); + this.rte.start(); + this.translations = []; + this.markTranslatableNodes(); + this.onTranslateReady(); + }, + onTranslateReady: function () { + this.$el.show(); + this.trigger("edit"); + }, + onChange: function (node) { + var $node = $(node); + var trans = this.getTranlationObject($node[0]); + $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' ')); + }, + getTranlationObject: function (node) { + var $node = $(node); + var id = +$node.data('oe-translation-id'); + if (!id) { + id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field'); + } + var trans = _.find(this.translations, function (trans) { + return trans.id === id; + }); + if (!trans) { + this.translations.push(trans = {'id': id}); + } + return trans; + }, + markTranslatableNodes: function (node) { + var self = this; + this.$target.each(function () { + var $node = $(this); + var trans = self.getTranlationObject(this); + trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' '); + }); + this.$target.parent().on('click', unbind_click); + console.info('Click on CTRL when you click in an translatable area to have the default behavior'); + }, + unarkTranslatableNode: function () { + this.$target.removeClass('o_editable').removeAttr('contentEditable'); + this.$target.parent().off('click', this.__unbind_click); + this.$target_attr.off('mousedown click mouseup', this.__translate_attribute); + }, + save_and_reload: function () { + return this.save().then(function () { + window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, ''); + }); + }, + save: function () { + var context = base.get_context(); + context.lang = this.lang; + return this.rte.save(context); + }, + cancel: function () { + var self = this; + this.rte.cancel(); + this.$target.each(function () { + $(this).html(self.getTranlationObject(this).value); + }); + this.unarkTranslatableNode(); + this.trigger("cancel"); + this.$el.hide(); + }, + destroy: function () { + this.cancel(); + this.$el.remove(); + this._super(); + }, + + config: function ($editable) { + if ($editable.data('oe-model')) { + return { + 'airMode' : true, + 'focus': false, + 'airPopover': [ + ['history', ['undo', 'redo']], + ], + 'styleWithSpan': false, + 'inlinemedia' : ['p'], + 'lang': "odoo", + 'onChange': function (html, $editable) { + $editable.trigger("content_changed"); + } + }; + } + return { + 'airMode' : true, + 'focus': false, + 'airPopover': [ + ['font', ['bold', 'italic', 'underline', 'clear']], + ['fontsize', ['fontsize']], + ['color', ['color']], + ['history', ['undo', 'redo']], + ], + 'styleWithSpan': false, + 'inlinemedia' : ['p'], + 'lang': "odoo", + 'onChange': function (html, $editable) { + $editable.trigger("content_changed"); + } + }; + } +}); + + +if (edit_translations) { + base.ready().then(function () { + data.instance = new Translate(this, $('#wrapwrap')); + data.instance.prependTo(document.body); + + $('a[href*=edit_translations]').each(function () { + this.href = this.href.replace(/[$?]edit_translations[^&?]+/, ''); + }); + $('form[action*=edit_translations]').each(function () { + this.action = this.action.replace(/[$?]edit_translations[^&?]+/, ''); + }); + }); +} + +var data = { + 'translatable': translatable, + 'edit_translations': edit_translations, + 'Class': Translate, +}; +return data; + +}); diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js new file mode 100644 index 00000000000..6fab80ad802 --- /dev/null +++ b/addons/web_editor/static/src/js/widgets.js @@ -0,0 +1,1069 @@ +odoo.define('web_editor.widget', function (require) { +'use strict'; + +var core = require('web.core'); +var ajax = require('web.ajax'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var rte = require('web_editor.rte'); + +var QWeb = core.qweb; +var range = $.summernote.core.range; +var dom = $.summernote.core.dom; + +////////////////////////////////////////////////////////////////////////////////////////////////////////// + +var Dialog = Widget.extend({ + events: { + 'hidden.bs.modal': 'destroy', + 'click button.save': 'save', + 'click button[data-dismiss="modal"]': 'cancel', + }, + init: function () { + this._super(); + }, + start: function () { + var sup = this._super(); + this.$el.modal({backdrop: 'static'}); + this.$('input:first').focus(); + return sup; + }, + save: function () { + this.close(); + this.trigger("saved"); + }, + cancel: function () { + this.trigger("cancel"); + }, + close: function () { + this.$el.modal('hide'); + }, +}); + +/** + * alt widget. Lets users change a alt & title on a media + */ +var alt = Dialog.extend({ + template: 'web_editor.dialog.alt', + init: function ($editable, media) { + this.$editable = $editable; + this.media = media; + this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"'); + this.title = ($(this.media).attr('title') || "").replace(/"/g, '"'); + return this._super(); + }, + save: function () { + var self = this; + range.createFromNode(self.media).select(); + this.$editable.data('NoteHistory').recordUndo(); + var alt = this.$('#alt').val(); + var title = this.$('#title').val(); + $(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null); + setTimeout(function () { + click_event(self.media, "mouseup"); + },0); + return this._super(); + }, +}); + +var click_event = function(el, type) { + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); + el.dispatchEvent(evt); +}; + +/** + * MediaDialog widget. Lets users change a media, including uploading a + * new image, font awsome or video and can change a media into an other + * media + * + * options: select_images: allow the selection of more of one image + */ +var MediaDialog = Dialog.extend({ + template: 'web_editor.dialog.media', + events : _.extend({}, Dialog.prototype.events, { + 'input input#icon-search': 'search', + }), + init: function ($editable, media, options) { + this._super(); + if ($editable) { + this.$editable = $editable; + this.rte = this.$editable.rte || this.$editable.data('rte'); + } + this.options = options || {}; + this.old_media = media; + this.media = media; + this.isNewMedia = !media; + this.range = range.create(); + }, + start: function () { + var self = this; + + this.only_images = this.options.only_images || this.options.select_images || (this.media && $(this.media).parent().data("oe-field") === "image"); + if (this.only_images) { + this.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); + } + + if (this.media) { + if (this.media.nodeName === "IMG") { + this.$('[href="#editor-media-image"]').tab('show'); + } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) { + this.$('[href="#editor-media-video"]').tab('show'); + } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) { + this.media = this.media.parentNode; + this.$('[href="#editor-media-video"]').tab('show'); + } else if (this.media.className.match(/(^|\s)fa($|\s)/)) { + this.$('[href="#editor-media-icon"]').tab('show'); + } + } + + this.imageDialog = new ImageDialog(this, this.media, this.options); + this.imageDialog.appendTo(this.$("#editor-media-image")); + this.iconDialog = new fontIconsDialog(this, this.media, this.options); + this.iconDialog.appendTo(this.$("#editor-media-icon")); + this.videoDialog = new VideoDialog(this, this.media, this.options); + this.videoDialog.appendTo(this.$("#editor-media-video")); + + this.active = this.imageDialog; + + $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) { + if ($(event.target).is('[href="#editor-media-image"]')) { + self.active = self.imageDialog; + self.$('li.search, li.previous, li.next').removeClass("hidden"); + } else if ($(event.target).is('[href="#editor-media-icon"]')) { + self.active = self.iconDialog; + self.$('li.search, li.previous, li.next').removeClass("hidden"); + self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden"); + } else if ($(event.target).is('[href="#editor-media-video"]')) { + self.active = self.videoDialog; + self.$('.nav-tabs li.search').addClass("hidden"); + } + }); + + return this._super(); + }, + save: function () { + if (this.options.select_images) { + this.trigger("saved", this.active.save()); + this.close(); + return; + } + if(this.rte) { + this.range.select(); + this.rte.historyRecordUndo(this.media); + } + + var self = this; + if (self.media) { + this.media.innerHTML = ""; + if (this.active !== this.imageDialog) { + this.imageDialog.clear(); + } + if (this.active !== this.iconDialog) { + this.iconDialog.clear(); + } + if (this.active !== this.videoDialog) { + this.videoDialog.clear(); + } + } else { + this.media = document.createElement("img"); + this.range.insertNode(this.media, true); + this.active.media = this.media; + } + this.active.save(); + + if (this.active.add_class) { + $(this.media).addClass(this.active.add_class); + } + + $(document.body).trigger("media-saved", [self.active.media, self.old_media]); + self.trigger("saved", [self.active.media, self.old_media]); + setTimeout(function () { + if (!self.active.media.parentNode) { + return; + } + range.createFromNode(self.active.media).select(); + click_event(self.active.media, "mousedown"); + if (!this.only_images) { + setTimeout(function () { + if($(self.active.media).parent().data("oe-field") !== "image") { + click_event(self.active.media, "click"); + } + click_event(self.active.media, "mouseup"); + },0); + } + },0); + + this.close(); + }, + searchTimer: null, + search: function () { + var self = this; + var needle = this.$("input#icon-search").val(); + clearTimeout(this.searchTimer); + this.searchTimer = setTimeout(function () { + self.active.search(needle || ""); + },250); + } +}); + +/** + * ImageDialog widget. Lets users change an image, including uploading a + * new image in OpenERP or selecting the image style (if supported by + * the caller). + */ +var IMAGES_PER_ROW = 6; +var IMAGES_ROWS = 2; +var ImageDialog = Widget.extend({ + template: 'web_editor.dialog.image', + events: _.extend({}, Dialog.prototype.events, { + 'change .url-source': function (e) { + this.changed($(e.target)); + }, + 'click button.filepicker': function () { + var filepicker = this.$('input[type=file]'); + if (!_.isEmpty(filepicker)){ + filepicker[0].click(); + } + }, + 'click .js_disable_optimization': function () { + this.$('input[name="disable_optimization"]').val('1'); + var filepicker = this.$('button.filepicker'); + if (!_.isEmpty(filepicker)){ + filepicker[0].click(); + } + }, + 'change input[type=file]': 'file_selection', + 'submit form': 'form_submit', + 'change input.url': "change_input", + 'keyup input.url': "change_input", + //'change select.image-style': 'preview_image', + 'click .existing-attachments img': 'select_existing', + 'click .existing-attachment-remove': 'try_remove', + }), + init: function (parent, media, options) { + this._super(); + this.options = options || {}; + this.parent = parent; + this.media = media; + this.images = []; + this.page = 0; + }, + start: function () { + this.$preview = this.$('.preview-container').detach(); + var self = this; + var res = this._super(); + var o = { url: null, alt: null }; + // avoid typos, prevent addition of new properties to the object + Object.preventExtensions(o); + + if ($(this.media).is("img")) { + o.url = this.media.getAttribute('src'); + } else { + this.add_class = "img-responsive pull-left"; + } + this.parent.$(".pager > li").click(function (e) { + e.preventDefault(); + var $target = $(e.currentTarget); + if ($target.hasClass('disabled')) { + return; + } + self.page += $target.hasClass('previous') ? -1 : 1; + self.display_attachments(); + }); + this.set_image(o.url, o.alt); + this.fetch_existing(); + return res; + }, + push: function (url, alt, id) { + if (this.options.select_images) { + var img = _.select(this.images, function (v) { return v.url == url;}); + if (img.length) { + this.images.splice(this.images.indexOf(img[0]),1); + return; + } + } else { + this.images = []; + } + this.images.push({'url': url, 'alt': alt, 'id': id}); + }, + save: function () { + if (this.options.select_images) { + this.parent.trigger("save", this.images); + return this.images; + } + this.parent.trigger("save", this.media); + + var img = this.images[0] || { + 'url': this.$(".existing-attachments img:first").attr('src'), + 'alt': this.$(".existing-attachments img:first").attr('alt') + }; + + if (this.media.tagName !== "IMG") { + var media = document.createElement('img'); + $(this.media).replaceWith(media); + this.media = media; + } + + $(this.media).attr('src', img.url).attr('alt', img.alt); + + var style = this.style; + this.media.setAttribute('src', img.url); + if (style) { this.media.addClass(style); } + + return this.media; + }, + clear: function () { + this.media.className = this.media.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' '); + }, + cancel: function () { + this.trigger('cancel'); + }, + change_input: function (e) { + var $input = $(e.target); + var $button = $input.parent().find("button"); + if ($input.val() === "") { + $button.addClass("btn-default").removeClass("btn-primary"); + } else { + $button.removeClass("btn-default").addClass("btn-primary"); + } + }, + search: function (needle) { + var self = this; + this.fetch_existing(needle).then(function () { + self.selected_existing(); + }); + }, + set_image: function (url, alt, error) { + var self = this; + if (url) { + this.push(url, alt); + } + this.$('input.url').val(''); + this.fetch_existing().then(function () { + self.selected_existing(); + }); + }, + form_submit: function (event) { + var self = this; + var $form = this.$('form[action="/web_editor/attachment/add"]'); + if (!$form.find('input[name="upload"]').val().length) { + var url = $form.find('input[name="url"]').val(); + if (this.selected_existing().size()) { + event.preventDefault(); + return false; + } + } + $form.find('.well > div').hide().last().after('<span class="fa fa-spin fa-3x fa-refresh"/>'); + + var callback = _.uniqueId('func_'); + this.$('input[name=func]').val(callback); + window[callback] = function (attachments, error) { + delete window[callback]; + $form.find('.well > span').remove(); + $form.find('.well > div').show(); + if (error || !attachments.length) { + self.file_selected(null, error || !attachments.length); + } + for (var i=0; i<attachments.length; i++) { + self.file_selected(attachments[i]['local_url'] || '/web_editor/image/ir.attachment/'+attachments[i].id+'_'+(new Date().getTime())+'/datas', error); + } + }; + }, + file_selection: function () { + this.$el.addClass('nosave'); + this.$('form').removeClass('has-error').find('.help-block').empty(); + this.$('button.filepicker').removeClass('btn-danger btn-success'); + this.$('form').submit(); + }, + file_selected: function(url, error) { + var $button = this.$('button.filepicker'); + if (!error) { + $button.addClass('btn-success'); + } else { + url = null; + this.$('form').addClass('has-error') + .find('.help-block').text(error); + $button.addClass('btn-danger'); + } + this.set_image(url, null, error); + + if (!this.options.select_images) { + // auto save and close popup + this.parent.save(); + } + }, + fetch_existing: function (needle) { + var domain = [['res_model', '=', 'ir.ui.view'], '|', + ['mimetype', '=', false], ['mimetype', '=like', 'image/%']]; + if (needle && needle.length) { + domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]); + } + return ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'ir.attachment', + method: 'search_read', + args: [], + kwargs: { + domain: domain, + fields: ['name', 'local_url'], + order: 'id desc', + context: base.get_context() + } + }).then(this.proxy('fetched_existing')); + }, + fetched_existing: function (records) { + this.records = records; + this.display_attachments(); + }, + display_attachments: function () { + this.$('.help-block').empty(); + var per_screen = IMAGES_PER_ROW * IMAGES_ROWS; + + var from = this.page * per_screen; + var records = this.records; + + // Create rows of 3 records + var rows = _(records).chain() + .slice(from, from + per_screen) + .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); }) + .values() + .value(); + this.$('.existing-attachments').replaceWith( + QWeb.render( + 'web_editor.dialog.image.existing.content', {rows: rows})); + this.parent.$('.pager') + .find('li.previous').toggleClass('disabled', (from === 0)).end() + .find('li.next').toggleClass('disabled', (from + per_screen >= records.length)); + + this.selected_existing(); + }, + select_existing: function (e) { + var $img = $(e.currentTarget); + this.push($img.attr('src'), $img.attr('alt'), $img.data('id')); + this.selected_existing(); + }, + selected_existing: function () { + var self = this; + this.$('.existing-attachment-cell.media_selected').removeClass("media_selected"); + var $select = this.$('.existing-attachment-cell img').filter(function () { + var $img = $(this); + var url = $img.attr("src"); + return !!_.select(self.images, function (v) { + if (v.url === url) { + if (!v.id) { + v.id = $img.data('id'); + v.alt = $img.attr('alt'); + } + return true; + } + }).length; + }); + $select.parent().addClass("media_selected"); + return $select; + }, + try_remove: function (e) { + var $help_block = this.$('.help-block').empty(); + var self = this; + var $a = $(e.target); + var id = parseInt($a.data('id'), 10); + var attachment = _.findWhere(this.records, {id: id}); + var $both = $a.parent().children(); + + $both.css({borderWidth: "5px", borderColor: "#f00"}); + + return ajax.jsonRpc('/web_editor/attachment/remove', 'call', {'ids': [id]}).then(function (prevented) { + if (_.isEmpty(prevented)) { + self.records = _.without(self.records, attachment); + self.display_attachments(); + return; + } + $both.css({borderWidth: "", borderColor: ""}); + $help_block.replaceWith(QWeb.render( + 'web_editor.dialog.image.existing.error', { + views: prevented[id] + } + )); + }); + }, +}); + + +var cacheCssSelectors = {}; +var getCssSelectors = function(filter) { + var css = []; + if (cacheCssSelectors[filter]) { + return cacheCssSelectors[filter]; + } + var sheets = document.styleSheets; + for(var i = 0; i < sheets.length; i++) { + var rules = sheets[i].rules || sheets[i].cssRules; + if (rules) { + for(var r = 0; r < rules.length; r++) { + var selectorText = rules[r].selectorText; + if (selectorText) { + var match = selectorText.match(filter); + if (match) { + css.push([match[1], rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, '')]); + } + } + } + } + } + return cacheCssSelectors[filter] = css; +}; +function computeFonts() { + _.each(fontIcons, function (data) { + data.icons = _.map(getCssSelectors(data.parser), function (css) { + return css[0].slice(1, css[0].length).replace(/::?before$/, ''); + }); + }); +} + +rte.Class.include({ + init: function (EditorBar) { + this._super.apply(this, arguments); + computeFonts(); + } +}); + +/* list of font icons to load by editor. The icons are displayed in the media editor and + * identified like font and image (can be colored, spinned, resized with fa classes). + * To add font, push a new object {base, parser} + * - base: class who appear on all fonts (eg: fa fa-refresh) + * - parser: regular expression used to select all font in css style sheets + */ +var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}]; + + +/** + * FontIconsDialog widget. Lets users change a font awsome, suport all + * font awsome loaded in the css files. + */ +var fontIconsDialog = Widget.extend({ + template: 'web_editor.dialog.font-icons', + events : _.extend({}, Dialog.prototype.events, { + 'click .font-icons-icon': function (e) { + e.preventDefault(); + e.stopPropagation(); + + this.$('#fa-icon').val(e.target.getAttribute('data-id')); + $(".font-icons-icon").removeClass("font-icons-selected"); + $(event.target).addClass("font-icons-selected"); + }, + }), + + // extract list of font (like awsome) from the cheatsheet. + renderElement: function() { + this.iconsParser = fontIcons; + this.icons = _.flatten(_.map(fontIcons, function (data) { + return data.icons; + })); + this._super(); + }, + + init: function (parent, media) { + this._super(); + this.parent = parent; + this.media = media; + if (_.find(fontIcons, function (data) {return !data.icons;})) { + computeFonts(); + } + }, + start: function () { + return this._super().then(this.proxy('load_data')); + }, + search: function (needle) { + var iconsParser = this.iconsParser; + if (needle) { + var parser = []; + var fontIcon; + + for (var k=0; k<fontIcons.length; k++) { + fontIcon = fontIcons[k]; + var icons = _(fontIcon.icons).filter(function (icon) { + return icon.indexOf(needle) !== -1; + }); + if (icons.length) { + parser.push({ + base: fontIcon.base, + icons: icons + }); + } + } + iconsParser = parser; + } + this.$('div.font-icons-icons').html( + QWeb.render('web_editor.dialog.font-icons.icons', {'iconsParser': iconsParser})); + }, + /** + * Removes existing FontAwesome classes on the bound element, and sets + * all the new ones if necessary. + */ + save: function () { + var self = this; + this.parent.trigger("save", this.media); + var icons = this.icons; + var style = this.media.attributes.style ? this.media.attributes.style.value : ''; + var classes = (this.media.className||"").split(/\s+/); + var non_fa_classes = _.reject(classes, function (cls) { + return self.getFont(cls); + }); + var final_classes = non_fa_classes.concat(this.get_fa_classes()); + if (this.media.tagName !== "SPAN") { + var media = document.createElement('span'); + $(this.media).replaceWith(media); + this.media = media; + style = style.replace(/\s*width:[^;]+/, ''); + } + $(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style); + }, + /** + * return the data font object (with base, parser and icons) or null + */ + getFont: function (classNames) { + if (!(classNames instanceof Array)) { + classNames = (classNames||"").split(/\s+/); + } + var fontIcon, className; + for (var i=0; i<classNames.length; i++) { + className = classNames[i]; + for (var k=0; k<fontIcons.length; k++) { + fontIcon = fontIcons[k]; + if (className === fontIcon.base || fontIcon.icons.indexOf(className) !== -1) { + return { + 'base': fontIcon.base, + 'parser': fontIcon.parser, + 'icons': fontIcon.icons, + 'font': className + }; + } + } + } + return null; + }, + + /** + * Looks up the various FontAwesome classes on the bound element and + * sets the corresponding template/form elements to the right state. + * If multiple classes of the same category are present on an element + * (e.g. fa-lg and fa-3x) the last one occurring will be selected, + * which may not match the visual look of the element. + */ + load_data: function () { + var classes = (this.media&&this.media.className||"").split(/\s+/); + for (var i = 0; i < classes.length; i++) { + var cls = classes[i]; + switch(cls) { + case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x': + // size classes + this.$('#fa-size').val(cls); + continue; + case 'fa-spin': + case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270': + case 'fa-flip-horizontal':case 'fa-rotate-vertical': + this.$('#fa-rotation').val(cls); + continue; + case 'fa-fw': + continue; + case 'fa-border': + this.$('#fa-border').prop('checked', true); + continue; + case '': continue; + default: + $(".font-icons-icon").removeClass("font-icons-selected").filter("."+cls).addClass("font-icons-selected"); + for (var k=0; k<this.icons.length; k++) { + if (this.icons.indexOf(cls) !== -1) { + this.$('#fa-icon').val(cls); + break; + } + } + } + } + }, + /** + * Serializes the dialog to an array of FontAwesome classes. Includes + * the base ``fa``. + */ + get_fa_classes: function () { + var font = this.getFont(this.$('#fa-icon').val()); + return [ + font ? font.base : 'fa', + font ? font.font : "", + this.$('#fa-size').val(), + this.$('#fa-rotation').val(), + this.$('#fa-border').prop('checked') ? 'fa-border' : '' + ]; + }, + clear: function () { + this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' '); + }, +}); + + +function createVideoNode(url) { + // video url patterns(youtube, instagram, vimeo, dailymotion, youku) + var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; + var ytMatch = url.match(ytRegExp); + + var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/; + var igMatch = url.match(igRegExp); + + var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/; + var vMatch = url.match(vRegExp); + + var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/; + var vimMatch = url.match(vimRegExp); + + var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/; + var dmMatch = url.match(dmRegExp); + + var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/; + var youkuMatch = url.match(youkuRegExp); + + var $video = $('<iframe>'); + if (ytMatch && ytMatch[1].length === 11) { + var youtubeId = ytMatch[1]; + $video = $('<iframe>') + .attr('src', '//www.youtube.com/embed/' + youtubeId) + .attr('width', '640').attr('height', '360'); + } else if (igMatch && igMatch[0].length) { + $video = $('<iframe>') + .attr('src', igMatch[0] + '/embed/') + .attr('width', '612').attr('height', '710') + .attr('scrolling', 'no') + .attr('allowtransparency', 'true'); + } else if (vMatch && vMatch[0].length) { + $video = $('<iframe>') + .attr('src', vMatch[0] + '/embed/simple') + .attr('width', '600').attr('height', '600') + .attr('class', 'vine-embed'); + } else if (vimMatch && vimMatch[3].length) { + $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') + .attr('src', '//player.vimeo.com/video/' + vimMatch[3]) + .attr('width', '640').attr('height', '360'); + } else if (dmMatch && dmMatch[2].length) { + $video = $('<iframe>') + .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2]) + .attr('width', '640').attr('height', '360'); + } else if (youkuMatch && youkuMatch[1].length) { + $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') + .attr('height', '498') + .attr('width', '510') + .attr('src', '//player.youku.com/embed/' + youkuMatch[1]); + } else { + // this is not a known video link. Now what, Cat? Now what? + } + + $video.attr('frameborder', 0); + + return $video; +} + +/** + * VideoDialog widget. Lets users change a video, support all summernote + * video, and embled iframe + */ +var VideoDialog = Widget.extend({ + template: 'web_editor.dialog.video', + events : _.extend({}, Dialog.prototype.events, { + 'click input#urlvideo ~ button': 'get_video', + 'click input#embedvideo ~ button': 'get_embed_video', + 'change input#urlvideo': 'change_input', + 'keyup input#urlvideo': 'change_input', + 'change input#embedvideo': 'change_input', + 'keyup input#embedvideo': 'change_input' + }), + init: function (parent, media) { + this._super(); + this.parent = parent; + this.media = media; + }, + start: function () { + this.$preview = this.$('.preview-container').detach(); + this.$iframe = this.$("iframe"); + var $media = $(this.media); + if ($media.hasClass("media_iframe_video")) { + var src = $media.data('src'); + this.$("input#urlvideo").val(src); + this.$("#autoplay").attr("checked", src.indexOf('autoplay=1') != -1); + this.get_video(); + } else { + this.add_class = "pull-left"; + } + return this._super(); + }, + change_input: function (e) { + var $input = $(e.target); + var $button = $input.parent().find("button"); + if ($input.val() === "") { + $button.addClass("btn-default").removeClass("btn-primary"); + } else { + $button.removeClass("btn-default").addClass("btn-primary"); + } + }, + get_embed_video: function (event) { + event.preventDefault(); + var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/); + if (embedvideo) { + this.$("input#urlvideo").val(embedvideo[1]); + this.get_video(event); + } + return false; + }, + get_video: function (event) { + if (event) event.preventDefault(); + var $video = createVideoNode(this.$("input#urlvideo").val()); + this.$iframe.replaceWith($video); + this.$iframe = $video; + return false; + }, + save: function () { + this.parent.trigger("save", this.media); + var video_id = this.$("#video_id").val(); + if (!video_id) { + this.$("button.btn-primary").click(); + video_id = this.$("#video_id").val(); + } + var video_type = this.$("#video_type").val(); + var $iframe = $( + '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+ + '<div class="css_editable_mode_display"> </div>'+ + '<div class="media_iframe_video_size" contentEditable="false"> </div>'+ + '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+ + '</div>'); + $(this.media).replaceWith($iframe); + this.media = $iframe[0]; + }, + clear: function () { + if (this.media.dataset.src) { + try { + delete this.media.dataset.src; + } catch(e) { + this.media.dataset.src = undefined; + } + } + this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' '); + }, +}); + +/* ----- EDITOR: LINK & MEDIA ---- */ +var LinkDialog = Dialog.extend({ + template: 'web_editor.dialog.link', + events: _.extend({}, Dialog.prototype.events, { + 'change :input.url-source': 'changed', + 'keyup :input.url': 'onkeyup', + 'keyup :input': 'preview', + 'click button.remove': 'remove_link', + 'change input#link-text': function (e) { + this.text = $(e.target).val(); + }, + 'change .link-style': function (e) { + this.preview(); + }, + }), + init: function (editable, linkInfo) { + this._super(editable, linkInfo); + this.editable = editable; + this.data = linkInfo || {}; + + this.data.className = ""; + if (this.data.range) { + this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || ""; + this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' '); + + var is_link = this.data.range.isOnAnchor(); + var r = this.data.range; + + var sc = r.sc; + var so = r.so; + var ec = r.ec; + var eo = r.eo; + + var nodes; + if (!is_link) { + if (sc.tagName) { + sc = dom.firstChild(so ? sc.childNodes[so] : sc); + so = 0; + } else if (so !== sc.textContent.length) { + if (sc === ec) { + ec = sc = sc.splitText(so); + eo -= so; + } else { + sc = sc.splitText(so); + } + so = 0; + } + if (ec.tagName) { + ec = dom.lastChild(eo ? ec.childNodes[eo-1] : ec); + eo = ec.textContent.length; + } else if (eo !== ec.textContent.length) { + ec.splitText(eo); + } + + nodes = dom.listBetween(sc, ec); + + // browsers can't target a picture or void node + if (dom.isVoid(sc) || dom.isImg(sc)) { + so = dom.listPrev(sc).length-1; + sc = sc.parentNode; + } + if (dom.isBR(ec)) { + eo = dom.listPrev(ec).length-1; + ec = ec.parentNode; + } else if (dom.isVoid(ec) || dom.isImg(sc)) { + eo = dom.listPrev(ec).length; + ec = ec.parentNode; + } + + this.data.range = range.create(sc, so, ec, eo); + this.data.range.select(); + } else { + nodes = dom.ancestor(sc, dom.isAnchor).childNodes; + } + + if (dom.isImg(sc) && nodes.indexOf(sc) === -1) { + nodes.push(sc); + } + if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) { + var text = ""; + this.data.images = []; + for (var i=0; i<nodes.length; i++) { + if (dom.ancestor(nodes[i], dom.isImg)) { + this.data.images.push(dom.ancestor(nodes[i], dom.isImg)); + text += '[IMG]'; + } else if (!is_link && i===0) { + text += nodes[i].textContent.slice(so, Infinity); + } else if (!is_link && i===nodes.length-1) { + text += nodes[i].textContent.slice(0, eo); + } else { + text += nodes[i].textContent; + } + } + this.data.text = text; + } + } + + this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' '); + }, + start: function () { + this.bind_data(); + this.$('input.url-source:eq(1)').closest('.list-group-item').addClass('active'); + return this._super(); + }, + get_data: function (test) { + var self = this; + var def = new $.Deferred(); + var $e = this.$('.active input.url-source'); + if (!$e.length) { + $e = this.$('input.url-source:first'); + } + var val = $e.val(); + var label = this.$('#link-text').val() || val; + + if (label && this.data.images) { + for(var i=0; i<this.data.images.length; i++) { + label = label.replace(/</, "<").replace(/>/, ">").replace(/\[IMG\]/, this.data.images[i].outerHTML); + } + } + + if (!test && (!val || !$e[0].checkValidity())) { + // FIXME: error message + $e.closest('.form-group').addClass('has-error'); + $e.focus(); + def.reject(); + } + + var style = this.$("input[name='link-style-type']:checked").val() || ''; + var size = this.$("input[name='link-style-size']:checked").val() || ''; + var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size; + var isNewWindow = this.$('input.window-new').prop('checked'); + + if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) { + self.get_data_buy_mail(def, $e, isNewWindow, label, classes); + } else { + self.get_data_buy_url(def, $e, isNewWindow, label, classes); + } + return def; + }, + get_data_buy_mail: function (def, $e, isNewWindow, label, classes) { + var val = $e.val(); + def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes); + }, + get_data_buy_url: function (def, $e, isNewWindow, label, classes) { + def.resolve($e.val(), isNewWindow, label, classes); + }, + save: function () { + var self = this; + var _super = this._super.bind(this); + return this.get_data() + .then(function (url, new_window, label, classes) { + self.data.url = url; + self.data.newWindow = new_window; + self.data.text = label; + self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, ''); + + self.trigger("save", self.data); + }).then(_super); + }, + bind_data: function () { + var href = this.data.url; + var new_window = this.data.isNewWindow; + var text = this.data.text; + var classes = this.data.iniClassName; + + this.$('input#link-text').val(text); + this.$('input.window-new').prop('checked', new_window); + + if (classes) { + this.$('input[value!=""]').each(function () { + var $option = $(this); + if (classes.indexOf($option.val()) !== -1) { + $option.attr("checked", "checked"); + } + }); + } + + if (href) { + var match; + if(match = /mailto:(.+)/.exec(href)) { + this.$('input.email-address').val(match = /mailto:(.+)/.exec(href) ? match[1] : ''); + } else { + this.$('input.url').val(href); + } + } + this.preview(); + }, + changed: function (e) { + $(e.target).closest('.list-group-item') + .addClass('active') + .siblings().removeClass('active') + .addBack().removeClass('has-error'); + this.preview(); + }, + onkeyup: function (e) { + var $e = $(e.target); + var is_link = ($e.val()||'').length && $e.val().indexOf("@") === -1; + this.$('input.window-new').closest("div").toggle(is_link); + this.preview(); + }, + preview: function () { + var $preview = this.$("#link-preview"); + this.get_data(true).then(function (url, new_window, label, classes) { + $preview.attr("target", new_window ? '_blank' : "") + .attr("href", url && url.length ? url : "#") + .html((label && label.length ? label : url)) + .attr("class", classes.replace(/pull-\w+/, '')); + }); + } +}); + +return { + 'getCssSelectors': getCssSelectors, + 'Dialog': Dialog, + 'alt': alt, + 'MediaDialog': MediaDialog, + 'fontIcons': fontIcons, + 'LinkDialog': LinkDialog +}; + +}); diff --git a/addons/web_editor/static/src/xml/backend.xml b/addons/web_editor/static/src/xml/backend.xml new file mode 100644 index 00000000000..5e3ed9695cd --- /dev/null +++ b/addons/web_editor/static/src/xml/backend.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + + <t t-name="web_editor.FieldTextHtmlSimple"> + <div class="oe_form_field oe_form_field_html_text" t-att-style="widget.node.attrs.style"> + <textarea readonly="readonly"/> + </div> + </t> + + <t t-name="web_editor.FieldTextHtml"> + <div class="oe_form_field oe_form_field_html oe_form_embedded_html" t-att-style="widget.node.attrs.style"> + <iframe/> + </div> + </t> + + <t t-name="web_editor.FieldTextHtml.button.reset"> + <div class="btn-group pull-right"> + <button class="o_reset_translation btn btn-default btn-sm btn-small" title="Reset from source"><span class="fa fa-upload"></span></button> + </div> + </t> + + <t t-name="web_editor.FieldTextHtml.button.translate"> + <div class="btn-group pull-right"> + <button t-if="widget.field.translate && widget.get('effective_readonly')" class="oe_field_translate btn btn-default btn-sm btn-small" style="height: 24px; padding: 1px 3px 0px 5px"> + <img border="0" class="oe_input_icon" height="16" width="16" src="/web/static/src/img/icons/terp-translate.png" style="margin: 2px 0px -2px 0px;"/> + </button> + </div> + </t> + + <t t-name="web_editor.FieldTextHtml.translate"> + <div t-if="widget.view.datarecord.id && widget.languages.length" style="margin: 5px; position: fixed; top: 0; left: 0; z-index: 2000;"> + <select style=" + width: 148px; + height: 22px; + padding: 2px 4px; + border: 1px solid #ccc; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; + background: white;"> + <t t-foreach="widget.languages" t-as="lang"> + <option value="en_US" t-att-selected="'en_US' == widget.lang ? 'selected' : null"> + <t t-if="!widget.get('effective_readonly')">English (edit mode)</t> + <t t-if="widget.get('effective_readonly')">English</t> + </option> + <option t-att-value="lang.code" t-att-selected="lang.code == widget.lang ? 'selected' : null"><t t-esc="lang.name"/></option> + </t> + </select> + </div> + </t> + <t t-name="web_editor.FieldTextHtml.fullscreen"> + <span style="margin: 5px; position: fixed; top: 0; right: 0; z-index: 2000;"> + <button class="o_fullscreen btn btn-primary" style="height: 24px; padding: 1px 2px 1px 3px;"> + <img border="0" class="oe_input_icon" height="16" width="16" src="/web/static/src/img/icons/STOCK_FULLSCREEN.png" style="margin: 2px 0px -2px 0px;"/> + </button> + </span> + </t> + +</templates> diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml new file mode 100644 index 00000000000..b2f4d583052 --- /dev/null +++ b/addons/web_editor/static/src/xml/editor.xml @@ -0,0 +1,372 @@ +<templates id="template" xml:space="preserve"> + <t t-name="web_editor.editorbar"> + <div id="web_editor-top-edit" class="o_css_editor navbar navbar-inverse navbar-fixed-top hidden-xs"> + <form class="navbar-form text-muted"> + <button type="button" data-action="save" + class="btn btn-primary btn-xs">Save</button> + <span> or </span> + <a href="#" data-action="cancel" class="btn btn-link btn-xs">Discard</a> + </form> + </div> + </t> + + <t t-name="web_editor.dialog"> + <div class="modal note-link-dialog" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3 class="modal-title"><t t-esc="title"/></h3> + </div> + <div class="modal-body"><t t-raw="0"/></div> + <div class="modal-footer"> + <button type="button" class="btn btn-primary save" t-if="!button">Save</button> + <button type="button" class="btn btn-primary save" t-if="button"><t t-raw="button"/></button> + <button type="button" class="btn hidden wait" disabled="disabled"/> + <span> or </span> + <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a> + </div> + </div> + </div> + </div> + </t> + <t t-name="web_editor.dialog.media"> + <div class="modal note-image-dialog" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog select-media"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3 class="modal-title">Select a Media</h3> + </div> + <div class="modal-body"> + <ul class="nav nav-tabs"> + <li class="active"><a href="#editor-media-image" data-toggle="tab">Image</a></li> + <li><a href="#editor-media-icon" data-toggle="tab">Pictogram</a></li> + <li><a href="#editor-media-video" data-toggle="tab">Video</a></li> + <li class="search" style="float: right;"> + <ul class="pager mb0 mt0"> + <li class="previous disabled"><a href="#">← Previous</a></li> + <li class="next disabled"><a href="#">Next →</a></li> + </ul> + </li> + <li class="search" style="float: right;"> + <form action="#"> + <div class="form-group font-icons fa fa-search mb0"> + <input type="search" class="form-control" id="icon-search"/> + </div> + </form> + </li> + </ul> + + <!-- Tab panes --> + <div class="tab-content"> + <div class="tab-pane fade in active" id="editor-media-image"> + </div> + <div class="tab-pane fade" id="editor-media-icon"> + </div> + <div class="tab-pane fade" id="editor-media-video"> + </div> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-primary save">Save</button> + <button type="button" class="btn hidden wait" disabled="disabled"/> + or + <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a> + </div> + </div> + </div> + </div> + </t> + <t t-name="web_editor.dialog.image"> + <div> + <form method="POST" + action="/web_editor/attachment/add" + enctype="multipart/form-data" + target="fileframe" + class="form-inline"> + <div class="well"> + <div class="form-group pull-left"> + <input type="file" name="upload" accept="image/*" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/> + <input type="hidden" name="disable_optimization" value=""/> + <div class="btn-group"> + <button type="button" class="btn btn-primary filepicker">Upload an image from your computer</button> + <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"> + <span class="caret"></span> + <span class="sr-only">Alternate Upload</span> + </button> + <ul class="dropdown-menu" role="menu"> + <li><a href="#" class="js_disable_optimization">Upload image without optimization</a></li> + </ul> + </div> + <button type="button" class="btn hidden wait" disabled="disabled">Uploading...</button> + </div> + <div> + <span class="text-muted"> — or — </span> + <label for="iamgeurl">Add an image URL</label> + <div class="form-group btn-group"> + <input type="text" name="url" class="form-control url pull-left" style="width: 320px;" id="iamgeurl" placeholder="http://openerp.com/logo.png"/> + <button class="btn btn-default" type="submit">Add</button> + </div> + </div> + </div> + <input type="hidden" name="func"/> + <div class="help-block"/> + <div class="existing-attachments"> + <t t-call="web_editor.dialog.image.existing"/> + </div> + </form> + </div> + <iframe src="about:blank" name="fileframe" class="hidden"/> + </t> + <t t-name="web_editor.dialog.image.existing"> + <div class="modal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog select-image"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3 class="modal-title">Select a Picture</h3> + </div> + <div class="modal-body has-error"> + <div class="existing-attachments"/> + <div class="help-block"/> + </div> + <div class="modal-footer"> + <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a> + </div> + </div> + </div> + </div> + </t> + <t t-name="web_editor.dialog.image.existing.content"> + <div class="existing-attachments"> + <div class="row mt16" t-foreach="rows" t-as="row"> + <div class="col-sm-2 existing-attachment-cell" + t-foreach="row" t-as="attachment"> + <i class="fa fa-times existing-attachment-remove" t-att-data-id="attachment.id"/> + <img t-att-src="attachment.local_url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" class="img img-responsive"/> + </div> + </div> + </div> + </t> + <t t-name="web_editor.dialog.image.existing.error"> + <div class="help-block"> + <p>The image could not be deleted because it is used in the + following pages or views:</p> + <ul t-foreach="views" t-as="view"> + <li> + <a t-attf-href="/web#model=ir.ui.view&id=#{view.id}"> + <t t-esc="view.name"/> + </a> + </li> + </ul> + </div> + </t> + + <t t-name="web_editor.discard"> + <div class="modal" tabindex="-1" role="dialog" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h3 class="modal-title">Discard edition</h3> + </div> + <div class="modal-body"> + <p> + If you discard the current edition, <em>all</em> + unsaved changes will be lost. + </p> + <p>You can cancel to return to the edition mode.</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-danger">Discard</button> + or + <a href="#" data-dismiss="modal" aria-hidden="true">Cancel</a> + </div> + </div> + </div> + </div> + </t> + + <t t-name="web_editor.dialog.alt"> + <t t-call="web_editor.dialog"> + <t t-set="title">Change media Alt and Title</t> + <form action="#" class="form-horizontal"> + <div class="form-group"> + <label for="alt" class="col-sm-2 control-label">Alt:</label> + <div class="col-sm-9"><input id="alt" type="text" class="form-control" required="required" t-att-value="widget.alt"/></div> + </div> + <div class="form-group"> + <label for="title" class="col-sm-2 control-label">Title:</label> + <div class="col-sm-9"><input id="title" type="text" class="form-control" required="required" t-att-value="widget.title"/></div> + </div> + </form> + </t> + </t> + + <t t-name="web_editor.dialog.font-icons"> + <form action="#"> + <input type="hidden" id="fa-icon"/> + <input type="hidden" id="fa-size" value="fa-1x"/> + <div class="font-icons-icons"> + <t t-call="web_editor.dialog.font-icons.icons"> + <t t-set="iconsParser" t-value="widget.iconsParser"/> + </t> + </div> + </form> + </t> + <t t-name="web_editor.dialog.font-icons.icons"> + <t t-foreach="iconsParser" t-as="data"> + <span t-foreach="data.icons" t-as="icon" + t-attf-class="#{data.base} font-icons-icon #{icon}" + t-att-data-id="icon"/> + </t> + </t> + + <t t-name="web_editor.dialog.video"> + <form action="#" class="form-inline"> + <div class="well"> + <div class="form-group btn-group"> + <span class="text-muted pull-right" style="margin-left:10px; line-height: 2em;">(Youtube, Vimeo, Dailymotion)</span> + <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left">Set a video URL</label> + <input type="text" + name="url" + class="form-control url pull-left" + style="width: 400px;" + id="urlvideo" + placeholder="//www.youtube.com/embed/yws1tbgNV7k" + t-translation="off"/> + <button class="btn btn-default">Preview</button> + </div> + <div class="form-group btn-group"> + <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left"><span class="text-muted">— or —</span> Embed Video (HTML)</label> + <input type="text" + name="embed" + class="form-control url pull-left" + style="width: 400px;" + id="embedvideo" + placeholder='<iframe src="//www.youtube.com/embed/yws1tbgNV7k"></iframe>' + t-translation="off"/> + <button class="btn btn-default">Preview</button> + </div> + </div> + + <div id="video-preview" style="width: 450px; margin: 0 auto;"> + <div class="media_iframe_video"> + <div class="media_iframe_video_size"></div> + <iframe + src="" + frameborder="0" + allowfullscreen="allowfullscreen"></iframe> + </div> + </div> + + <div class="text-center mt32"> + <div class="checkbox"> + <label><input type="checkbox" id="autoplay"/> Autoplay</label> + </div> + </div> + </form> + </t> + + <t t-name="web_editor.dialog.link"> + <t t-call="web_editor.dialog"> + <t t-set="title">Link to</t> + <form> + <div class="list-group"> + <div class="form-group list-group-item active"> + <h3 class="list-group-item-heading"> + <label for="link-new" class="control-label"> + Link Label + </label> + </h3> + <input type="text" class="form-control" id="link-text" required="required"/> + </div> + </div> + <ul class="list-group"> + <li class="list-group-item form-group clearfix active"> + <div class="pull-right" style="display: none;"> + <label> + <input type="checkbox" class="window-new"/> + Open in new window + </label> + </div> + <h4 class="list-group-item-heading"> + <label for="link-external" class="control-label"> + URL or Email Address + </label> + </h4> + <input type="text" class="form-control url email-address url-source" + id="link-external" placeholder="http://openerp.com"/> + </li> + </ul> + <div class="form-horizontal link-style"> + <div class="form-group"> + <label class="col-sm-2 control-label"> + Style + </label> + <div class="col-sm-4"> + <div class="dropdown" style="line-height: 2em;"> + <a data-toggle="dropdown" class="btn btn-default" href="#">Color Style <span class="caret"></span></a> + <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> + <li> + <label class="btn btn-link">Link<input value="" class="link-style" type="radio" name="link-style-type" checked="checked"/></label> + </li> + <li> + <label class="btn btn-default">Basic<input value="btn-default" class="link-style" type="radio" name="link-style-type"/></label> + </li> + <li> + <label class="btn btn-success">Success<input value="btn-success" class="link-style" type="radio" name="link-style-type"/></label> + </li> + <li> + <label class="btn btn-primary">Primary<input value="btn-primary" class="link-style" type="radio" name="link-style-type"/></label> + </li> + <li> + <label class="btn btn-info">Info<input value="btn-info" class="link-style" type="radio" name="link-style-type"/></label> + </li> + <li> + <label class="btn btn-warning">Warning<input value="btn-warning" class="link-style" type="radio" name="link-style-type"/></label> + </li> + <li> + <label class="btn btn-danger">Danger<input value="btn-danger" class="link-style" type="radio" name="link-style-type"/></label> + </li> + </ul> + </div> + </div> + <div class="col-sm-4"> + <div class="dropdown" style="line-height: 2em;"> + <a data-toggle="dropdown" class="btn btn-default" href="#">Size <span class="caret"></span></a> + <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> + <li> + <label class="btn btn-default btn-xs">Extra Small<input value="btn-xs" class="link-style" type="radio" name="link-style-size"/></label> + </li> + <li> + <label class="btn btn-default btn-sm">Small<input value="btn-sm" class="link-style" type="radio" name="link-style-size"/></label> + </li> + <li> + <label class="btn btn-default">Default<input value="" class="link-style" type="radio" name="link-style-size" checked="checked"/></label> + </li> + <li> + <label class="btn btn-default btn-lg">Large<input value="btn-lg" class="link-style" type="radio" name="link-style-size"/></label> + </li> + </ul> + </div> + </div> + </div> + </div> + <div class="form-horizontal link-style"> + <div class="form-group"> + <label class="col-sm-2 control-label"> + Preview + </label> + <div class="col-sm-10" style="line-height: 2em;"> + <div style="overflow: auto; max-width: 100%; max-height: 200px;"> + <a id="link-preview" href="#"></a> + </div> + </div> + </div> + </div> + </form> + </t> + </t> +</templates> diff --git a/addons/website/static/src/xml/website.snippets.xml b/addons/web_editor/static/src/xml/snippets.xml index 42a925184fa..ee996548379 100644 --- a/addons/website/static/src/xml/website.snippets.xml +++ b/addons/web_editor/static/src/xml/snippets.xml @@ -2,7 +2,7 @@ <templates id="template" xml:space="preserve"> <!-- Snippet loader --> - <t t-name="website.snippets_style"> + <t t-name="web_editor.snippets_style"> <li class="navbar-form"> <div class="btn-group"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> @@ -13,7 +13,7 @@ </li> </t> - <t t-name="website.snippets"> + <t t-name="web_editor.snippets"> <div id='oe_snippets' class="hidden-xs o_open"> <div id="o_arrow"> <span class="fa fa-angle-double-right fa-1x" style="top: 10px;"></span> @@ -24,7 +24,7 @@ </div> </t> - <t t-name="website.snippet_overlay"> + <t t-name="web_editor.snippet_overlay"> <div class="oe_overlay"> <div class="oe_overlay_options" contentEditable="false"> <div class="btn-group"> @@ -51,13 +51,13 @@ </div> </t> - <t t-name="website.snippet_manipulator"> + <t t-name="web_editor.snippet_manipulator"> <div class='oe_snippet_manipulator'></div> </t> <!-- t-field options --> - <t t-name="website.many2one.button"> + <t t-name="web_editor.many2one.button"> <div class="dropdown"> <a href="#" class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" title="Search Contact"><i class="fa fa-search"></i></a> <ul class="dropdown-menu contact_menu" role="menu"> @@ -66,7 +66,7 @@ </div> </t> - <t t-name="website.many2one.search"> + <t t-name="web_editor.many2one.search"> <t t-foreach="contacts" t-as="item"> <li><a href="#" t-att-data-id="item.id" t-att-data-name="item.display_name"><t t-esc="item.display_name"/> <t t-if="item.city or item.country_id"><small class="text-muted">(<t t-esc="item.city"/> <t t-esc="item.country_id and item.country_id[1]"/>)</small></t></a></li> </t> diff --git a/addons/web_editor/static/src/xml/translator.xml b/addons/web_editor/static/src/xml/translator.xml new file mode 100644 index 00000000000..5a825d4ad0b --- /dev/null +++ b/addons/web_editor/static/src/xml/translator.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<templates id="template" xml:space="preserve"> + +<t t-name="web_editor.translator"> + <div id="web_editor-top-edit" class="o_css_editor o_translate navbar navbar-inverse navbar-fixed-top hidden-xs"> + <form class="navbar-form text-muted"> + <button type="button" data-action="save" class="btn btn-primary btn-xs">Save</button> + <span>or</span> + <a href="#" data-action="cancel" class="btn btn-link btn-xs">Discard</a> + </form> + </div> +</t> + +</templates> diff --git a/addons/web_editor/tests/__init__.py b/addons/web_editor/tests/__init__.py new file mode 100644 index 00000000000..ee3fba301a9 --- /dev/null +++ b/addons/web_editor/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +import test_ui +import test_converter diff --git a/addons/web_editor/tests/test_converter.py b/addons/web_editor/tests/test_converter.py new file mode 100644 index 00000000000..ec8124b66b5 --- /dev/null +++ b/addons/web_editor/tests/test_converter.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +import textwrap +import unittest2 + +from lxml import etree, html +from lxml.builder import E + +from openerp.tests import common +from openerp.addons.base.ir import ir_qweb +from openerp.addons.web_editor.models.ir_qweb import html_to_text + + +class TestHTMLToText(unittest2.TestCase): + def test_rawstring(self): + self.assertEqual( + "foobar", + html_to_text(E.div("foobar"))) + + def test_br(self): + self.assertEqual( + "foo\nbar", + html_to_text(E.div("foo", E.br(), "bar"))) + + self.assertEqual( + "foo\n\nbar\nbaz", + html_to_text(E.div( + "foo", E.br(), E.br(), + "bar", E.br(), + "baz"))) + + def test_p(self): + self.assertEqual( + "foo\n\nbar\n\nbaz", + html_to_text(E.div( + "foo", + E.p("bar"), + "baz"))) + + self.assertEqual( + "foo", + html_to_text(E.div(E.p("foo")))) + + self.assertEqual( + "foo\n\nbar", + html_to_text(E.div("foo", E.p("bar")))) + self.assertEqual( + "foo\n\nbar", + html_to_text(E.div(E.p("foo"), "bar"))) + + self.assertEqual( + "foo\n\nbar\n\nbaz", + html_to_text(E.div( + E.p("foo"), + E.p("bar"), + E.p("baz"), + ))) + + def test_div(self): + self.assertEqual( + "foo\nbar\nbaz", + html_to_text(E.div( + "foo", + E.div("bar"), + "baz" + ))) + + self.assertEqual( + "foo", + html_to_text(E.div(E.div("foo")))) + + self.assertEqual( + "foo\nbar", + html_to_text(E.div("foo", E.div("bar")))) + self.assertEqual( + "foo\nbar", + html_to_text(E.div(E.div("foo"), "bar"))) + + self.assertEqual( + "foo\nbar\nbaz", + html_to_text(E.div( + "foo", + E.div("bar"), + E.div("baz") + ))) + + def test_other_block(self): + self.assertEqual( + "foo\nbar\nbaz", + html_to_text(E.div( + "foo", + E.section("bar"), + "baz" + ))) + + def test_inline(self): + self.assertEqual( + "foobarbaz", + html_to_text(E.div("foo", E.span("bar"), "baz"))) + + def test_whitespace(self): + self.assertEqual( + "foo bar\nbaz", + html_to_text(E.div( + "foo\nbar", + E.br(), + "baz") + )) + + self.assertEqual( + "foo bar\nbaz", + html_to_text(E.div( + E.div(E.span("foo"), " bar"), + "baz"))) + + +class TestConvertBack(common.TransactionCase): + def setUp(self): + super(TestConvertBack, self).setUp() + + def field_rountrip_result(self, field, value, expected): + model = 'web_editor.converter.test' + Model = self.registry(model) + id = Model.create( + self.cr, self.uid, { + field: value + }) + [record] = Model.browse(self.cr, self.uid, [id]) + + e = etree.Element('span') + field_value = 'record.%s' % field + e.set('t-field', field_value) + + rendered = self.registry('ir.qweb').render_tag_field( + e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, { + 'record': record, + }, context={'inherit_branding': True})) + element = html.fromstring( + rendered, parser=html.HTMLParser(encoding='utf-8')) + + converter = self.registry('ir.qweb').get_converter_for( + element.get('data-oe-type')) + + value_back = converter.from_html( + self.cr, self.uid, model, Model._fields[field], element) + + if isinstance(expected, str): + expected = expected.decode('utf-8') + self.assertEqual(value_back, expected) + + def field_roundtrip(self, field, value): + self.field_rountrip_result(field, value, value) + + def test_integer(self): + self.field_roundtrip('integer', 42) + + def test_float(self): + self.field_roundtrip('float', 42.567890) + self.field_roundtrip('float', 324542.567890) + + def test_numeric(self): + self.field_roundtrip('numeric', 42.77) + + def test_char(self): + self.field_roundtrip('char', "foo bar") + + self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ") + + def test_selection(self): + self.field_roundtrip('selection', 3) + + def test_selection_str(self): + self.field_roundtrip('selection_str', 'B') + + def test_text(self): + self.field_roundtrip('text', textwrap.dedent("""\ + You must obey the dance commander + Givin' out the order for fun + You must obey the dance commander + You know that he's the only one + Who gives the orders here, + Alright + Who gives the orders here, + Alright + + It would be awesome + If we could dance-a + It would be awesome, yeah + Let's take the chance-a + It would be awesome, yeah + Let's start the show + Because you never know + You never know + You never know until you go""")) + + def test_m2o(self): + """ the M2O field conversion (from html) is markedly different from + others as it directly writes into the m2o and returns nothing at all. + """ + model = 'web_editor.converter.test' + field = 'many2one' + + Sub = self.registry('web_editor.converter.test.sub') + sub_id = Sub.create(self.cr, self.uid, {'name': "Foo"}) + sub2_id = Sub.create(self.cr, self.uid, {'name': "Bar"}) + + Model = self.registry(model) + id = Model.create(self.cr, self.uid, {field: sub_id}) + [record] = Model.browse(self.cr, self.uid, [id]) + + e = etree.Element('span') + field_value = 'record.%s' % field + e.set('t-field', field_value) + + rendered = self.registry('ir.qweb').render_tag_field( + e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, { + 'record': record, + }, context={'inherit_branding': True})) + + element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8')) + # emulate edition + element.set('data-oe-many2one-id', str(sub2_id)) + element.text = "New content" + + converter = self.registry('ir.qweb').get_converter_for( + element.get('data-oe-type')) + + value_back = converter.from_html( + self.cr, self.uid, model, Model._fields[field], element) + + self.assertIsNone( + value_back, "the m2o converter should return None to avoid spurious" + " or useless writes on the parent record") + + self.assertEqual( + Sub.browse(self.cr, self.uid, sub_id).name, + "Foo", + "element edition can't change directly the m2o record" + ) + self.assertEqual( + Model.browse(self.cr, self.uid, id).many2one.name, + "Bar", + "element edition should have been change the m2o id" + ) diff --git a/addons/web_editor/tests/test_ui.py b/addons/web_editor/tests/test_ui.py new file mode 100644 index 00000000000..c0887201bba --- /dev/null +++ b/addons/web_editor/tests/test_ui.py @@ -0,0 +1,9 @@ +import openerp.tests + + +class TestUi(openerp.tests.HttpCase): + def test_01_admin_rte(self): + self.phantom_js("/web", "odoo.__DEBUG__.services['web.Tour'].run('rte', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.rte", login='admin') + + def test_02_admin_rte_inline(self): + self.phantom_js("/web", "odoo.__DEBUG__.services['web.Tour'].run('rte_inline', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.rte", login='admin') diff --git a/addons/web_editor/views/editor.xml b/addons/web_editor/views/editor.xml new file mode 100644 index 00000000000..c7d785d58f8 --- /dev/null +++ b/addons/web_editor/views/editor.xml @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + +<template id="summernote" name="Summernote"> + <script type="text/javascript"> + (function () { + "use strict"; + odoo.define('jquery', function () {return $;}); + + var uniqId = 0; + odoo.__define__ = window.define; + window.define = function (id) { + var args = Array.prototype.slice.call(arguments); + var factorie = args.pop(); + var id = args[0]; + + if (id instanceof Array) { + var name = "summernote_factorie_" + (++uniqId); + odoo[name] = factorie; + + var head = ''; + var fn = 'var fn = odoo.'+name+';\ndelete odoo.'+name+';\n'; + fn += 'return fn('; + for (var k=0; k<id.length; k++) { + head += 'var a'+(++uniqId)+' = require("'+id[k]+'");\n'; + fn += 'a'+uniqId+', '; + } + fn += 'null);'; + + odoo.define(odoo.website_next_define, new Function('require', head + fn)); + } else { + odoo.define(id, factorie); + } + }; + })(); + </script> + <script>odoo.website_next_define = 'summernote/core/async';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/async.js" /> + <script>odoo.website_next_define = 'summernote/core/func';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/func.js" /> + <script>odoo.website_next_define = 'summernote/core/agent';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/agent.js" /> + <script>odoo.website_next_define = 'summernote/core/list';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/list.js" /> + <script>odoo.website_next_define = 'summernote/core/dom';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/dom.js" /> + <script>odoo.website_next_define = 'summernote/core/key';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/key.js" /> + <script>odoo.website_next_define = 'summernote/core/range';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/core/range.js" /> + + <script>odoo.website_next_define = 'summernote/editing/Bullet';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Bullet.js" /> + <script>odoo.website_next_define = 'summernote/editing/History';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/History.js" /> + <script>odoo.website_next_define = 'summernote/editing/Style';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Style.js" /> + <script>odoo.website_next_define = 'summernote/editing/Table';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Table.js" /> + <script>odoo.website_next_define = 'summernote/editing/Typing';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Typing.js" /> + <script>odoo.website_next_define = 'summernote/editing/Editor';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/editing/Editor.js" /> + + <script>odoo.website_next_define = 'summernote/module/Button';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Button.js" /> + <script>odoo.website_next_define = 'summernote/module/Dialog';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Dialog.js" /> + <script>odoo.website_next_define = 'summernote/module/Handle';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Handle.js" /> + <script>odoo.website_next_define = 'summernote/module/Popover';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Popover.js" /> + <script>odoo.website_next_define = 'summernote/module/Toolbar';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/module/Toolbar.js" /> + + <script>odoo.website_next_define = 'summernote/Renderer';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/Renderer.js" /> + <script>odoo.website_next_define = 'summernote/EventHandler';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/EventHandler.js" /> + + <script>odoo.website_next_define = 'summernote/settings';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/settings.js" /> + + <script>odoo.website_next_define = 'summernote/summernote';</script> + <script type="text/javascript" src="/web_editor/static/lib/summernote/src/js/summernote.js" /> + + <script type="text/javascript" src="/web_editor/static/src/js/summernote.js" /> + <script type="text/javascript"> + window.define = odoo.__define__; + delete odoo.__define__; + delete odoo.website_next_define; + </script> + + <!--link rel="stylesheet" href="/web_editor/static/lib/summernote/src/less/summernote.less"/--> + <link rel="stylesheet" href="/web_editor/static/lib/summernote/summernote.css"/> +</template> + +<template id="editor" name="Editor"> + <link rel='stylesheet' href='/web_editor/static/src/css/snippets.css'/> + <link rel="stylesheet" href='/web_editor/static/src/css/editor.css'/> + <script type="text/javascript" src='/web_editor/static/lib/nearest/jquery.nearest.js' /> + <script type="text/javascript" src="/web_editor/static/src/js/editor.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/rte.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/rte.summernote.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/widgets.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/snippets.editor.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/snippets.options.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/tour.rte.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/transcoder.js"></script> + <script type="text/javascript" src="/web_editor/static/src/js/translator.js"></script> +</template> + +<template id="assets_common" inherit_id="web.assets_common" name="web_editor base assets_common"> + <xpath expr="." position="inside"> + <script type="text/javascript" src="/web_editor/static/src/js/base.js" /> + <script type="text/javascript" src="/web_editor/static/src/js/snippets.animation.js" /> + </xpath> +</template> + +<template id="assets_editor" name="web_editor assets for editor"> + <t t-call="web.assets_common"/> + <t t-call="web.bootstrap"/> + <script type="text/javascript" src="/web/static/lib/select2/select2.js"></script> + <link rel="stylesheet" href='/web_editor/static/src/css/web_editor.css'/> + <t t-call="web_editor.summernote"/> + <t t-call="web_editor.editor"/> +</template> + +<template id="assets_backend" name="web_editor assets for backend" inherit_id="web.assets_backend"> + <xpath expr="." position="inside"> + <t t-call="web_editor.summernote"/> + <t t-call="web_editor.editor"/> + <link rel="stylesheet" href="/web_editor/static/src/css/backend.css"/> + <script type="text/javascript" src="/web_editor/static/src/js/backend.js" /> + </xpath> +</template> + +<!-- color-picker --> +<!-- HTML class to hide option for one mode : only-text, only-bg --> + +<template id="web_editor.colorpicker" name="Color-Picker"> + <table class="colorpicker"> + <tr> + <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> + </table> +</template> + +</openerp> diff --git a/addons/web_editor/views/iframe.xml b/addons/web_editor/views/iframe.xml new file mode 100644 index 00000000000..32cbebd30ed --- /dev/null +++ b/addons/web_editor/views/iframe.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> + +<template id="layout" name="Editor layout"><!DOCTYPE html> + <html t-att-lang="lang and lang.replace('_', '-')" + t-att-data-editable="'1' if editable else None" + t-att-data-translatable="'1' if translatable else None" + t-att-data-edit_translations="'1' if edit_translations else None" + t-att-data-main-object="repr(main_object) if editable else None"> + <head> + <meta charset="utf-8" /> + <title>Odoo Editor layout</title> + <meta name="viewport" content="initial-scale=1"/> + <script type="text/javascript"> + var snippets_url = '<t t-esc="snippets or ''"/>'; + var callback = window.location.href.match(/callback=([^&=]+)/)[1]; + </script> + <link t-if="inline_mode" rel="stylesheet" href="/web_editor/static/src/css/inline.css" type="text/css"/> + <t t-if="not dont_load_assets"> + <t t-call-assets="web_editor.assets_editor"/> + <style> + .note-popover .popover { left: 5px !important; } + html[data-translatable] .note-popover .popover { left: 150px !important; } + </style> + <script type="text/javascript" src="/web/static/src/js/framework/session_instance.js"></script> + <script type="text/javascript" src="/web_editor/static/src/js/iframe.js"></script> + <script t-if="enable_editor and inline_mode" type="text/javascript" src="/web_editor/static/src/js/inline.js"></script> + </t> + <script type="text/javascript"> + var snippets_url = '<t t-esc="snippets or ''"/>'; + var callback = window.location.href.match(/callback=([^&=]+)/)[1]; + </script> + <script> + if (window.top.odoo && !window.top.odoo[callback+"_updown"]) { + window.top.odoo[callback+"_updown"] = function (value, fields_values) { + var editable = document.getElementsByClassName("o_editable")[0]; + if (value !== editable.innerHTML) { + editable.innerHTML = value; + } + }; + } + </script> + <t t-raw="head or ''"/> + </head> + <body onload="var fn=window.top.odoo && window.top.odoo[callback+'_content']; if(fn) {fn();} else {console.warn('Please don\'t open iframe without the odoo backend');}"> + <div id="wrapwrap"> + <main> + <t t-raw="0"/> + </main> + </div> + <t t-raw="add_html or ''"/> + </body> + </html> +</template> + +<template id="FieldTextHtml" name="Editor HTML"> + <t t-call="web_editor.layout"> + <div id="editable_area" t-att-class="'' if edit_translations else 'o_editable'"></div> + </t> +</template> + +</openerp> diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml new file mode 100644 index 00000000000..4499187f6a7 --- /dev/null +++ b/addons/web_editor/views/snippets.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<openerp> +<data> +<template id="snippets"> + + +<ul> + <li class="active"> + <a href="#snippet_structure" class="scroll-link"> + <span class="fa-stack fa-lg"> + <i class="fa fa-square fa-stack-2x fa-inverse"></i> + </span> + </a> + </li> +</ul> + + +<div id="o_scroll"> + <div id="snippet_structure" class="o_panel"> + <div class="o_panel_header"> + <span class="fa-stack"> + <i class="fa fa-th-large fa-stack-1x"></i> + </span> First Panel + </div> + <div class="o_panel_body"> + + </div> + </div> +</div> + + +<div id="snippet_options" class="hidden"> + <t t-call="web_editor.snippet_options"/> +</div> +</template> + +<!-- options (data-selector, data-drop-in, data-drop-near, data-js to link js object ) --> + +<template id="snippet_options"> + <!-- media --> + + <div data-js='media' + data-selector="img, .media_iframe_video, span.fa, i.fa, .glyphicon"> + <li data-edition="edition"><a>Change...</a></li> + </div> + + <!-- t-field --> + + <div data-js='many2one' + data-selector="[data-oe-many2one-model]:not([data-oe-readonly])" + data-no-check="true"> + </div> +</template> + +</data> +</openerp> diff --git a/addons/web_tests/static/src/js/web.tour.js b/addons/web_tests/static/src/js/web.tour.js index bc93b1ba079..9ec5905213b 100644 --- a/addons/web_tests/static/src/js/web.tour.js +++ b/addons/web_tests/static/src/js/web.tour.js @@ -303,7 +303,7 @@ Tour.register({ }, { title: "select demo user", - element: '.modal li a:contains(Demo User)', + element: '.modal li:contains(Demo User)', }, { title: "test one2many's line onchange after many2one", @@ -399,7 +399,7 @@ Tour.register({ }, { title: "select an other user", - element: '.oe_application li a:contains(Demo User)', + element: '.ui-autocomplete li:contains(Demo User)', }, { title: "test one2many's line onchange after many2one", @@ -433,7 +433,7 @@ Tour.register({ }, { title: "add a tag", - element: '.ui-autocomplete a:first', + element: '.ui-autocomplete li:first', }, // remove record diff --git a/addons/website/__openerp__.py b/addons/website/__openerp__.py index 4a40d869b0e..19cf30928da 100644 --- a/addons/website/__openerp__.py +++ b/addons/website/__openerp__.py @@ -11,7 +11,7 @@ OpenERP Website CMS """, 'author': 'OpenERP SA', - 'depends': ['web', 'web_planner'], + 'depends': ['web', 'web_editor', 'web_planner'], 'installable': True, 'data': [ 'data/data.xml', diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 9f01f71b2a0..a755cb249b4 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -import cStringIO import datetime from itertools import islice import json @@ -11,13 +10,12 @@ import re import werkzeug.utils import urllib2 import werkzeug.wrappers -from PIL import Image import openerp from openerp.addons.web.controllers.main import WebClient +from openerp.addons.web_editor.controllers.main import Web_Editor from openerp.addons.web import http -from openerp.http import request, STATIC_CACHE -from openerp.tools import image_save_for_web +from openerp.http import request logger = logging.getLogger(__name__) @@ -199,7 +197,7 @@ class Website(openerp.addons.web.controllers.main.Home): return werkzeug.wrappers.Response(url, mimetype='text/plain') return werkzeug.utils.redirect(url + "?enable_editor=1") - @http.route(['/website/snippets'], type='json', auth="public", website=True) + @http.route(['/website/snippets'], type='json', auth="user", website=True) def snippets(self): return request.website._render('website.snippets') @@ -234,111 +232,16 @@ class Website(openerp.addons.web.controllers.main.Home): return request.registry["ir.ui.view"].customize_template_get( request.cr, request.uid, key, full=full, bundles=bundles, context=request.context) - @http.route('/website/get_view_translations', type='json', auth='public', website=True) - def get_view_translations(self, xml_id, lang=None): - lang = lang or request.context.get('lang') - return request.registry["ir.ui.view"].get_view_translations( - request.cr, request.uid, xml_id, lang=lang, context=request.context) - - @http.route('/website/set_translations', type='json', auth='public', website=True) - def set_translations(self, data, lang): - irt = request.registry.get('ir.translation') - for view_id, trans in data.items(): - view_id = int(view_id) - for t in trans: - initial_content = t['initial_content'].strip() - new_content = t['new_content'].strip() - tid = t['translation_id'] - if not tid: - old_trans = irt.search_read( - request.cr, request.uid, - [ - ('type', '=', 'model'), - ('name', '=', 'ir.ui.view,arch_db'), - ('res_id', '=', view_id), - ('lang', '=', lang), - ('src', '=', initial_content), - ]) - if old_trans: - tid = old_trans[0]['id'] - if tid: - vals = {'value': new_content, 'state': 'translated'} - irt.write(request.cr, request.uid, [tid], vals) - else: - new_trans = { - 'type': 'model', - 'name': 'ir.ui.view,arch_db', - 'state': 'translated', - 'res_id': view_id, - 'lang': lang, - 'src': initial_content, - 'value': new_content, - } - if t.get('gengo_translation'): - new_trans['gengo_translation'] = t.get('gengo_translation') - new_trans['gengo_comment'] = t.get('gengo_comment') - irt.create(request.cr, request.uid, new_trans) - return True @http.route('/website/translations', type='json', auth="public", website=True) - def get_website_translations(self, lang): + def get_website_translations(self, lang, mods=None): module_obj = request.registry['ir.module.module'] module_ids = module_obj.search(request.cr, request.uid, [('name', 'ilike', 'website'), ('state', '=', 'installed')], context=request.context) modules = [x['name'] for x in module_obj.read(request.cr, request.uid, module_ids, ['name'], context=request.context)] + if mods: + modules += mods return WebClient().translations(mods=modules, lang=lang) - @http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True) - def attach(self, func, upload=None, url=None, disable_optimization=None): - # the upload argument doesn't allow us to access the files if more than - # one file is uploaded, as upload references the first file - # therefore we have to recover the files from the request object - Attachments = request.registry['ir.attachment'] # registry for the attachment table - - uploads = [] - message = None - if not upload: # no image provided, storing the link and the image name - uploads.append({'website_url': url}) - name = url.split("/").pop() # recover filename - attachment_id = Attachments.create(request.cr, request.uid, { - 'name': name, - 'type': 'url', - 'url': url, - 'res_model': 'ir.ui.view', - }, request.context) - else: # images provided - try: - attachment_ids = [] - for c_file in request.httprequest.files.getlist('upload'): - image_data = c_file.read() - image = Image.open(cStringIO.StringIO(image_data)) - w, h = image.size - if w*h > 42e6: # Nokia Lumia 1020 photo resolution - raise ValueError( - u"Image size excessive, uploaded images must be smaller " - u"than 42 million pixel") - - if not disable_optimization and image.format in ('PNG', 'JPEG'): - image_data = image_save_for_web(image) - - attachment_id = Attachments.create(request.cr, request.uid, { - 'name': c_file.filename, - 'datas': image_data.encode('base64'), - 'datas_fname': c_file.filename, - 'res_model': 'ir.ui.view', - }, request.context) - attachment_ids.append(attachment_id) - - uploads = Attachments.read( - request.cr, request.uid, attachment_ids, ['website_url'], - context=request.context) - except Exception, e: - logger.exception("Failed to upload image to attachment") - message = unicode(e) - - return """<script type='text/javascript'> - window.parent['%s'](%s, %s); - </script>""" % (func, json.dumps(uploads), json.dumps(message)) - @http.route(['/website/publish'], type='json', auth="public", website=True) def publish(self, id, object): _id = int(id) @@ -432,63 +335,6 @@ class Website(openerp.addons.web.controllers.main.Home): def kanban(self, **post): return request.website.kanban_col(**post) - def placeholder(self, response): - return request.registry['website']._image_placeholder(response) - - @http.route([ - '/website/image', - '/website/image/<xmlid>', - '/website/image/<xmlid>/<int:max_width>x<int:max_height>', - '/website/image/<xmlid>/<field>', - '/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.ref(xmlid) - model, id = record._name, record.id - except: - raise werkzeug.exceptions.NotFound() - if model == 'ir.attachment' and not field: - if record.sudo().type == "url": - field = "url" - else: - field = "datas" - - if not model or not id or not field: - raise werkzeug.exceptions.NotFound() - - try: - idsha = str(id).split('_') - id = idsha[0] - response = werkzeug.wrappers.Response() - return request.registry['website']._image( - request.cr, request.uid, model, id, field, response, max_width, max_height, - cache=STATIC_CACHE if len(idsha) > 1 else None) - except Exception: - logger.exception("Cannot render image field %r of record %s[%s] at size(%s,%s)", - field, model, id, max_width, max_height) - response = werkzeug.wrappers.Response() - return self.placeholder(response) - #------------------------------------------------------ # Server actions #------------------------------------------------------ @@ -529,21 +375,15 @@ class Website(openerp.addons.web.controllers.main.Home): return request.redirect('/') #------------------------------------------------------ - # Backend html field + # image route for browse record #------------------------------------------------------ - @http.route('/website/field/html', type='http', auth="public", website=True) - def FieldTextHtml(self, model=None, res_id=None, field=None, callback=None, **kwargs): - record = None - if model and res_id: - res_id = int(res_id) - record = request.registry[model].browse(request.cr, request.uid, res_id, request.context) - - datarecord = json.loads(kwargs['datarecord']) - kwargs.update({ - 'content': record and getattr(record, field) or "", - 'model': model, - 'res_id': res_id, - 'field': field, - 'datarecord': datarecord - }) - return request.website.render(kwargs.get("template") or "website.FieldTextHtml", kwargs) + @http.route([ + '/website/image', + '/website/image/<xmlid>', + '/website/image/<xmlid>/<field>', + '/website/image/<model>/<id>/<field>', + '/website/image/<model>/<id>/<field>/<int:max_width>x<int:max_height>' + ], type='http', auth="public") + def website_image(self, model=None, id=None, field=None, xmlid=None, max_width=None, max_height=None): + logger.warning("Deprecated image controller, please use /web_editor/image/") + return Web_Editor().image(model=model, id=id, field=field, xmlid=xmlid, max_width=max_width, max_height=max_height) diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index 67672babb4e..9815e0c8285 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -8,5 +8,3 @@ import ir_qweb import ir_http import res_config import web_planner - -import test_models diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py index 2668e5008d6..76e0d9a0945 100644 --- a/addons/website/models/ir_http.py +++ b/addons/website/models/ir_http.py @@ -157,7 +157,8 @@ class ir_http(orm.AbstractModel): elif url_lang: path.pop(1) return self.reroute('/'.join(path) or '/') - + if path[1] == request.website.default_lang_code: + request.context['edit_translations'] = False if not request.context.get('tz'): request.context['tz'] = request.session['geoip'].get('time_zone') # bind modified context diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py index d10e33a4708..ea0c6e33438 100644 --- a/addons/website/models/ir_qweb.py +++ b/addons/website/models/ir_qweb.py @@ -1,44 +1,11 @@ # -*- coding: utf-8 -*- -""" -Website-context rendering needs to add some metadata to rendered fields, -as well as render a few fields differently. - -Also, adds methods to convert values back to openerp models. -""" - -import cStringIO -import datetime -import itertools -import logging -import os -import urllib2 -import urlparse -import re - -import pytz -import werkzeug.urls -import werkzeug.utils -from dateutil import parser -from lxml import etree, html -from PIL import Image as I -import openerp.modules - -import openerp -from openerp.osv import orm, fields -from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT -from openerp.tools import html_escape as escape from openerp.addons.web.http import request -from openerp.addons.base.ir import ir_qweb -import simplejson +from openerp.osv import orm -REMOTE_CONNECTION_TIMEOUT = 2.5 - -logger = logging.getLogger(__name__) class QWeb(orm.AbstractModel): """ QWeb object for rendering stuff in the website context """ - _name = 'website.qweb' _inherit = 'ir.qweb' URL_ATTRS = { @@ -46,7 +13,12 @@ class QWeb(orm.AbstractModel): 'a': 'href', } - re_remove_spaces = re.compile('\s+') + CDN_TRIGGERS = { + 'link': 'href', + 'script': 'src', + 'img': 'src', + } + PRESERVE_WHITESPACE = [ 'pre', 'textarea', @@ -54,48 +26,15 @@ class QWeb(orm.AbstractModel): 'style', ] - CDN_TRIGGERS = { - 'link': 'href', - 'script': 'src', - 'img': 'src', - } - def render_attribute(self, element, name, value, qwebcontext): context = qwebcontext.context or {} if not context.get('rendering_bundle'): - if name == self.URL_ATTRS.get(element.tag): + if name == self.URL_ATTRS.get(element.tag) and qwebcontext.get('url_for'): value = qwebcontext.get('url_for')(value) - if request and request.website and request.website.cdn_activated and name == self.CDN_TRIGGERS.get(element.tag): + elif request and request.website and request.website.cdn_activated and (name == self.URL_ATTRS.get(element.tag) or name == self.CDN_TRIGGERS.get(element.tag)): value = request.website.get_cdn_url(value) - return super(QWeb, self).render_attribute(element, name, value, qwebcontext) - def render_tag_snippet(self, element, template_attributes, generated_attributes, qwebcontext): - cr = qwebcontext['request'].cr - uid = qwebcontext['request'].uid - view_reg = self.pool['ir.ui.view'] - page_xmlid = self.eval_format(template_attributes["snippet"], qwebcontext) - view = view_reg.browse(cr, uid, view_reg.search(cr, uid, [('key', '=', page_xmlid)])) - generated_attributes += " " + "".join([ - 'data-oe-name="%s"' % view.name, - 'data-oe-type="snippet"', - 'data-oe-thumbnail="%s"' % template_attributes.get('thumbnail', "oe-thumbnail")]) - - template_attributes['call'] = template_attributes['snippet'] - return self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext) - - def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext): - if request and request.website and request.website.cdn_activated: - if qwebcontext.context is None: - qwebcontext.context = {} - qwebcontext.context['url_for'] = request.website.get_cdn_url - return super(QWeb, self).render_tag_call_assets(element, template_attributes, generated_attributes, qwebcontext) - - def get_converter_for(self, field_type): - return self.pool.get( - 'website.qweb.field.' + field_type, - self.pool['website.qweb.field']) - def render_text(self, text, element, qwebcontext): compress = request and not request.debug and request.website and request.website.compress_html if compress and element.tag not in self.PRESERVE_WHITESPACE: @@ -108,482 +47,3 @@ class QWeb(orm.AbstractModel): # No need to recurse because those tags children are not html5 parser friendly tail = self.re_remove_spaces.sub(' ', tail.rstrip()) return super(QWeb, self).render_tail(tail, element, qwebcontext) - -class Field(orm.AbstractModel): - _name = 'website.qweb.field' - _inherit = 'ir.qweb.field' - - def attributes(self, cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context, context=None): - if options is None: options = {} - field = record._model._fields[field_name] - attrs = [('data-oe-translate', 1 if getattr(field, 'translate', False) else 0)] - - placeholder = options.get('placeholder') \ - or source_element.get('placeholder') \ - or getattr(field, 'placeholder', None) - if placeholder: - attrs.append(('placeholder', placeholder)) - - return itertools.chain( - super(Field, self).attributes(cr, uid, field_name, record, options, - source_element, g_att, t_att, - qweb_context, context=context), - attrs - ) - - def value_from_string(self, value): - return value - - def from_html(self, cr, uid, model, field, element, context=None): - return self.value_from_string(element.text_content().strip()) - - def qweb_object(self): - return self.pool['website.qweb'] - -class Integer(orm.AbstractModel): - _name = 'website.qweb.field.integer' - _inherit = ['website.qweb.field'] - - value_from_string = int - -class Float(orm.AbstractModel): - _name = 'website.qweb.field.float' - _inherit = ['website.qweb.field', 'ir.qweb.field.float'] - - def from_html(self, cr, uid, model, field, element, context=None): - lang = self.user_lang(cr, uid, context=context) - - value = element.text_content().strip() - - return float(value.replace(lang.thousands_sep, '') - .replace(lang.decimal_point, '.')) - - -def parse_fuzzy(in_format, value): - day_first = in_format.find('%d') < in_format.find('%m') - - if '%y' in in_format: - year_first = in_format.find('%y') < in_format.find('%d') - else: - year_first = in_format.find('%Y') < in_format.find('%d') - - return parser.parse(value, dayfirst=day_first, yearfirst=year_first) - -class Date(orm.AbstractModel): - _name = 'website.qweb.field.date' - _inherit = ['website.qweb.field', 'ir.qweb.field.date'] - - def attributes(self, cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context, - context=None): - attrs = super(Date, self).attributes( - cr, uid, field_name, record, options, source_element, g_att, t_att, - qweb_context, context=None) - return itertools.chain(attrs, [('data-oe-original', record[field_name])]) - - def from_html(self, cr, uid, model, field, element, context=None): - value = element.text_content().strip() - if not value: return False - - datetime.datetime.strptime(value, DEFAULT_SERVER_DATE_FORMAT) - return value - -class DateTime(orm.AbstractModel): - _name = 'website.qweb.field.datetime' - _inherit = ['website.qweb.field', 'ir.qweb.field.datetime'] - - def attributes(self, cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context, - context=None): - value = record[field_name] - if isinstance(value, basestring): - value = datetime.datetime.strptime( - value, DEFAULT_SERVER_DATETIME_FORMAT) - if value: - # convert from UTC (server timezone) to user timezone - value = fields.datetime.context_timestamp( - cr, uid, timestamp=value, context=context) - value = value.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - - attrs = super(DateTime, self).attributes( - cr, uid, field_name, record, options, source_element, g_att, t_att, - qweb_context, context=None) - return itertools.chain(attrs, [ - ('data-oe-original', value) - ]) - - def from_html(self, cr, uid, model, field, element, context=None): - if context is None: context = {} - value = element.text_content().strip() - if not value: return False - - # parse from string to datetime - dt = datetime.datetime.strptime(value, DEFAULT_SERVER_DATETIME_FORMAT) - - # convert back from user's timezone to UTC - tz_name = context.get('tz') \ - or self.pool['res.users'].read(cr, openerp.SUPERUSER_ID, uid, ['tz'], context=context)['tz'] - if tz_name: - try: - user_tz = pytz.timezone(tz_name) - utc = pytz.utc - - dt = user_tz.localize(dt).astimezone(utc) - except Exception: - logger.warn( - "Failed to convert the value for a field of the model" - " %s back from the user's timezone (%s) to UTC", - model, tz_name, - exc_info=True) - - # format back to string - return dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - -class Text(orm.AbstractModel): - _name = 'website.qweb.field.text' - _inherit = ['website.qweb.field', 'ir.qweb.field.text'] - - def from_html(self, cr, uid, model, field, element, context=None): - return html_to_text(element) - -class Selection(orm.AbstractModel): - _name = 'website.qweb.field.selection' - _inherit = ['website.qweb.field', 'ir.qweb.field.selection'] - - def from_html(self, cr, uid, model, field, element, context=None): - record = self.browse(cr, uid, [], context=context) - value = element.text_content().strip() - selection = field.get_description(record.env)['selection'] - for k, v in selection: - if isinstance(v, str): - v = ustr(v) - if value == v: - return k - - raise ValueError(u"No value found for label %s in selection %s" % ( - value, selection)) - -class ManyToOne(orm.AbstractModel): - _name = 'website.qweb.field.many2one' - _inherit = ['website.qweb.field', 'ir.qweb.field.many2one'] - - def attributes(self, cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context, - context=None): - attrs = super(ManyToOne, self).attributes( - cr, uid, field_name, record, options, source_element, g_att, t_att, - qweb_context, context=context) - many2one = getattr(record, field_name) - if many2one: - return itertools.chain(attrs, [ - ('data-oe-many2one-id', many2one.id), - ('data-oe-many2one-model', many2one._name)]) - else: - return attrs - - def from_html(self, cr, uid, model, field, element, context=None): - Model = self.pool[element.get('data-oe-model')] - M2O = self.pool[field.comodel_name] - field_name = element.get('data-oe-field') - id = int(element.get('data-oe-id')) - many2one_id = int(element.get('data-oe-many2one-id')) - - if many2one_id and M2O.exists(cr, uid, [many2one_id]): - # save the new id of the many2one - Model.write(cr, uid, [id], { - field_name: many2one_id - }, context=context) - - # not necessary, but might as well be explicit about it - return None - -class HTML(orm.AbstractModel): - _name = 'website.qweb.field.html' - _inherit = ['website.qweb.field', 'ir.qweb.field.html'] - - def from_html(self, cr, uid, model, field, element, context=None): - content = [] - if element.text: content.append(element.text) - content.extend(html.tostring(child) - for child in element.iterchildren(tag=etree.Element)) - return '\n'.join(content) - -class Image(orm.AbstractModel): - """ - Widget options: - - ``class`` - set as attribute on the generated <img> tag - """ - _name = 'website.qweb.field.image' - _inherit = ['website.qweb.field', 'ir.qweb.field.image'] - - def to_html(self, cr, uid, field_name, record, options, - source_element, t_att, g_att, qweb_context, context=None): - assert source_element.tag != 'img',\ - "Oddly enough, the root tag of an image field can not be img. " \ - "That is because the image goes into the tag, or it gets the " \ - "hose again." - - return super(Image, self).to_html( - cr, uid, field_name, record, options, - source_element, t_att, g_att, qweb_context, context=context) - - def record_to_html(self, cr, uid, field_name, record, options=None, context=None): - if options is None: options = {} - aclasses = ['img', 'img-responsive'] + options.get('class', '').split() - classes = ' '.join(itertools.imap(escape, aclasses)) - - max_size = None - max_width, max_height = options.get('max_width', 0), options.get('max_height', 0) - if max_width or max_height: - max_size = '%sx%s' % (max_width, max_height) - - src = self.pool['website'].image_url(cr, uid, record, field_name, max_size) - if options.get('resize'): - src = "%s/%s" % (src, options.get('resize')) - - img = '<img class="%s" src="%s" style="%s"/>' % (classes, src, options.get('style', '')) - return ir_qweb.HTMLSafe(img) - - local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$') - - def from_html(self, cr, uid, model, field, element, context=None): - url = element.find('img').get('src') - - url_object = urlparse.urlsplit(url) - if url_object.path.startswith('/website/image'): - # url might be /website/image/<model>/<id>[_<checksum>]/<field>[/<width>x<height>] - fragments = url_object.path.split('/') - query = dict(urlparse.parse_qsl(url_object.query)) - model = query.get('model', fragments[3]) - oid = query.get('id', fragments[4].split('_')[0]) - field = query.get('field', fragments[5]) - item = self.pool[model].browse(cr, uid, int(oid), context=context) - return item[field] - - if self.local_url_re.match(url_object.path): - return self.load_local_url(url) - - return self.load_remote_url(url) - - def load_local_url(self, url): - match = self.local_url_re.match(urlparse.urlsplit(url).path) - - rest = match.group('rest') - for sep in os.sep, os.altsep: - if sep and sep != '/': - rest.replace(sep, '/') - - path = openerp.modules.get_module_resource( - match.group('module'), 'static', *(rest.split('/'))) - - if not path: - return None - - try: - with open(path, 'rb') as f: - # force complete image load to ensure it's valid image data - image = I.open(f) - image.load() - f.seek(0) - return f.read().encode('base64') - except Exception: - logger.exception("Failed to load local image %r", url) - return None - - def load_remote_url(self, url): - try: - # should probably remove remote URLs entirely: - # * in fields, downloading them without blowing up the server is a - # challenge - # * in views, may trigger mixed content warnings if HTTPS CMS - # linking to HTTP images - # implement drag & drop image upload to mitigate? - - req = urllib2.urlopen(url, timeout=REMOTE_CONNECTION_TIMEOUT) - # PIL needs a seekable file-like image, urllib result is not seekable - image = I.open(cStringIO.StringIO(req.read())) - # force a complete load of the image data to validate it - image.load() - except Exception: - logger.exception("Failed to load remote image %r", url) - return None - - # don't use original data in case weird stuff was smuggled in, with - # luck PIL will remove some of it? - out = cStringIO.StringIO() - image.save(out, image.format) - return out.getvalue().encode('base64') - -class Monetary(orm.AbstractModel): - _name = 'website.qweb.field.monetary' - _inherit = ['website.qweb.field', 'ir.qweb.field.monetary'] - - def from_html(self, cr, uid, model, field, element, context=None): - lang = self.user_lang(cr, uid, context=context) - - value = element.find('span').text.strip() - - return float(value.replace(lang.thousands_sep, '') - .replace(lang.decimal_point, '.')) - -class Duration(orm.AbstractModel): - _name = 'website.qweb.field.duration' - _inherit = [ - 'ir.qweb.field.duration', - 'website.qweb.field.float', - ] - - def attributes(self, cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context, - context=None): - attrs = super(Duration, self).attributes( - cr, uid, field_name, record, options, source_element, g_att, t_att, - qweb_context, context=None) - return itertools.chain(attrs, [('data-oe-original', record[field_name])]) - - def from_html(self, cr, uid, model, field, element, context=None): - value = element.text_content().strip() - - # non-localized value - return float(value) - - -class RelativeDatetime(orm.AbstractModel): - _name = 'website.qweb.field.relative' - _inherit = [ - 'ir.qweb.field.relative', - 'website.qweb.field.datetime', - ] - - # get formatting from ir.qweb.field.relative but edition/save from datetime - - -class Contact(orm.AbstractModel): - _name = 'website.qweb.field.contact' - _inherit = ['ir.qweb.field.contact', 'website.qweb.field.many2one'] - - def attributes(self, cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context, - context=None): - attrs = super(Contact, self).attributes( - cr, uid, field_name, record, options, source_element, g_att, t_att, - qweb_context, context=context) - many2one = getattr(record, field_name) - if many2one: - return itertools.chain(attrs, [('data-oe-contact-options', simplejson.dumps(options))]) - else: - return attrs - - # helper to call the rendering of contact field in website - def get_record_to_html(self, cr, uid, ids, options=None, context=None): - node = self.record_to_html(cr, uid, - 'record', {'record': self.pool['res.partner'].browse(cr, uid, ids[0], context=context)}, - options=options, context=context) - return node and node.__html__() - -class QwebView(orm.AbstractModel): - _name = 'website.qweb.field.qweb' - _inherit = ['ir.qweb.field.qweb'] - - -def html_to_text(element): - """ Converts HTML content with HTML-specified line breaks (br, p, div, ...) - in roughly equivalent textual content. - - Used to replace and fixup the roundtripping of text and m2o: when using - libxml 2.8.0 (but not 2.9.1) and parsing HTML with lxml.html.fromstring - whitespace text nodes (text nodes composed *solely* of whitespace) are - stripped out with no recourse, and fundamentally relying on newlines - being in the text (e.g. inserted during user edition) is probably poor form - anyway. - - -> this utility function collapses whitespace sequences and replaces - nodes by roughly corresponding linebreaks - * p are pre-and post-fixed by 2 newlines - * br are replaced by a single newline - * block-level elements not already mentioned are pre- and post-fixed by - a single newline - - ought be somewhat similar (but much less high-tech) to aaronsw's html2text. - the latter produces full-blown markdown, our text -> html converter only - replaces newlines by <br> elements at this point so we're reverting that, - and a few more newline-ish elements in case the user tried to add - newlines/paragraphs into the text field - - :param element: lxml.html content - :returns: corresponding pure-text output - """ - - # output is a list of str | int. Integers are padding requests (in minimum - # number of newlines). When multiple padding requests, fold them into the - # biggest one - output = [] - _wrap(element, output) - - # remove any leading or tailing whitespace, replace sequences of - # (whitespace)\n(whitespace) by a single newline, where (whitespace) is a - # non-newline whitespace in this case - return re.sub( - r'[ \t\r\f]*\n[ \t\r\f]*', - '\n', - ''.join(_realize_padding(output)).strip()) - -_PADDED_BLOCK = set('p h1 h2 h3 h4 h5 h6'.split()) -# https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements minus p -_MISC_BLOCK = set(( - 'address article aside audio blockquote canvas dd dl div figcaption figure' - ' footer form header hgroup hr ol output pre section tfoot ul video' -).split()) - -def _collapse_whitespace(text): - """ Collapses sequences of whitespace characters in ``text`` to a single - space - """ - return re.sub('\s+', ' ', text) -def _realize_padding(it): - """ Fold and convert padding requests: integers in the output sequence are - requests for at least n newlines of padding. Runs thereof can be collapsed - into the largest requests and converted to newlines. - """ - padding = None - for item in it: - if isinstance(item, int): - padding = max(padding, item) - continue - - if padding: - yield '\n' * padding - padding = None - - yield item - # leftover padding irrelevant as the output will be stripped - -def _wrap(element, output, wrapper=u''): - """ Recursively extracts text from ``element`` (via _element_to_text), and - wraps it all in ``wrapper``. Extracted text is added to ``output`` - - :type wrapper: basestring | int - """ - output.append(wrapper) - if element.text: - output.append(_collapse_whitespace(element.text)) - for child in element: - _element_to_text(child, output) - output.append(wrapper) - -def _element_to_text(e, output): - if e.tag == 'br': - output.append(u'\n') - elif e.tag in _PADDED_BLOCK: - _wrap(e, output, 2) - elif e.tag in _MISC_BLOCK: - _wrap(e, output, 1) - else: - # inline - _wrap(e, output) - - if e.tail: - output.append(_collapse_whitespace(e.tail)) diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index c63f67a191c..38c34c1bc08 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -104,55 +104,7 @@ class view(osv.osv): result.append(r) return result - def extract_embedded_fields(self, cr, uid, arch, context=None): - return arch.xpath('//*[@data-oe-model != "ir.ui.view"]') - - def save_embedded_field(self, cr, uid, el, context=None): - Model = self.pool[el.get('data-oe-model')] - field = el.get('data-oe-field') - - converter = self.pool['website.qweb'].get_converter_for(el.get('data-oe-type')) - value = converter.from_html(cr, uid, Model, Model._fields[field], el) - - if value is not None: - # TODO: batch writes? - Model.write(cr, uid, [int(el.get('data-oe-id'))], { - field: value - }, context=context) - - def to_field_ref(self, cr, uid, el, context=None): - # filter out meta-information inserted in the document - attributes = dict((k, v) for k, v in el.items() - if not k.startswith('data-oe-')) - attributes['t-field'] = el.get('data-oe-expression') - - out = html.html_parser.makeelement(el.tag, attrib=attributes) - out.tail = el.tail - return out - - def replace_arch_section(self, cr, uid, view_id, section_xpath, replacement, context=None): - # the root of the arch section shouldn't actually be replaced as it's - # not really editable itself, only the content truly is editable. - - [view] = self.browse(cr, uid, [view_id], context=context) - arch = etree.fromstring(view.arch.encode('utf-8')) - # => get the replacement root - if not section_xpath: - root = arch - else: - # ensure there's only one match - [root] = arch.xpath(section_xpath) - - root.text = replacement.text - root.tail = replacement.tail - # replace all children - del root[:] - for child in replacement: - root.append(copy.deepcopy(child)) - - return arch - - @tools.ormcache_context('uid', 'xml_id', keys=('website_id',)) + @tools.ormcache_context('uid', 'xml_id', keys=('website_id', 'editable', 'translatable', 'edit_translations')) def get_view_id(self, cr, uid, xml_id, context=None): if context and 'website_id' in context and not isinstance(xml_id, (int, long)): domain = [('key', '=', xml_id), '|', ('website_id', '=', context['website_id']), ('website_id', '=', False)] @@ -163,29 +115,10 @@ class view(osv.osv): view_id = super(view, self).get_view_id(cr, uid, xml_id, context=context) return view_id - def _prepare_qcontext(self, cr, uid, context=None): - if not context: - context = {} - - company = self.pool['res.company'].browse(cr, SUPERUSER_ID, request.website.company_id.id, context=context) - - qcontext = dict( - context.copy(), - website=request.website, - url_for=website.url_for, - slug=website.slug, - res_company=company, - user_id=self.pool.get("res.users").browse(cr, uid, uid), - translatable=context.get('lang') != request.website.default_lang_code, - editable=request.website.is_publisher(), - menu_data=self.pool['ir.ui.menu'].load_menus_root(cr, uid, context=context) if request.website.is_user() else None, - ) - return qcontext - @api.cr_uid_ids_context def render(self, cr, uid, id_or_xml_id, values=None, engine='ir.qweb', context=None): if request and getattr(request, 'website_enabled', False): - engine = 'website.qweb' + engine = 'ir.qweb' if isinstance(id_or_xml_id, list): id_or_xml_id = id_or_xml_id[0] @@ -197,10 +130,11 @@ class view(osv.osv): qcontext.update(values) # in edit mode ir.ui.view will tag nodes - if qcontext.get('editable'): - context = dict(context, inherit_branding=True) - elif request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher'): - context = dict(context, inherit_branding_auto=True) + if not qcontext.get('translatable'): + if qcontext.get('editable'): + context = dict(context, inherit_branding=True) + elif request.registry['res.users'].has_group(cr, uid, 'base.group_website_publisher'): + context = dict(context, inherit_branding_auto=True) view_obj = request.website.get_template(id_or_xml_id) if 'main_object' not in qcontext: @@ -210,49 +144,30 @@ class view(osv.osv): return super(view, self).render(cr, uid, id_or_xml_id, values=values, engine=engine, context=context) - def _pretty_arch(self, arch): - # remove_blank_string does not seem to work on HTMLParser, and - # pretty-printing with lxml more or less requires stripping - # whitespace: http://lxml.de/FAQ.html#why-doesn-t-the-pretty-print-option-reformat-my-xml-output - # so serialize to XML, parse as XML (remove whitespace) then serialize - # as XML (pretty print) - arch_no_whitespace = etree.fromstring( - etree.tostring(arch, encoding='utf-8'), - parser=etree.XMLParser(encoding='utf-8', remove_blank_text=True)) - return etree.tostring( - arch_no_whitespace, encoding='unicode', pretty_print=True) - - def save(self, cr, uid, res_id, value, xpath=None, context=None): - """ Update a view section. The view section may embed fields to write - - :param str model: - :param int res_id: - :param str xpath: valid xpath to the tag to replace - """ - res_id = int(res_id) - - arch_section = html.fromstring( - value, parser=html.HTMLParser(encoding='utf-8')) - - if xpath is None: - # value is an embedded field on its own, not a view section - self.save_embedded_field(cr, uid, arch_section, context=context) - return - - for el in self.extract_embedded_fields(cr, uid, arch_section, context=context): - self.save_embedded_field(cr, uid, el, context=context) + def _prepare_qcontext(self, cr, uid, context=None): + if not context: + context = {} - # transform embedded field back to t-field - el.getparent().replace(el, self.to_field_ref(cr, uid, el, context=context)) + company = self.pool['res.company'].browse(cr, SUPERUSER_ID, request.website.company_id.id, context=context) - arch = self.replace_arch_section(cr, uid, res_id, xpath, arch_section, context=context) - self.write(cr, uid, res_id, { - 'arch': self._pretty_arch(arch) - }, context=context) + editable = request.website.is_publisher() + translatable = editable and context.get('lang') != request.website.default_lang_code + editable = not translatable and editable - view = self.browse(cr, SUPERUSER_ID, res_id, context=context) - if view.model_data_id: - view.model_data_id.write({'noupdate': True}) + qcontext = dict( + context.copy(), + website=request.website, + url_for=website.url_for, + slug=website.slug, + res_company=company, + user_id=self.pool.get("res.users").browse(cr, uid, uid), + default_lang_code=request.website.default_lang_code, + languages=request.website.get_languages(), + translatable=translatable, + editable=editable, + menu_data=self.pool['ir.ui.menu'].load_menus_root(cr, uid, context=context) if request.website.is_user() else None, + ) + return qcontext def customize_template_get(self, cr, uid, key, full=False, bundles=False, context=None): """ Get inherit view's informations of the template ``key``. By default, only @@ -292,10 +207,3 @@ class view(osv.osv): 'active': v.active, }) return result - - def get_view_translations(self, cr, uid, xml_id, lang, field=['id', 'res_id', 'value', 'state', 'gengo_translation'], context=None): - views = self.customize_template_get(cr, uid, xml_id, full=True, context=context) - views_ids = [view.get('id') for view in views if view.get('active')] - domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)] - return self.pool['ir.translation'].search_read(cr, uid, domain, field, context=context) - diff --git a/addons/website/models/website.py b/addons/website/models/website.py index 6fbb17f9d47..09c75336ed6 100644 --- a/addons/website/models/website.py +++ b/addons/website/models/website.py @@ -1,18 +1,11 @@ # -*- coding: utf-8 -*- -import cStringIO -import contextlib -import datetime -import hashlib import inspect import logging import math import unicodedata -import os import re -import time import urlparse -from PIL import Image from sys import maxint import werkzeug @@ -130,6 +123,7 @@ DEFAULT_CDN_FILTERS = [ "^/[^/]+/static/", "^/web/(css|js)/", "^/website/image/", + "^/web_editor/image/", ] def unslug(s): @@ -630,123 +624,15 @@ class website(osv.osv): return html def _image_placeholder(self, response): - # file_open may return a StringIO. StringIO can be closed but are - # not context managers in Python 2 though that is fixed in 3 - with contextlib.closing(openerp.tools.misc.file_open( - os.path.join('web', 'static', 'src', 'img', 'placeholder.png'), - mode='rb')) as f: - response.data = f.read() - return response.make_conditional(request.httprequest) + logger.warning("Deprecated _image_placeholder method, please use this method on ir.attachment") + return self.pool['ir.attachment']._image_placeholder(response) 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' and field in record: - path = record[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) - 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 + logger.warning("Deprecated _image method, please use this method on ir.attachment") + return self.pool['ir.attachment']._image(cr, uid, model, id, field, response, max_width=max_width, max_height=max_height, cache=cache, context=context) def image_url(self, cr, uid, record, field, size=None, context=None): - """Returns a local url that points to the image field of a given browse record.""" - model = record._name - sudo_record = record.sudo() - id = '%s_%s' % (record.id, hashlib.sha1(sudo_record.write_date or sudo_record.create_date or '').hexdigest()[0:7]) - size = '' if size is None else '/%s' % size - return '/website/image/%s/%s/%s%s' % (model, id, field, size) + return self.pool['ir.attachment'].image_url(cr, uid, record, field, size=size, context=context) class website_menu(osv.osv): _name = "website.menu" @@ -819,64 +705,22 @@ class website_menu(osv.osv): class ir_attachment(osv.osv): - _inherit = "ir.attachment" - def _website_url_get(self, cr, uid, ids, name, arg, context=None): - result = {} - for attach in self.browse(cr, uid, ids, context=context): - if attach.url: - result[attach.id] = attach.url - else: - result[attach.id] = self.pool['website'].image_url(cr, uid, attach, 'datas') - return result - - def _datas_big(self, cr, uid, ids, name, arg, context=None): - result = dict.fromkeys(ids, False) - if context and context.get('bin_size'): - return result - - for record in self.browse(cr, uid, ids, context=context): - if record.res_model != 'ir.ui.view' or not record.datas: continue - try: - result[record.id] = openerp.tools.image_resize_image_big(record.datas) - except IOError: # apparently the error PIL.Image.open raises - pass - - return result - _columns = { - 'website_url': fields.function(_website_url_get, string="Attachment URL", type='char'), - 'datas_big': fields.function (_datas_big, type='binary', store=True, - string="Resized file content"), + 'website_url': fields.related("local_url", string="Attachment URL", type='char', deprecated=True), # related for backward compatibility with saas-6 } - def try_remove(self, cr, uid, ids, context=None): - """ Removes a web-based image attachment if it is used by no view - (template) + def _image(self, cr, uid, model, id_or_ids, field, response, max_width=maxint, max_height=maxint, cache=None, context=None): + Model = self.pool[model] + ids = isinstance(id_or_ids, (list, tuple)) and id_or_ids or [int(id_or_ids)] + ids = Model.search(cr, uid, [('id', 'in', ids)], context=context) - Returns a dict mapping attachments which would not be removed (if any) - mapped to the views preventing their removal - """ - Views = self.pool['ir.ui.view'] - attachments_to_remove = [] - # views blocking removal of the attachment - removal_blocked_by = {} - - for attachment in self.browse(cr, uid, ids, context=context): - # in-document URLs are html-escaped, a straight search will not - # find them - url = escape(attachment.website_url) - ids = Views.search(cr, uid, ["|", ('arch_db', 'like', '"%s"' % url), ('arch_db', 'like', "'%s'" % url)], context=context) - - if ids: - removal_blocked_by[attachment.id] = Views.read( - cr, uid, ids, ['name'], context=context) - else: - attachments_to_remove.append(attachment.id) - if attachments_to_remove: - self.unlink(cr, uid, attachments_to_remove, context=context) - return removal_blocked_by + if not ids and 'website_published' in Model._fields: + ids = Model.search(cr, openerp.SUPERUSER_ID, [('id', 'in', ids), ('website_published', '=', True)], context=context) + + return super(ir_attachment, self)._image(cr, openerp.SUPERUSER_ID, model, id_or_ids, field, response, + max_width=max_width, max_height=max_height, cache=cache, context=context) class res_partner(osv.osv): _inherit = "res.partner" diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index 8682a349f70..4c91ee15c85 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -4,8 +4,6 @@ access_website,website,website.model_website,base.group_website_designer,1,1,1,1 access_website_menu,access_website_menu,model_website_menu,,1,0,0,0 access_website_menu_designer,Web Menu Manager,model_website_menu,base.group_website_designer,1,1,1,1 access_website,web menu manager,website.model_website,base.group_website_designer,1,1,1,1 -access_website_converter_test,access_website_converter_test,model_website_converter_test,,1,1,1,1 -access_website_converter_test_sub,access_website_converter_test_sub,model_website_converter_test_sub,,1,1,1,1 access_website_ir_ui_view,access_website_ir_ui_view,model_ir_ui_view,base.group_website_designer,1,1,1,1 access_seo_public,access_seo_public,model_website_seo_metadata,,1,0,0,0 access_seo_public,access_seo_manager,model_website_seo_metadata,base.group_website_designer,1,1,1,1 diff --git a/addons/website/static/lib/summernote/src/js/Renderer.js b/addons/website/static/lib/summernote/src/js/Renderer.js index 9d0f3c7c689..07584edc453 100644 --- a/addons/website/static/lib/summernote/src/js/Renderer.js +++ b/addons/website/static/lib/summernote/src/js/Renderer.js @@ -884,7 +884,7 @@ define([ this.removeLayout = function ($holder, layoutInfo, options) { if (options.airMode) { $holder.removeClass('note-air-editor note-editable') - .removeAttr('id contentEditable'); + .removeAttr('contentEditable'); layoutInfo.popover.remove(); layoutInfo.handle.remove(); diff --git a/addons/website/static/src/css/snippets.css b/addons/website/static/src/css/snippets.css index 4d3f2c6c5ff..eb0b4d3e69b 100644 --- a/addons/website/static/src/css/snippets.css +++ b/addons/website/static/src/css/snippets.css @@ -1,725 +1,3 @@ -/* ---- SNIPPET EDITOR ---- {{{ */ -#oe_snippets #o_left_bar, #oe_snippets #o_arrow { - position: fixed; - top: 34px; - left: 0px; - bottom: 0; - background: #282828; - -moz-box-shadow: 0px 10px 10px -10px black inset; - -webkit-box-shadow: 0px 10px 10px -10px black inset; - box-shadow: 0px 10px 10px -10px black inset; -} -#oe_snippets #o_arrow { - z-index: 1009; - width: 24px; - opacity: 0.75; -} -#oe_snippets #o_arrow > * { - position: absolute; - color: white; -} -#oe_snippets #o_arrow div { - -moz-transform: translateX(-36%) rotate(-90deg); - -ms-transform: translateX(-36%) rotate(-90deg); - -webkit-transform: translateX(-36%) rotate(-90deg); - transform: translateX(-36%) rotate(-90deg); - top: 50%; - white-space: nowrap; -} -#oe_snippets #o_arrow .fa { - width: 100%; - left: 40%; -} -#oe_snippets #o_left_bar { - left: -228px; - z-index: 1010; - width: 208px; - -moz-transition: all 500ms ease; - -o-transition: all 500ms ease; - -webkit-transition: all 500ms ease; - transition: all 500ms ease; -} -#oe_snippets #o_left_bar ul { - text-align: center; - margin: 0; - padding: 6px 0; - list-style-type: none; -} -#oe_snippets #o_left_bar ul li { - display: inline-block; - padding: 0 3px; -} -#oe_snippets #o_left_bar ul li a { - color: #aaa; -} -#oe_snippets #o_left_bar ul li .fa-stack { - overflow: hidden; -} -#oe_snippets #o_left_bar ul li.active a.fa, #oe_snippets #o_left_bar ul li.active .fa-stack-1x { - color: #428bca; -} -#oe_snippets.o_open #o_left_bar { - left: 0; -} -#oe_snippets .o_panel { - margin: 0; - border: 0; - width: 208px; -} -#oe_snippets .o_panel_header { - background: #dff0d8; - padding: 1px 8px; - color: #414141; - text-align: center; -} -#oe_snippets .o_panel_header .fa-stack { - overflow: hidden; - font-size: 1em; -} -#oe_snippets .o_panel_body { - white-space: normal; - background: black; - padding: 7px 3px; -} -#oe_snippets .o_panel_body > div { - background: black; -} -#oe_snippets .o_panel_body > div label { - width: 44px; - color: #fff; - padding-left: 10px; -} -#oe_snippets .o_panel_body > div label div { - width: 100px; - text-align: center; - -moz-transform: translate(-39px, 44px); - -webkit-transform: translate(-39px, 44px); - transform: translate(-39px, 44px); - -moz-transform-origin: 50% 50%; - -ms-transform-origin: 50% 50%; - -webkit-transform-origin: 50% 50%; - transform-origin: 50% 50%; -} - -.oe_snippet { - display: inline-block; - vertical-align: top; - width: 93px; - margin-left: 3px; - margin-right: 3px; - margin-top: 1px; - position: relative; - overflow: hidden; - -moz-user-select: -moz-none; - -ms-user-select: none; - -webkit-user-select: none; - user-select: none; - cursor: move; -} -.oe_snippet .oe_snippet_thumbnail { - text-align: center; - height: 100%; - background: transparent; - color: white; - position: relative; -} -.oe_snippet .oe_snippet_thumbnail:hover .oe_snippet_thumbnail_img { - -moz-transform: scale(0.95, 0.95); - -ms-transform: scale(0.95, 0.95); - -webkit-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; - width: 92px; - background-size: cover; - -moz-transform: scale(1, 1); - -ms-transform: scale(1, 1); - -webkit-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_snippet.disable .oe_snippet_thumbnail { - background-color: rgba(255, 0, 0, 0.75); -} -.oe_snippet.disable .oe_snippet_thumbnail_img { - opacity: 0.8; -} - -#oe_snippets .oe_snippet_thumbnail { - background: #747474, url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHJhZGlhbEdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgY3g9IjUwJSIgY3k9IjUwJSIgcj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIgc3RvcC1vcGFjaXR5PSIwLjI1Ii8+PHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMDAwMDAwIiBzdG9wLW9wYWNpdHk9IjAuNCIvPjwvcmFkaWFsR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA=='); - background: #747474, -moz-radial-gradient(rgba(0, 0, 0, 0.25), 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, 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; - -moz-transition: margin 250ms linear; - -o-transition: margin 250ms linear; - -webkit-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_vertical_text { - width: 48px; - float: none; - display: inline-block; - position: relative; - margin: 0px -24px !important; -} -.oe_drop_zone.oe_insert.oe_overlay { - -moz-border-radius: 3px; - -webkit-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); - -moz-border-radius: 4px; - -webkit-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: transparent; - z-index: 1001; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -moz-transition: opacity 100ms linear; - -o-transition: opacity 100ms linear; - -webkit-transition: opacity 100ms linear; - transition: opacity 100ms linear; - -moz-box-sizing: border-box; - -webkit-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%; - -moz-box-sizing: border-box; - -webkit-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; - -moz-box-shadow: 0px 0px 0px 1px rgba(255, 255, 255, 0.5), 0px 0px 0px 1px rgba(255, 255, 255, 0.5) inset; - -webkit-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); - -moz-border-radius: 5px; - -webkit-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); - -moz-transition: background 100ms linear; - -o-transition: background 100ms linear; - -webkit-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: #fff; - -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7); - -webkit-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: ""; - 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: ""; - 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: #fff; - -moz-box-shadow: 0 0 5px 3px rgba(255, 255, 255, 0.7); - -webkit-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-group > div { - float: none; -} -.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: "" !important; -} -.oe_overlay.block-w-right .w:before { - content: "" !important; -} -.oe_overlay.block-w-left.block-w-right .w:before { - display: none !important; -} -.oe_overlay.block-e-left .e:before { - content: "" !important; -} -.oe_overlay.block-e-right .e:before { - content: "" !important; -} -.oe_overlay.block-e-left.block-e-right .e:before { - display: none !important; -} -.oe_overlay.block-s-top .s:before { - content: "" !important; -} -.oe_overlay.block-s-bottom .s:before { - content: "" !important; -} -.oe_overlay.block-n-top .n:before { - content: "" !important; -} -.oe_overlay.block-n-bottom .n:before { - content: "" !important; -} -.oe_overlay.o_keypress { - opacity: 0; - -webkit-transition: 0.8s; - -moz-transition: 0.8s; - -o-transition: 0.8s; - -transition: 0.8s; -} - -.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: #ffffff; -} -.dropdown-submenu > a:after { - display: block; - content: " "; - float: right; - width: 0; - height: 0; - border-color: transparent; - 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 transparent; - 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: transparent; - 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: transparent; - border: 0; -} -.colorpicker td { - padding: 0; - background: transparent; -} -.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: #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; -} - /* ---- Table Snippet ---- {{{ */ .table_desc { margin: 0 0 20px 0; diff --git a/addons/website/static/src/css/snippets.sass b/addons/website/static/src/css/snippets.sass index 62a3040c19c..cb18e21db53 100644 --- a/addons/website/static/src/css/snippets.sass +++ b/addons/website/static/src/css/snippets.sass @@ -2,585 +2,6 @@ @import "compass/css3/user-interface" @import "compass/css3/transition" - -/* ---- SNIPPET EDITOR ---- {{{ */ - -#oe_snippets - #o_left_bar, #o_arrow - position: fixed - top: 34px - left: 0px - bottom: 0 - background: rgb(40,40,40) - +box-shadow(0px 10px 10px -10px black inset) - #o_arrow - z-index: 1009 - width: 24px - opacity: 0.75 - > * - position: absolute - color: white - div - @include transform( translateX(-36%) rotate(-90deg) ) - top: 50% - white-space: nowrap - .fa - width: 100% - left: 40% - #o_left_bar - left: -228px - z-index: 1010 - width: 208px - @include transition(all 500ms ease) - ul - text-align: center - margin: 0 - padding: 6px 0 - list-style-type: none - li - display: inline-block - padding: 0 3px - a - color: #aaa - .fa-stack - overflow: hidden - li.active a.fa, li.active .fa-stack-1x - color: #428bca - - &.o_open #o_left_bar - left: 0 - .o_panel - margin: 0 - border: 0 - width: 208px - .o_panel_header - background: #dff0d8 - padding: 1px 8px - color: #414141 - text-align: center - .fa-stack - overflow: hidden - font-size: 1em - .o_panel_body - white-space: normal - background: black - padding: 7px 3px - > 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%) - -.oe_snippet - display: inline-block - vertical-align: top - width: 93px - margin-left: 3px - margin-right: 3px - margin-top: 1px - position: relative - overflow: hidden - +user-select(none) - cursor: move - .oe_snippet_thumbnail - text-align: center - height: 100% - background: transparent - 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 - width: 92px - background-size: cover - @include transform( scale(1,1)) - span, div - line-height: 18px - & > :not(.oe_snippet_thumbnail) - display: none !important - &.disable - .oe_snippet_thumbnail - background-color: rgba(255, 0, 0, 0.75) - .oe_snippet_thumbnail_img - opacity: 0.8 -#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_vertical_text - width: 48px - float: none - display: inline-block - 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: transparent - 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: "\f07e" - 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 - > div - float: none - .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:before - 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:before - 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 - - &.o_keypress - opacity: 0 - -webkit-transition: .8s - -moz-transition: .8s - -o-transition: .8s - -transition: .8s - - -.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: transparent - 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 transparent - 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: transparent - 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: transparent - border: 0 - td - padding: 0 - background: transparent - > * - 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 - -// }}} - /* ---- Table Snippet ---- {{{ */ .table_desc diff --git a/addons/website/static/src/css/website.backend.css b/addons/website/static/src/css/website.backend.css index 333118ce1b3..927b3907d6f 100644 --- a/addons/website/static/src/css/website.backend.css +++ b/addons/website/static/src/css/website.backend.css @@ -3,35 +3,4 @@ } .oe_button_box .oe_stat_button .o_stat_info.published > .o_stat_text.oe_unpublish { display: none; -} -.oe_form_field.oe_form_field_html_frame { - display: block; -} -.oe_form_field_html_frame iframe { - display: block; - width: 100%; - border: 0; - padding: 0; - margin: 0; - min-height: 300px; -} -.o_form_FieldTextHtml_fullscreen .oe_form_field_html_frame iframe { - position: absolute !important; - left: 0 !important; - right: 0 !important; - top: 0px !important; - bottom: 0 !important; - width: 100% !important; - z-index: 1001 !important; - border: 0; -} -.o_form_FieldTextHtml_fullscreen .modal .oe_form_field_html_frame iframe { - top: 0 !important; -} -.o_form_FieldTextHtml_fullscreen .oe_webclient *, .o_form_FieldTextHtml_fullscreen .modal * { - position: static !important; - top: 0 !important; - left: 0 !important; - overflow: visible !important; - width: auto !important; }
\ No newline at end of file diff --git a/addons/website/static/src/css/website.css b/addons/website/static/src/css/website.css index 40febaf9b1b..642113e179b 100644 --- a/addons/website/static/src/css/website.css +++ b/addons/website/static/src/css/website.css @@ -1,158 +1,10 @@ -@charset "UTF-8"; +@charset "utf-8"; /* THIS CSS FILE IS FOR WEBSITE THEMING CUSTOMIZATION ONLY * * css for editor buttons, openerp widget included in the website and other * stuff must go to the editor.css * */ -/* ----- GENERIC LAYOUTING HELPERS ---- */ -/* Vertical Spacing */ -.mt128 { - margin-top: 128px !important; -} - -.mt92 { - margin-top: 92px !important; -} - -.mt64 { - margin-top: 64px !important; -} - -.mt48 { - margin-top: 48px !important; -} - -.mt32 { - margin-top: 32px !important; -} - -.mt16 { - margin-top: 16px !important; -} - -.mt8 { - margin-top: 8px !important; -} - -.mt4 { - margin-top: 4px !important; -} - -.mt0 { - margin-top: 0px !important; -} - -.mb128 { - margin-bottom: 128px !important; -} - -.mb92 { - margin-bottom: 92px !important; -} - -.mb64 { - margin-bottom: 64px !important; -} - -.mb48 { - margin-bottom: 48px !important; -} - -.mb32 { - margin-bottom: 32px !important; -} - -.mb16 { - margin-bottom: 16px !important; -} - -.mb8 { - margin-bottom: 8px !important; -} - -.mb4 { - margin-bottom: 4px !important; -} - -.mb0 { - margin-bottom: 0px !important; -} - -.ml8 { - margin-left: 8px !important; -} - -.ml4 { - margin-left: 4px !important; -} - -.ml0 { - margin-left: 0px !important; -} - -.mr8 { - margin-right: 8px !important; -} - -.mr4 { - margin-right: 4px !important; -} - -.mr0 { - margin-right: 0px !important; -} - -img.padding-small, .img.padding-small, span.fa.padding-small, iframe.padding-small { - padding: 4px; -} -img.padding-medium, .img.padding-medium, span.fa.padding-medium, iframe.padding-medium { - padding: 8px; -} -img.padding-large, .img.padding-large, span.fa.padding-large, iframe.padding-large { - padding: 16px; -} -img.padding-xl, .img.padding-xl, span.fa.padding-xl, iframe.padding-xl { - padding: 32px; -} - -div.media_iframe_video.padding-small iframe { - padding: 4px; -} -div.media_iframe_video.padding-medium iframe { - padding: 8px; -} -div.media_iframe_video.padding-large iframe { - padding: 16px; -} -div.media_iframe_video.padding-xl iframe { - padding: 32px; -} - -.fa-6x { - font-size: 6em; -} - -.fa-7x { - font-size: 7em; -} - -.fa-8x { - font-size: 8em; -} - -.fa-9x { - font-size: 9em; -} - -.fa-10x { - font-size: 10em; -} - -.fa.center-block { - text-align: center; -} - ul { list-style-type: disc; } @@ -247,12 +99,6 @@ font[style*='background'], font[class*='bg-'] { } /* Extra Styles */ -img.shadow { - -moz-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); - -webkit-box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); - box-shadow: 0px 3px 8px rgba(0, 0, 0, 0.2); -} - h1.text-muted, h2.text-muted, h3.text-muted { margin-top: 10px; } @@ -269,19 +115,6 @@ hr { padding-bottom: 10px; } -td { - min-width: 20px; -} - -/* ----- Editor (for public user, like forum) ----- */ -.note-toolbar { - margin-left: 0 !important; -} - -.note-popover .popover > .arrow { - display: none; -} - /* ----- Snippets Styles ----- */ .para_large { font-size: 120%; @@ -294,11 +127,6 @@ td { margin-right: auto; } -/* ----- EDITOR ----- */ -.css_non_editable_mode_hidden { - display: none !important; -} - /* ----- BOOTSTRAP FIX ----- */ .container .container, .readable .container { padding-left: 0; @@ -363,26 +191,6 @@ body > nav.navbar { width: 100%; } } -/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */ -.oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child { - position: static; -} - -.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: " "; - text-align: center; - display: block; - min-height: 1em; - color: grey; - font-size: 24px; - padding-top: 160px; -} - -.css_editable_display { - display: none; -} - -/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */ .navbar .nav > li > p { margin-bottom: 0px; } @@ -844,17 +652,6 @@ table.well tr td { color: white; } -address .fa.fa-mobile-phone { - margin: 0 3px 0 2px; -} -address .fa.fa-file-text-o { - margin-right: 1px; -} - -span[data-oe-type="monetary"] { - white-space: nowrap; -} - .oe_template_fallback { -moz-column-count: 3; -webkit-column-count: 3; @@ -879,49 +676,6 @@ span[data-oe-type="monetary"] { text-align: center; } -div.media_iframe_video { - margin: 0 auto; - text-align: center; - position: relative; - overflow: hidden; -} -div.media_iframe_video iframe { - width: 100%; - height: 100%; - top: 0; - position: absolute; - margin: 0 auto; - margin-left: -50%; -} -div.media_iframe_video .media_iframe_video_size { - padding-bottom: 66.5%; - position: relative; - width: 100%; - height: 0; -} -div.media_iframe_video .css_editable_mode_display { - position: absolute; - width: 100%; - height: 100%; - display: none; - z-index: 2; -} - -html[data-browser^="msie"] div.media_iframe_video iframe { - margin-left: 0; -} - -/* Mobile view */ -@media (max-width: 768px) { - img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa { - -webkit-transform: none !important; - -moz-transform: none !important; - -ms-transform: none !important; - -o-transform: none !important; - transform: none !important; - } -} -/* Fix: backward compatibility saas-3 */ div.carousel .container > .carousel-caption { position: absolute; right: 50%; @@ -1003,7 +757,6 @@ div.carousel .item.text_only .container > .carousel-image { .container nav#oe_main_menu_navbar { position: inherit; } - .oe_search_clear { position: absolute; display:none; diff --git a/addons/website/static/src/css/website.editor.css b/addons/website/static/src/css/website.editor.css new file mode 100644 index 00000000000..69e6e451d6b --- /dev/null +++ b/addons/website/static/src/css/website.editor.css @@ -0,0 +1,263 @@ +@charset "utf-8"; +/* ---- Common---- {{{ */ +.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + min-height: 24px; +} + +/* ---- 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; +} + +.oe_include_bundles { + font-weight: normal; + padding: 0 8px; +} + +/* ---- CUSTOMIZE THEME ---- {{{ */ +#theme_error { + background: #ffc; +} + +#theme_customize_modal { + overflow: visible; + z-index: 1020; + background: transparent; + 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; +} diff --git a/addons/website/static/src/css/website.editor.sass b/addons/website/static/src/css/website.editor.sass new file mode 100644 index 00000000000..911631bb8ee --- /dev/null +++ b/addons/website/static/src/css/website.editor.sass @@ -0,0 +1,234 @@ +@charset "utf-8" +@import "common" + +/* ---- Common---- {{{ */ + +.col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 + min-height: 24px + +// }}} + +/* ---- 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) + +.oe_include_bundles + font-weight: normal + padding: 0 8px + +// }}} + +/* ---- CUSTOMIZE THEME ---- {{{ */ +#theme_error + background: #ffc +#theme_customize_modal + overflow: visible + z-index: 1020 + background: transparent + 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 + +// }}} diff --git a/addons/website/static/src/css/website.sass b/addons/website/static/src/css/website.sass index cb526b29b01..f8f8c4fe05f 100644 --- a/addons/website/static/src/css/website.sass +++ b/addons/website/static/src/css/website.sass @@ -10,93 +10,6 @@ * */ -/* ----- GENERIC LAYOUTING HELPERS ---- */ - -/* Vertical Spacing */ -.mt128 - margin-top: 128px !important -.mt92 - margin-top: 92px !important -.mt64 - margin-top: 64px !important -.mt48 - margin-top: 48px !important -.mt32 - margin-top: 32px !important -.mt16 - margin-top: 16px !important -.mt8 - margin-top: 8px !important -.mt4 - margin-top: 4px !important -.mt0 - margin-top: 0px !important - -.mb128 - margin-bottom: 128px !important -.mb92 - margin-bottom: 92px !important -.mb64 - margin-bottom: 64px !important -.mb48 - margin-bottom: 48px !important -.mb32 - margin-bottom: 32px !important -.mb16 - margin-bottom: 16px !important -.mb8 - margin-bottom: 8px !important -.mb4 - margin-bottom: 4px !important -.mb0 - margin-bottom: 0px !important - -.ml8 - margin-left: 8px !important -.ml4 - margin-left: 4px !important -.ml0 - margin-left: 0px !important - -.mr8 - margin-right: 8px !important -.mr4 - margin-right: 4px !important -.mr0 - margin-right: 0px !important - -img, .img, span.fa, iframe - &.padding-small - padding: 4px - &.padding-medium - padding: 8px - &.padding-large - padding: 16px - &.padding-xl - padding: 32px -div.media_iframe_video - &.padding-small iframe - padding: 4px - &.padding-medium iframe - padding: 8px - &.padding-large iframe - padding: 16px - &.padding-xl iframe - padding: 32px - -.fa-6x - font-size: 6em -.fa-7x - font-size: 7em -.fa-8x - font-size: 8em -.fa-9x - font-size: 9em -.fa-10x - font-size: 10em -.fa.center-block - text-align: center - ul list-style-type: disc li > ul @@ -158,9 +71,6 @@ font[style*='background'], font[class*='bg-'] /* Extra Styles */ -img.shadow - +box-shadow(0px 3px 8px rgba(0, 0, 0, 0.2)) - h1.text-muted, h2.text-muted, h3.text-muted margin-top: 10px @@ -175,17 +85,6 @@ hr margin-bottom: 10px padding-bottom: 10px -td - min-width: 20px - -/* ----- Editor (for public user, like forum) ----- */ - -.note-toolbar - margin-left: 0 !important - -.note-popover .popover > .arrow - display: none - /* ----- Snippets Styles ----- */ .para_large @@ -197,11 +96,6 @@ td margin-left: auto margin-right: auto -/* ----- EDITOR ----- */ - -.css_non_editable_mode_hidden - display: none !important - /* ----- BOOTSTRAP FIX ----- */ .container, .readable @@ -241,7 +135,7 @@ body > nav.navbar width: 100% position: absolute z-index: 10000 -@media (max-min: 767px) +@media (min-width: 767px) nav.navbar + #wrapwrap padding-top: 34px @@ -255,33 +149,9 @@ body > nav.navbar .table .img-responsive width: 100% -/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */ -.oe_structure.oe_empty > .oe_drop_zone.oe_insert:only-child, [data-oe-type=html] > .oe_drop_zone.oe_insert:only-child - position: static - -.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: ' ' - text-align: center - display: block - min-height: 1em - color: grey - font-size: 24px - padding-top: 160px - -.css_editable_display - display: none - -/* ---- HACK FOR COVERING UP EDITOR BOGUS P INSERTION --- */ - .navbar .nav > li > p margin-bottom: 0px -// .navbar .nav > li a -// text-shadow: none - -// .nav > li a -// display: block - /* ---- HOMEPAGE THEME CUSTOMIZATION ---- */ .nav-hierarchy @@ -659,15 +529,6 @@ table.well tr a color: white -address - .fa.fa-mobile-phone - margin: 0 3px 0 2px - .fa.fa-file-text-o - margin-right: 1px - -span[data-oe-type="monetary"] - white-space: nowrap - .oe_template_fallback @include column-count(3) @@ -685,46 +546,6 @@ span[data-oe-type="monetary"] input text-align: center -div.media_iframe_video - margin: 0 auto - text-align: center - position: relative - overflow: hidden - iframe - width: 100% - height: 100% - top: 0 - position: absolute - margin: 0 auto - margin-left: -50% - .media_iframe_video_size - padding-bottom: 66.5% - position: relative - width: 100% - height: 0 - .css_editable_mode_display - position: absolute - width: 100% - height: 100% - display: none - z-index: 2 - -html[data-browser^="msie"] div.media_iframe_video iframe - margin-left: 0 - -/* Mobile view */ - -@media (max-width: 768px) - img:not(.cke_iframe), .media_iframe_video, span.fa, i.fa - -webkit-transform: none !important - -moz-transform: none !important - -ms-transform: none !important - -o-transform: none !important - transform: none !important - - -/* Fix: backward compatibility saas-3 */ - div.carousel .container > .carousel-caption diff --git a/addons/website/static/src/js/openerp.define.js b/addons/website/static/src/js/openerp.define.js deleted file mode 100644 index 85e0486546e..00000000000 --- a/addons/website/static/src/js/openerp.define.js +++ /dev/null @@ -1,107 +0,0 @@ -odoo.define('website.define', function () { -'use strict'; - -var containsDefine = typeof(define) !== "undefined" ? define : undefined; -var scripts = { - 'jquery': jQuery -}; - -function odoo_define(parse) { - return function (name, list, callback) { - - var def = $.Deferred(); - def.openerpDefine = true; - - if (!callback) { - callback = list; - list = null; - } - if (!list) { - list = name; - if (odoo.website_next_define) { - name = odoo.website_next_define; - odoo.website_next_define = null; - } - } - - if (list instanceof Array) { - var defs = []; - var args = []; - _.each(list, function (js, index) { - var path = parse(js); - - if (!(path in scripts)) { - - scripts[path] = $.Deferred(); - scripts[path].openerpDefine = true; - - $.ajax({ 'url': path, 'dataType': 'html', 'async': false }) - .then(function (data) { - var code = new Function("return "+data)(); - if (code.openerpDefine) { - code.then(function (code) { - scripts[path].resolve(code); - scripts[path] = code; - }); - } else { - scripts[path].resolve(code); - scripts[path] = code; - } - }); - - } - - if (scripts[path] && scripts[path].openerpDefine) { - scripts[path].then(function (code) { - args[index] = code; - }); - defs.push(scripts[path]); - } else { - args[index] = scripts[path] = scripts[path] || null; - } - }); - - $.when.apply($, defs).then(function () { - var result = callback.apply(window, args); - if (name) { - scripts[name] = scripts[parse(name)] = result; - } - def.resolve(result); - }); - - } else { - - def = scripts[name] = scripts[parse(name)] = scripts[name] || callback() || null; - - } - - return def; - } -} - -function enable() { - window.define = odoo_define(function (id) { - - if (id.slice(0,10) === 'summernote') { - return '/website/static/lib/summernote/src/js' + id.slice(10,Infinity) + '.js'; - } - return id; - }); -} - -function disable() { - window.define = containsDefine; -} - -odoo.website_next_define = undefined; -odoo.enable_website_define = enable; -odoo.disable_website_define = disable; - -return { - enable: enable, - disable: disable, - define: odoo_define, - scripts: scripts, -}; - -}); diff --git a/addons/website/static/src/js/website.backend.iframe.js b/addons/website/static/src/js/website.backend.iframe.js deleted file mode 100644 index 925fb6aa4b1..00000000000 --- a/addons/website/static/src/js/website.backend.iframe.js +++ /dev/null @@ -1,75 +0,0 @@ -odoo.define('website.backend.iframe', function (require) { -'use strict'; - -var editor = require('website.editor'); -var snippet_editor = require('website.snippets.editor'); - -window.top.odoo[callback+"_set_value"] = function (value, fields_values) { - var $editable = $("#wrapwrap .o_editable:first"); - if (value !== $editable.html()) { - if ($('body').hasClass('editor_enable')) { - editor.editor_bar.rte.historyRecordUndo($editable); - editor.editor_bar.snippets.make_active(false); - } - $editable.html(value); - } -}; - -editor.EditorBar.include({ - start: function () { - var self = this; - var res = this._super.apply(this, arguments); - if (location.search.indexOf("enable_editor") !== -1) { - this.on('rte:ready', this, function () { - $("#website-top-edit").hide(); - setTimeout(function () { - if (window.top.odoo[callback+"_editor"]) { - window.top.odoo[callback+"_editor"](this); - } - }, 0); - - var $editable = $("#wrapwrap .o_editable:first"); - setTimeout(function () { - $($editable.find("*").filter(function () {return !this.children.length;}).first()[0] || $editable) - .focusIn().trigger("mousedown").trigger("keyup"); - },0); - - this.rte.on('change', this, function () { - window.top.odoo[callback+"_downup"]($editable.prop('innerHTML')); - }); - - - $(window).on('keydown', function (event) { - if(event.keyCode === 27 && $("body", window.top.document).hasClass("o_form_FieldTextHtml_fullscreen")) { - $(".o_fullscreen").trigger("click"); - } - $(window).trigger('resize'); - }); - - }); - - var style = document.createElement("style"); - style.textContent = "#wrapwrap > main > * { margin-top: 50px; }"; - $("head").append(style); - - this.on("snippets:ready", this, function () { - $(window.top).trigger("resize"); - }); - - } else if (window.top.odoo[callback+"_editor"]) { - window.top.odoo[callback+"_editor"](this); - } - return res; - }, - cancel: function () { - return; - }, -}); - -snippet_editor.BuildingBlock.include({ - _get_snippet_url: function () { - return snippets_url; - } -}); - -}); diff --git a/addons/website/static/src/js/website.backend.js b/addons/website/static/src/js/website.backend.js index fb93bf0dadf..f877c0e90fc 100644 --- a/addons/website/static/src/js/website.backend.js +++ b/addons/website/static/src/js/website.backend.js @@ -22,157 +22,4 @@ var WidgetWebsiteButton = form_common.AbstractField.extend({ core.form_widget_registry.add('website_button', WidgetWebsiteButton); -var FieldWidget = form_common.AbstractField.extend(form_common.ReinitializeFieldMixin); - - -var FieldTextHtmlFrame = FieldWidget.extend({ - template: 'FieldTextHtmlFrame', - start: function () { - var self = this; - - this.callback = _.uniqueId('FieldTextHtml_'); - odoo[this.callback+"_editor"] = function (EditorBar) { - self.on_editor_loaded(EditorBar); - }; - odoo[this.callback+"_content"] = function () { - self.on_content_loaded(); - }; - odoo[this.callback+"_downup"] = function (value) { - self.dirty = true; - self.internal_set_value(value); - self.trigger('changed_value'); - self.resize(); - }; - - // init jqery objects - this.$iframe = this.$('iframe'); - this.document = null; - this.$body = $(); - this.$content = $(); - - // init resize - this.resize = function resize() { - if (self.get('effective_readonly')) { - $("body").removeClass("o_form_FieldTextHtml_fullscreen"); - } - if ($("body").hasClass("o_form_FieldTextHtml_fullscreen")) { - self.$iframe.css('height', $("body").hasClass('o_form_FieldTextHtml_fullscreen') ? (document.body.clientHeight - self.$iframe.offset().top) + 'px' : ''); - } else { - self.$iframe.css("height", (self.$body.find("#oe_snippets").length ? 500 : 300) + "px"); - } - }; - $(window).on('resize', self.resize); - - return this._super(); - }, - get_url: function () { - var src = this.options.editor_url ? this.options.editor_url+"?" : "/website/field/html?"; - var datarecord = this.view.get_fields_values(); - - var attr = { - 'model': this.view.model, - 'field': this.name, - 'res_id': datarecord.id || '', - 'callback': this.callback - }; - if (this.options.snippets) { - attr.snippets = this.options.snippets; - } - if (!this.get("effective_readonly")) { - attr.enable_editor = 1; - } - if (core.debug) { - attr.debug = 1; - } - - for (var k in attr) { - src += "&"+k+"="+attr[k]; - } - - delete datarecord[this.name]; - src += "&datarecord="+ encodeURIComponent(JSON.stringify(datarecord)); - - return src; - }, - initialize_content: function() { - if (!this.get("effective_readonly")) { - this.$iframe = this.$el.find('iframe'); - this.document = null; - this.$body = $(); - this.$content = $(); - this.dirty = false; - this.editor = false; - odoo[this.callback+"_set_value"] = null; - this.$iframe.attr("src", this.get_url()); - } - }, - on_content_loaded: function () { - this.document = this.$iframe.contents()[0]; - this.$body = $("body", this.document); - this.$content = this.$body.find("#wrapwrap .o_editable:first"); - setTimeout(this.resize,0); - }, - on_editor_loaded: function (EditorBar) { - var self = this; - this.editor = EditorBar; - - $("body").on('click', function (event) { - if ($("body").hasClass('o_form_FieldTextHtml_fullscreen')) { - $("body").removeClass("o_form_FieldTextHtml_fullscreen"); - self.$iframe.css('height', ''); - event.preventDefault(); - event.stopPropagation(); - } - }); - - if (this.get('value') && window.odoo[this.callback+"_set_value"] && !(this.$content.prop('innerHTML')||"").length) { - this.render_value(); - } - - setTimeout(function () { - var $fullscreen = $('<span class="btn btn-primary" style="margin: 5px;padding: 1px; position: fixed; top: 0; right: 0; z-index: 2000;"><span class="o_fullscreen fa fa-arrows-alt" style="color: white;margin: 3px 5px;"></span></span>'); - $("#website-top-navbar", self.document).append($fullscreen); - $fullscreen.on('click', function () { - $("body").toggleClass("o_form_FieldTextHtml_fullscreen"); - self.resize(); - }); - setTimeout(self.resize,0); - }, 500); - }, - render_value: function() { - var value = (this.get('value') || "").replace(/^<p[^>]*>\s*<\/p>$/, ''); - if (!this.get("effective_readonly")) { - if (!this.$content) { - return; - } - if(window.odoo[this.callback+"_set_value"]) { - window.odoo[this.callback+"_set_value"](value || '', this.view.get_fields_values(), this.name); - this.resize(); - } - } else { - this.$el.html(value); - } - }, - is_false: function() { - return this.get('value') === false || this.get('value') === ""; - }, - get_value: function(save_mode) { - if (save_mode && this.editor && this.editor.snippets && this.dirty) { - this.editor.snippets.clean_for_save(); - this.internal_set_value( this.$content.prop('innerHTML') ); - } - return this.get('value'); - }, - destroy: function () { - $("body").removeClass("o_form_FieldTextHtml_fullscreen"); - $(window).off('resize', self.resize); - $(window).off('keydown', self.escape); - delete window.odoo[this.callback+"_content"]; - delete window.odoo[this.callback+"_downup"]; - delete window.odoo[this.callback+"_set_value"]; - } -}); - -core.form_widget_registry.add('html_frame', FieldTextHtmlFrame); - }); diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 67e9b3da734..b6f463b11ba 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -3,1145 +3,55 @@ odoo.define('website.editor', function (require) { var ajax = require('web.ajax'); var core = require('web.core'); -var utils = require('web.utils'); var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var editor = require('web_editor.editor'); +var widget = require('web_editor.widget'); var website = require('website.website'); +var qweb = core.qweb; var _t = core._t; +ajax.loadXML('/website/static/src/xml/website.editor.xml', qweb); -////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* Summernote Lib (neek change to make accessible: method and object) */ -var dom = $.summernote.core.dom; -var range = $.summernote.core.range; -var eventHandler = $.summernote.eventHandler; -var renderer = $.summernote.renderer; - -var tplButton = renderer.getTemplate().button; -var tplIconButton = renderer.getTemplate().iconButton; - -////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* update and change the popovers content, and add history button */ - -var fn_createPalette = renderer.createPalette; -renderer.createPalette = function ($container, options) { - fn_createPalette.call(this, $container, options); - - if (!core.qweb.has_template('website.colorpicker')) { - return; - } - - var $color = $container.find('.note-color'); - var html = core.qweb.render('website.colorpicker'); - $color.find('.note-color-palette').prepend(html); - var $bg = $color.find('.colorpicker:first button'); - var $fore = $color.find('.colorpicker:last button'); - - $bg.each(function () { $(this).attr('data-event', 'backColor').attr('data-value', $(this).attr('class')); }); - $fore.each(function () { $(this).attr('data-event', 'foreColor').attr('data-value', $(this).attr('class').replace(/bg-/, 'text-')); }); -}; - -var fn_tplPopovers = renderer.tplPopovers; -renderer.tplPopovers = function (lang, options) { - var $popover = $(fn_tplPopovers.call(this, lang, options)); - - var $imagePopover = $popover.find('.note-image-popover'); - var $linkPopover = $popover.find('.note-link-popover'); - var $airPopover = $popover.find('.note-air-popover'); - - if (window === window.top) { - $popover.children().addClass("hidden-xs"); - } - - //////////////// image popover - - // add center button for images - $(tplIconButton('fa fa-align-center', { - title: _t('Center'), - event: 'floatMe', - value: 'center' - })).insertAfter($imagePopover.find('[data-event="floatMe"][data-value="left"]')); - $imagePopover.find('button[data-event="removeMedia"]').parent().remove(); - $imagePopover.find('button[data-event="floatMe"][data-value="none"]').remove(); - - var $alt = $('<div class="btn-group"/>'); - $alt.prependTo($imagePopover.find('.popover-content')); - $alt.append('<button class="btn btn-default btn-sm btn-small" data-event="alt"><strong>Alt: </strong><span class="o_image_alt"></span></button>'); - - // padding button - var $padding = $('<div class="btn-group"/>'); - $padding.insertBefore($imagePopover.find('.btn-group:first')); - var $button = $(tplIconButton('fa fa-plus-square-o', { - title: _t('Padding'), - dropdown: true - })).appendTo($padding); - var $ul = $('<ul class="dropdown-menu"/>').insertAfter($button); - $ul.append('<li><a data-event="padding" href="#" data-value="">'+_t('None')+'</a></li>'); - $ul.append('<li><a data-event="padding" href="#" data-value="small">'+_t('Small')+'</a></li>'); - $ul.append('<li><a data-event="padding" href="#" data-value="medium">'+_t('Medium')+'</a></li>'); - $ul.append('<li><a data-event="padding" href="#" data-value="large">'+_t('Large')+'</a></li>'); - $ul.append('<li><a data-event="padding" href="#" data-value="xl">'+_t('Xl')+'</a></li>'); - - // circle, boxed... options became toggled - $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove(); - $button = $(tplIconButton('fa fa-sun-o', { - title: _t('Shadow'), - event: 'imageShape', - value: 'shadow' - })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-circle"]')); - - // add spin for fa - var $spin = $('<div class="btn-group hidden only_fa"/>').insertAfter($button.parent()); - $(tplIconButton('fa fa-refresh', { - title: _t('Spin'), - event: 'imageShape', - value: 'fa-spin' - })).appendTo($spin); - - // resize for fa - var $resizefa = $('<div class="btn-group hidden only_fa"/>') - .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])')); - for (var size=1; size<=5; size++) { - $(tplButton('<span class="note-fontsize-10">'+size+'x</span>', { - title: size+"x", - event: 'resizefa', - value: size+'' - })).appendTo($resizefa); - } - var $colorfa = $airPopover.find('.note-color').clone(); - $colorfa.find(".btn-group:first").remove(); - $colorfa.find("ul.dropdown-menu").css('min-width', '172px'); - $colorfa.find('button[data-event="color"]').attr('data-value', '{"foreColor": "#f00"}') - .find("i").css({'background': '', 'color': '#f00'}); - $resizefa.after($colorfa); - - // show dialog box and delete - var $imageprop = $('<div class="btn-group"/>'); - $imageprop.appendTo($imagePopover.find('.popover-content')); - $(tplIconButton('fa fa-picture-o', { - title: _t('Edit'), - event: 'showImageDialog' - })).appendTo($imageprop); - $(tplIconButton('fa fa-trash-o', { - title: _t('Remove'), - event: 'delete' - })).appendTo($imageprop); - - $imagePopover.find('.popover-content').append($airPopover.find(".note-history").clone()); - - $imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone()); - - //////////////// link popover - - $linkPopover.find('.popover-content').append($airPopover.find(".note-history").clone()); - - $linkPopover.find('button[data-event="showLinkDialog"] i').attr("class", "fa fa-link"); - $linkPopover.find('button[data-event="unlink"]').before($airPopover.find('button[data-event="showImageDialog"]').clone()); - - //////////////// text/air popover - - //// highlight the text format - $airPopover.find('.note-style').on('mousedown', function () { - var $format = $airPopover.find('[data-event="formatBlock"]'); - var node = range.create().sc; - var formats = $format.map(function () { return $(this).data("value"); }).get(); - while (node && (!node.tagName || (!node.tagName || formats.indexOf(node.tagName.toLowerCase()) === -1))) { - node = node.parentNode; - } - $format.parent().removeClass('active'); - $format.filter('[data-value="'+(node ? node.tagName.toLowerCase() : "p")+'"]') - .parent().addClass("active"); - }); - - //////////////// tooltip - - $airPopover.add($linkPopover).add($imagePopover).find("button") - .tooltip('destroy') - .tooltip({ - container: 'body', - trigger: 'hover', - placement: 'bottom' - }).on('click', function () {$(this).tooltip('hide');}); - - return $popover; -}; - -var fn_boutton_update = eventHandler.popover.button.update; -eventHandler.popover.button.update = function ($container, oStyle) { - // stop animation when edit content - var previous = $(".note-control-selection").data('target'); - if (previous) { - $(previous).css({"-webkit-animation-play-state": "", "animation-play-state": "", "-webkit-transition": "", "transition": "", "-webkit-animation": "", "animation": ""}); - } - if (oStyle.image) { - $(oStyle.image).css({"-webkit-animation": "none", "animation": "none"}); - } - // end - - fn_boutton_update.call(this, $container, oStyle); - - $container.find('button[data-event="undo"]').attr('disabled', !history.hasUndo()); - $container.find('button[data-event="redo"]').attr('disabled', !history.hasRedo()); - - if (oStyle.image) { - $container.find('[data-event]').parent().removeClass("active"); - - $container.find('a[data-event="padding"][data-value="small"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-small")); - $container.find('a[data-event="padding"][data-value="medium"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-medium")); - $container.find('a[data-event="padding"][data-value="large"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-large")); - $container.find('a[data-event="padding"][data-value="xl"]').parent().toggleClass("active", $(oStyle.image).hasClass("padding-xl")); - $container.find('a[data-event="padding"][data-value=""]').parent().toggleClass("active", !$container.find('.active a[data-event="padding"]').length); - - if (dom.isImgFont(oStyle.image)) { - - $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-event="imageShape"])').addClass("hidden"); - $container.find('.only_fa').removeClass("hidden"); - $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x")); - $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x")); - $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x")); - $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x")); - $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length); - - $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin")); - - $container.find('.note-color').removeClass("hidden"); - - } else { - - $container.find('.hidden:not(.only_fa)').removeClass("hidden"); - $container.find('.only_fa').addClass("hidden"); - var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/); - if (width) { - width = width[2]; - } - $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%"); - $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%"); - $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%"); - - $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow")); - - if (!$(oStyle.image).is("img")) { - $container.find('.btn-group:has(button[data-event="imageShape"])').addClass("hidden"); - } - - $container.find('.note-color').addClass("hidden"); - - } - - $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("pull-left")); - $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("center-block")); - $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("pull-right")); - - $(oStyle.image).trigger('attributes_change'); - } -}; - -var fn_popover_update = eventHandler.popover.update; -eventHandler.popover.update = function ($popover, oStyle, isAirMode) { - var $imagePopover = $popover.find('.note-image-popover'); - var $linkPopover = $popover.find('.note-link-popover'); - var $airPopover = $popover.find('.note-air-popover'); - - fn_popover_update.call(this, $popover, oStyle, isAirMode); - - if (!isAirMode || $(oStyle.range.sc).closest('[data-oe-model]:not([data-oe-model="ir.ui.view"]):not([data-oe-type="html"])').length) { - $imagePopover.hide(); - $linkPopover.hide(); - $airPopover.hide(); - return; - } - - if (oStyle.image) { - if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) { - oStyle.image = oStyle.image.parentNode; - } - var alt = $(oStyle.image).attr("alt"); - - $imagePopover.find('.o_image_alt').text( (alt || "").replace(/"/g, '"') ).parent().toggle(oStyle.image.tagName === "IMG"); - $imagePopover.show(); - - range.createFromNode(dom.firstChild(oStyle.image)).select(); - } else { - $(".note-control-selection").hide(); - } - - if (oStyle.image || (!oStyle.range.isCollapsed() || (oStyle.range.sc.tagName && !dom.isAnchor(oStyle.range.sc)) || (oStyle.image && !$(oStyle.image).closest('a').length))) { - $linkPopover.hide(); - oStyle.anchor = false; - } - - if (oStyle.image || oStyle.anchor || !$(oStyle.range.sc).closest('.note-editable').length) { - $airPopover.hide(); - } else { - $airPopover.show(); - } -}; - -eventHandler.handle.update = function ($handle, oStyle) { - $handle.toggle(!!oStyle.image); - if (oStyle.image) { - var $selection = $handle.find('.note-control-selection'); - var $image = $(oStyle.image); - var szImage = { - w: parseInt($image.outerWidth(true), 10), - h: parseInt($image.outerHeight(true), 10) - }; - $selection.data('target', oStyle.image); // save current image element. - var sSizing = szImage.w + 'x' + szImage.h; - $selection.find('.note-control-selection-info').text(szImage.h > 50 ? sSizing : ""); - - $selection.find('.note-control-sizing').toggleClass('note-control-sizing note-control-holder').css({ - 'border-top': 0, - 'border-left': 0 - }); - } -}; - -$(document).on('click keyup', function () { - $('button[data-event="undo"]').attr('disabled', !history.hasUndo()); - $('button[data-event="redo"]').attr('disabled', !history.hasRedo()); -}); - -eventHandler.editor.undo = function ($popover) { - if(!$popover.attr('disabled')) history.undo(); -}; -eventHandler.editor.redo = function ($popover) { - if(!$popover.attr('disabled')) history.redo(); -}; - -////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* hack for image and link editor */ - -function getImgTarget () { - return $(".note-control-selection").data('target'); -} -eventHandler.editor.padding = function ($editable, sValue) { - var $target = $(getImgTarget()); - var paddings = "small medium large xl".split(/\s+/); - $editable.data('NoteHistory').recordUndo(); - if (sValue.length) { - paddings.splice(paddings.indexOf(sValue),1); - $target.toggleClass('padding-'+sValue); - } - $target.removeClass("padding-" + paddings.join(" padding-")); -}; -eventHandler.editor.resize = function ($editable, sValue) { - var $target = $(getImgTarget()); - $editable.data('NoteHistory').recordUndo(); - var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/); - if (width) { - width = width[2]/100; - } - $(getImgTarget()).css('width', width != sValue ? (sValue * 100) + '%' : ''); -}; -eventHandler.editor.resizefa = function ($editable, sValue) { - var $target = $(getImgTarget()); - $editable.data('NoteHistory').recordUndo(); - $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, '')); - if (+sValue > 1) { - $target.addClass('fa-'+sValue+'x'); - } -}; -eventHandler.editor.floatMe = function ($editable, sValue) { - var $target = $(getImgTarget()); - $editable.data('NoteHistory').recordUndo(); - switch (sValue) { - case 'center': $target.toggleClass('center-block').removeClass('pull-right pull-left'); break; - case 'left': $target.toggleClass('pull-left').removeClass('pull-right center-block'); break; - case 'right': $target.toggleClass('pull-right').removeClass('pull-left center-block'); break; - } -}; -eventHandler.editor.imageShape = function ($editable, sValue) { - var $target = $(getImgTarget()); - $editable.data('NoteHistory').recordUndo(); - $target.toggleClass(sValue); -}; - -eventHandler.dialog.showLinkDialog = function ($editable, $dialog, linkInfo) { - $editable.data('range').select(); - $editable.data('NoteHistory').recordUndo(); - - var editor = new LinkDialog($editable, linkInfo); - editor.appendTo(document.body); - - var def = new $.Deferred(); - editor.on("save", this, function (linkInfo) { - linkInfo.range.select(); - $editable.data('range', linkInfo.range); - def.resolve(linkInfo); - $editable.data('NoteHistory').popUndo(); - $('.note-popover .note-link-popover').show(); - }); - editor.on("cancel", this, function () { - def.reject(); - $editable.data('NoteHistory').popUndo(); - }); - return def; -}; -eventHandler.dialog.showImageDialog = function ($editable) { - var r = $editable.data('range'); - if (r.sc.tagName && r.sc.childNodes.length) { - r.sc = r.sc.childNodes[r.so]; - } - var editor = new MediaDialog($editable, dom.isImg(r.sc) ? r.sc : null); - editor.appendTo(document.body); - return new $.Deferred().reject(); -}; -$.summernote.pluginEvents.alt = function (event, editor, layoutInfo) { - var $editable = layoutInfo.editable(); - var $selection = layoutInfo.handle().find('.note-control-selection'); - var media = $selection.data('target'); - new alt($editable, media).appendTo(document.body); -}; - -////////////////////////////////////////////////////////////////////////////////////////////////////////// - -dom.isVoid = function (node) { - return node && /^BR|^IMG|^HR/.test(node.nodeName.toUpperCase()) || dom.isImg(node); -}; -dom.isImg = function (node) { - return dom.isImgFont(node) || (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i)) )); -}; -dom.isForbiddenNode = function (node) { - return $(node).is(".media_iframe_video, .fa, img"); -}; - -dom.isImgFont = function (node) { - var nodeName = node && node.nodeName.toUpperCase(); - var className = (node && node.className || ""); - if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) { - var classNames = className.split(/\s+/); - for (var k=0; k<fontIcons.length; k++) { - if (_.intersection(fontIcons[k].icons, classNames).length) { - return true; - } - } - } - return false; -}; -// re-overwrite font to include theme icons -var isFont = dom.isFont; -dom.isFont = function (node) { - return dom.isImgFont(node) || isFont(node); -}; - -////////////////////////////////////////////////////////////////////////////////////////////////////////// - -var fn_visible = $.summernote.pluginEvents.visible; -$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) { - var res = fn_visible.call(this, event, editor, layoutInfo); - var $node = $(dom.node(range.create().sc)); - if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) && $node.hasClass("o_editable") && !$node[0].children.length) { - var p = $('<p><br/></p>')[0]; - $node.append( p ); - range.createFromNode(p.firstChild).select(); - } - return res; -}; - -////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* fix ie and re-range to don't break snippet*/ - -var initial_data = {}; -function reRangeSelectKey (event) { - initial_data.range = null; - if (event.shiftKey && event.keyCode >= 37 && event.keyCode <= 40 && !$(event.target).is("input, textarea, select")) { - var r = range.create(); - if (r) { - var rng = r.reRange(event.keyCode <= 38); - if (r !== rng) { - rng.select(); - } - } - } -} -function reRangeSelect (event, dx, dy) { - var r = range.create(); - if (!r || r.isCollapsed()) return; - - // check if the user move the caret on up or down - var data = r.reRange(dy < 0 || (dy === 0 && dx < 0)); - - if (data.sc !== r.sc || data.so !== r.so || data.ec !== r.ec || data.eo !== r.eo) { - setTimeout(function () { - data.select(); - $(data.sc.parentNode).closest('.note-popover'); - },0); - } - - $(data.sc).closest('.o_editable').data('range', r); - return r; -} -function summernote_mouseup (event) { - if ($(event.target).closest("#website-top-navbar, .note-popover").length) { - return; - } - // don't rerange if simple click - if (initial_data.event) { - var dx = event.clientX - (event.shiftKey ? initial_data.rect.left : initial_data.event.clientX); - var dy = event.clientY - (event.shiftKey ? initial_data.rect.top : initial_data.event.clientY); - if (10 < Math.pow(dx, 2)+Math.pow(dy, 2)) { - reRangeSelect(event, dx, dy); - } - } - - if (!$(event.target).closest(".o_editable").length) { - return; - } - if (!initial_data.range || !event.shiftKey) { - setTimeout(function () { - initial_data.range = range.create(); - },0); - } -} -var remember_selection; -function summernote_mousedown (event) { - history.splitNext(); - - var $editable = $(event.target).closest(".o_editable, .note-editor"); - - if (!!document.documentMode) { - summernote_ie_fix(event, function (node) { return node.tagName === "DIV" || node.tagName === "IMG" || (node.dataset && node.dataset.oeModel); }); - } else if (last_div && event.target !== last_div) { - if (last_div.tagName === "A") { - summernote_ie_fix(event, function (node) { return node.dataset && node.dataset.oeModel; }); - } else if ($editable.length) { - if (summernote_ie_fix(event, function (node) { return node.tagName === "A"; })) { - r = range.create(); - r.select(); - } - } - } - - // remember_selection when click on non editable area - var r = range.create(); - if ($(r ? dom.node(r.sc) : event.srcElement || event.target).closest('#website-top-navbar, #oe_main_menu_navbar, .note-popover, .note-toolbar, .modal').length) { - if (!$(event.target).is('input, select, label, button, a')) { - if (!remember_selection && $editable[0]) { - remember_selection = range.create(dom.firstChild($editable[0]), 0); - } - try { - remember_selection.select(); - } catch (e) { - console.warn(e); - } - } - } else if (r && $(dom.node(r.sc)).closest('.o_editable, .note-editable').length) { - remember_selection = r; - } - - initial_data.event = event; - - // keep selection when click with shift - if (event.shiftKey && $editable.length) { - if (initial_data.range) { - initial_data.range.select(); - } - var rect = r && r.getClientRects(); - initial_data.rect = rect && rect.length ? rect[0] : { top: 0, left: 0 }; - } -} - -var last_div; -var last_div_change; -var last_editable; -function summernote_ie_fix (event, pred) { - var editable; - var div; - var node = event.target; - while(node.parentNode) { - if (!div && pred(node)) { - div = node; - } - if(last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) { - break; - } - if (node.className && node.className.indexOf('o_editable') !== -1) { - if (!div) { - div = node; - } - editable = node; - break; - } - node = node.parentNode; - } - - if (!editable) { - $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable"); - $(last_editable).attr("contentEditable", "true").prop("contentEditable", "true"); - last_div_change = null; - last_editable = null; - return; - } - - if (div === last_div) { - return; - } - - last_div = div; - - $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable"); - - if (last_editable !== editable) { - if ($(editable).is("[contentEditable='true']")) { - $(editable).removeAttr("contentEditable").removeProp("contentEditable"); - last_editable = editable; - } else { - last_editable = null; - } - } - if (!$(div).attr("contentEditable") && !$(div).is("[data-oe-type='many2one'], [data-oe-type='contact']")) { - $(div).attr("contentEditable", "true").prop("contentEditable", "true"); - last_div_change = div; - } else { - last_div_change = null; - } - return editable !== div ? div : null; -} - -var fn_attach = eventHandler.attach; -eventHandler.attach = function (oLayoutInfo, options) { - fn_attach.call(this, oLayoutInfo, options); - oLayoutInfo.editor.on('dragstart', 'img', function (e) { e.preventDefault(); }); - $(document).on('mousedown', summernote_mousedown); - $(document).on('mouseup', summernote_mouseup); - oLayoutInfo.editor.off('click').on('click', function (e) {e.preventDefault();}); // if the content editable is a link - oLayoutInfo.editor.on('dblclick', 'img, .media_iframe_video, span.fa, i.fa, span.fa', function (event) { - if (!$(event.target).closest(".note-toolbar").length) { // prevent icon edition of top bar for default summernote - new MediaDialog(oLayoutInfo.editor, event.target).appendTo(document.body); - } - }); - $(document).on("keyup", reRangeSelectKey); - - var clone_data = false; - var $node = oLayoutInfo.editor; - if ($node.data('oe-model')) { - $node.on('content_changed', function () { - - var $nodes = $('[data-oe-model]') - .filter(function () { return this != $node[0];}) - .filter('[data-oe-model="'+$node.data('oe-model')+'"]') - .filter('[data-oe-id="'+$node.data('oe-id')+'"]') - .filter('[data-oe-field="'+$node.data('oe-field')+'"]'); - if ($node.data('oe-type')) $nodes = $nodes.filter('[data-oe-type="'+$node.data('oe-type')+'"]'); - if ($node.data('oe-expression')) $nodes = $nodes.filter('[data-oe-expression="'+$node.data('oe-expression')+'"]'); - if ($node.data('oe-xpath')) $nodes = $nodes.filter('[data-oe-xpath="'+$node.data('oe-xpath')+'"]'); - if ($node.data('oe-contact-options')) $nodes = $nodes.filter('[data-oe-contact-options="'+$node.data('oe-contact-options')+'"]'); - - var nodes = $node.get(); - - if ($node.data('oe-type') === "many2one") { - $nodes = $nodes.add($('[data-oe-model]') - .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; }) - .filter('[data-oe-many2one-model="'+$node.data('oe-many2one-model')+'"]') - .filter('[data-oe-many2one-id="'+$node.data('oe-many2one-id')+'"]') - .filter('[data-oe-type="many2one"]')); - - $nodes = $nodes.add($('[data-oe-model]') - .filter(function () { return this != $node[0] && nodes.indexOf(this) === -1; }) - .filter('[data-oe-model="'+$node.data('oe-many2one-model')+'"]') - .filter('[data-oe-id="'+$node.data('oe-many2one-id')+'"]') - .filter('[data-oe-field="name"]')); - } - - if (!clone_data) { - clone_data = true; - $nodes.html(this.innerHTML); - clone_data = false; - } - }); - } -}; -var fn_dettach = eventHandler.dettach; -eventHandler.dettach = function (oLayoutInfo, options) { - fn_dettach.call(this, oLayoutInfo, options); - oLayoutInfo.editor.off("dragstart"); - $(document).off('mousedown', summernote_mousedown); - $(document).off('mouseup', summernote_mouseup); - oLayoutInfo.editor.off("dblclick"); - $(document).off("keyup", reRangeSelectKey); -}; - -////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* Translation for odoo */ - -$.summernote.lang.odoo = { - font: { - bold: _t('Bold'), - italic: _t('Italic'), - underline: _t('Underline'), - strikethrough: _t('Strikethrough'), - subscript: _t('Subscript'), - superscript: _t('Superscript'), - clear: _t('Remove Font Style'), - height: _t('Line Height'), - name: _t('Font Family'), - size: _t('Font Size') - }, - image: { - image: _t('Picture'), - insert: _t('Insert Image'), - resizeFull: _t('Resize Full'), - resizeHalf: _t('Resize Half'), - resizeQuarter: _t('Resize Quarter'), - floatLeft: _t('Float Left'), - floatRight: _t('Float Right'), - floatNone: _t('Float None'), - dragImageHere: _t('Drag an image here'), - selectFromFiles: _t('Select from files'), - url: _t('Image URL'), - remove: _t('Remove Image') - }, - link: { - link: _t('Link'), - insert: _t('Insert Link'), - unlink: _t('Unlink'), - edit: _t('Edit'), - textToDisplay: _t('Text to display'), - url: _t('To what URL should this link go?'), - openInNewWindow: _t('Open in new window') - }, - video: { - video: _t('Video'), - videoLink: _t('Video Link'), - insert: _t('Insert Video'), - url: _t('Video URL?'), - providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)') - }, - table: { - table: _t('Table') - }, - hr: { - insert: _t('Insert Horizontal Rule') - }, - style: { - style: _t('Style'), - normal: _t('Normal'), - blockquote: _t('Quote'), - pre: _t('Code'), - h1: _t('Header 1'), - h2: _t('Header 2'), - h3: _t('Header 3'), - h4: _t('Header 4'), - h5: _t('Header 5'), - h6: _t('Header 6') - }, - lists: { - unordered: _t('Unordered list'), - ordered: _t('Ordered list') - }, - options: { - help: _t('Help'), - fullscreen: _t('Full Screen'), - codeview: _t('Code View') - }, - paragraph: { - paragraph: _t('Paragraph'), - outdent: _t('Outdent'), - indent: _t('Indent'), - left: _t('Align left'), - center: _t('Align center'), - right: _t('Align right'), - justify: _t('Justify full') - }, - color: { - recent: _t('Recent Color'), - more: _t('More Color'), - background: _t('Background Color'), - foreground: _t('Foreground Color'), - transparent: _t('Transparent'), - setTransparent: _t('Set transparent'), - reset: _t('Reset'), - resetToDefault: _t('Reset to default') - }, - shortcut: { - shortcuts: _t('Keyboard shortcuts'), - close: _t('Close'), - textFormatting: _t('Text formatting'), - action: _t('Action'), - paragraphFormatting: _t('Paragraph formatting'), - documentStyle: _t('Document Style') +/** + * Object who contains all method and bind for the top bar, the template is create server side. + */ +website.TopBar.include({ + events: { + 'click [data-action="edit"]': 'edit', }, - history: { - undo: _t('Undo'), - redo: _t('Redo') - } -}; - -////////////////////////////////////////////////////////////////////////////////////////////////////////// -/* Change History to have a global History for all summernote instances */ - -var History = function History ($editable) { - var aUndo = []; - var pos = 0; - - this.makeSnap = function () { - var rng = range.create(), - elEditable = dom.ancestor(rng && rng.commonAncestor(), dom.isEditable) || $('.o_editable:first')[0]; - return { - editable: elEditable, - contents: elEditable.innerHTML, - bookmark: rng && rng.bookmark(elEditable), - scrollTop: $(elEditable).scrollTop() - }; - }; - - this.applySnap = function (oSnap) { - var $editable = $(oSnap.editable); - - if (!!document.documentMode) { - $editable.removeAttr("contentEditable").removeProp("contentEditable"); - } - - $editable.html(oSnap.contents).scrollTop(oSnap.scrollTop); - $(".oe_overlay").remove(); - $(".note-control-selection").hide(); - - $editable.trigger("content_changed"); - - if (!oSnap.bookmark) { - return; - } - - try { - var r = range.createFromBookmark(oSnap.editable, oSnap.bookmark); - r.select(); - } catch(e) { - console.error(e); - return; - } - - $(document).trigger("click"); - $(".o_editable *").filter(function () { - var $el = $(this); - if($el.data('snippet-editor')) { - $el.removeData(); + start: function () { + var self = this; + $("#wrapwrap").on('click', '.o_editable, [data-oe-model]', 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); } }); - setTimeout(function () { - var target = dom.isBR(r.sc) ? r.sc.parentNode : dom.node(r.sc); - if (!target) { - return; - } - var evt = document.createEvent("MouseEvents"); - evt.initMouseEvent("mousedown", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target); - target.dispatchEvent(evt); - - var other_evt = document.createEvent("MouseEvents"); - other_evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target); - target.dispatchEvent(other_evt); - },0); - }; - - this.undo = function () { - if (!pos) { return; } - last = null; - if (!aUndo[pos]) aUndo[pos] = this.makeSnap(); - if (aUndo[pos-1].jump) pos--; - this.applySnap(aUndo[--pos]); - }; - this.hasUndo = function () { - return pos > 0; - }; - - this.redo = function () { - if (aUndo.length <= pos+1) { return; } - if (aUndo[pos].jump) pos++; - this.applySnap(aUndo[++pos]); - }; - this.hasRedo = function () { - return aUndo.length > pos+1; - }; + $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]:empty") + .addClass("oe_empty") + .attr("data-oe-placeholder", _t("Press The Top-Left Edit Button")); - this.popUndo = function () { - pos--; - aUndo.pop(); - }; - - var last; - this.recordUndo = function ($editable, event, internal_history) { - if (!internal_history) { - if (!event || !last || !aUndo[pos-1] || aUndo[pos-1].editable !== $editable[0]) { // don't trigger change for all keypress - $(".o_editable.note-editable").trigger("content_changed"); - } - } - if (event) { - if (last && aUndo[pos-1] && aUndo[pos-1].editable !== $editable[0]) { - // => make a snap when the user change editable zone (because: don't make snap for each keydown) - aUndo.splice(pos, aUndo.length); - var prev = aUndo[pos-1]; - aUndo[pos] = { - editable: prev.editable, - contents: $(prev.editable).html(), - bookmark: prev.bookmark, - scrollTop: prev.scrollTop, - jump: true - }; - pos++; - } - else if (event === last) return; + if (location.search.indexOf("enable_editor") >= 0 && $('html').attr('lang') === "en_US") { + this.$el.hide(); } - last = event; - aUndo.splice(pos, aUndo.length); - aUndo[pos] = this.makeSnap($editable); - pos++; - }; - - this.splitNext = function () { - last = false; - }; -}; -var history = new History(); - -////////////////////////////////////////////////////////////////////////////////////////////////////////// -// add focusIn to jQuery to allow to move caret into a div of a contentEditable area -$.fn.extend({ - focusIn: function () { - if (this.length) { - range.create(dom.firstChild(this[0]), 0).select(); - } - return this; - }, - selectContent: function () { - if (this.length) { - var next = dom.lastChild(this[0]); - range.create(dom.firstChild(this[0]), 0, next, next.textContent.length).select(); - } - return this; + return this._super(); }, - activateBlock: function () { - var target = snippet_editor.snippet.globalSelector.closest($(this))[0] || (dom.isBR(this) ? this.parentNode : dom.node(this)); - var evt = document.createEvent("MouseEvents"); - evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target); - target.dispatchEvent(evt); - return this; + edit: function () { + this.$('button[data-action=edit]').prop('disabled', true); + this.$el.hide(); + editor.editor_bar = new editor.Class(this); + editor.editor_bar.prependTo(document.body); } }); -////////////////////////////////////////////////////////////////////////////////////////////////////////// - -function change_default_bootstrap_animation_to_edit() { - var fn_carousel = $.fn.carousel; - $.fn.carousel = function () { - var res = fn_carousel.apply(this, arguments); - // off bootstrap keydown event to remove event.preventDefault() - // and allow to change cursor position - $(this).off('keydown.bs.carousel'); - return res; - }; -} - -////////////////////////////////////////////////////////////////////////////////////////////////////////// - -website.no_editor = !!$(document.documentElement).data('editable-no-editor'); - -website.add_template_file('/website/static/src/xml/website.editor.xml'); -website.dom_ready.done(function () { - website.ready().then(init_editor); - - $(document).on('click', 'a.js_link2post', function (ev) { - ev.preventDefault(); - website.form(this.pathname, 'POST'); - }); - - $(document).on('click', '.note-editable', function (ev) { - ev.preventDefault(); - }); - - $(document).on('submit', '.note-editable form .btn', function (ev) { - // Disable form submition in editable mode - ev.preventDefault(); - }); - - $(document).on('hide.bs.dropdown', '.dropdown', function (ev) { - // Prevent dropdown closing when a contenteditable children is focused - if (ev.originalEvent - && $(ev.target).has(ev.originalEvent.target).length - && $(ev.originalEvent.target).is('[contenteditable]')) { - ev.preventDefault(); - } - }); -}); - - -/* ----- TOP EDITOR BAR FOR ADMIN ---- */ -var EditorBar = Widget.extend({ - template: 'website.editorbar', - events: { - 'click button[data-action=save]': 'save', - 'click a[data-action=cancel]': 'cancel', - }, - start: function() { - var self = this; - this.saving_mutex = new utils.Mutex(); - - this.$buttons = { - edit: this.$el.parents().find('button[data-action=edit]'), - save: this.$('button[data-action=save]'), - cancel: this.$('button[data-action=cancel]'), - }; - - this.$('#website-top-edit').hide(); - this.$('#website-top-view').show(); - - var $edit_button = this.$buttons.edit - .prop('disabled', website.no_editor); - if (website.no_editor) { - var help_text = $(document.documentElement).data('editable-no-editor'); - $edit_button.parent() - // help must be set on form above button because it does - // not appear on disabled button - .attr('title', help_text); - } - - $('.dropdown-toggle').dropdown(); - - this.$buttons.edit.click(function(ev) { - self.edit(); - }); - - this.rte = new RTE(this); - this.rte.on('change', this, this.proxy('rte_changed')); - this.rte.on('rte:ready', this, function () { - self.trigger('rte:ready'); - }); - - this.rte.appendTo(this.$('#website-top-edit .nav.js_editor_placeholder')); - return this._super.apply(this, arguments); - }, - edit: function (no_editor) { - this.$buttons.edit.prop('disabled', true); - this.$('#website-top-view').hide(); - this.$el.show(); - this.$('#website-top-edit').show(); - - if (!no_editor) { - this.rte.start_edition(); - this.trigger('rte:called'); - } - - var flag = false; - window.onbeforeunload = function(event) { - if ($('.o_editable.o_dirty').length && !flag) { - flag = true; - setTimeout(function () {flag=false;},0); - return _t('This document is not saved!'); - } - }; - }, - rte_changed: function () { - this.$buttons.save.prop('disabled', false); - }, - _save: function () { - var self = this; - - var saved = {}; // list of allready saved views and data - - var defs = $('.o_editable') - .filter('.o_dirty') - .removeAttr('contentEditable') - .removeClass('o_dirty o_editable oe_carlos_danger o_is_inline_editable') - .map(function () { - var $el = $(this); - - // remove multi edition - var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression'); - if (saved[key]) return true; - saved[key] = true; - - // TODO: Add a queue with concurrency limit in webclient - // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js - return self.saving_mutex.exec(function () { - return self.saveElement($el) - .then(undefined, function (thing, response) { - // because ckeditor regenerates all the dom, - // we can't just setup the popover here as - // everything will be destroyed by the DOM - // regeneration. Add markings instead, and - // returns a new rejection with all relevant - // info - var id = _.uniqueId('carlos_danger_'); - $el.addClass('o_dirty oe_carlos_danger'); - $el.addClass(id); - return $.Deferred().reject({ - id: id, - error: response.data, - }); - }); - }); - }).get(); - return $.when.apply(null, defs).then(function () { - window.onbeforeunload = null; - website.reload(); - }, function (failed) { - // If there were errors, re-enable edition - self.rte.start_edition(true); - // jquery's deferred being a pain in the ass - if (!_.isArray(failed)) { failed = [failed]; } - - _(failed).each(function (failure) { - var html = failure.error.exception_type === "except_osv"; - if (html) { - var msg = $("<div/>").text(failure.error.message).html(); - var data = msg.substring(3,msg.length-2).split(/', u'/); - failure.error.message = '<b>' + data[0] + '</b>' + dom.blank + data[1]; - } - $(root).find('.' + failure.id) - .removeClass(failure.id) - .popover({ - html: html, - trigger: 'hover', - content: failure.error.message, - placement: 'auto top', - }) - // Force-show popovers so users will notice them. - .popover('show'); - }); - }); - }, - save: function () { - return this._save().then(function () { - website.reload(); - }); - }, - /** - * Saves an RTE content, which always corresponds to a view section (?). - */ - save_without_reload: function () { - return this._save(); - }, - saveElement: function ($el) { - var markup = $el.prop('outerHTML'); - return ajax.jsonRpc('/web/dataset/call', 'call', { - model: 'ir.ui.view', - method: 'save', - args: [$el.data('oe-id'), markup, - $el.data('oe-xpath') || null, - website.get_context()], - }); - }, - cancel: function () { - new $.Deferred(function (d) { - var $dialog = $(core.qweb.render('website.editor.discard')).appendTo(document.body); - $dialog.on('click', '.btn-danger', function () { - d.resolve(); - }).on('hidden.bs.modal', function () { - d.reject(); - }); - d.always(function () { - $dialog.remove(); - }); - $dialog.modal('show'); - }).then(function () { - window.onbeforeunload = null; - website.reload(); - }); - }, -}); +/* ----- Customize Template ---- */ -var EditorBarCustomize = Widget.extend({ +website.TopBarCustomize = Widget.extend({ events: { 'mousedown a.dropdown-toggle': 'load_menu', 'click ul a[data-view-id]': 'do_customize', @@ -1183,349 +93,29 @@ var EditorBarCustomize = Widget.extend({ args: [], kwargs: { ids: [parseInt(view_id, 10)], - context: website.get_context() + context: base.get_context() } }).then( function() { window.location.reload(); }); }, }); - -$(document).ready(function() { - var editorBarCustomize = new EditorBarCustomize(); - editorBarCustomize.setElement($('li[id=customize-menu]')); - editorBarCustomize.start(); -}); - -/* ----- RICH TEXT EDITOR ---- */ - -var RTE = Widget.extend({ - init: function (EditorBar) { - this.EditorBar = EditorBar; - $('.inline-media-link').remove(); - this._super.apply(this, arguments); - - computeFonts(); - }, - /** - * Add a record undo to history - * @param {DOM} target where the dom is changed is editable zone - */ - historyRecordUndo: function ($target, internal_history) { - var rng = range.create(); - var $editable = $($target || (rng && rng.sc)).closest(".o_editable"); - if ($editable.length) { - rng = $editable.data('range') || rng; - } - if (!rng && $target.length) { - rng = range.create($target.closest("*")[0],0); - } - if (rng) { - try { - rng.select(); - } catch (e) { - console.error(e); - } - } - $target = $(rng.sc); - $target.mousedown(); - this.history.recordUndo($target, null, internal_history); - $target.mousedown(); - }, - /** - * Makes the page editable - * - * @param {Boolean} [restart=false] in case the edition was already set - * up once and is being re-enabled. - * @returns {$.Deferred} deferred indicating when the RTE is ready - */ - start_edition: function (restart) { - var self = this; - - change_default_bootstrap_animation_to_edit(); - - this.history = history; - - // handler for cancel editor - $(document).on('keydown', function (event) { - if (event.keyCode === 27 && !$('.modal-content:visible').length) { - setTimeout(function () { - $('#website-top-navbar [data-action="cancel"]').click(); - var $modal = $('.modal-content > .modal-body').parents(".modal:first"); - $modal.off('keyup.dismiss.bs.modal'); - setTimeout(function () { - $modal.on('keyup.dismiss.bs.modal', function () { - $(this).modal('hide'); - }); - },500); - },0); - } - }); - - // activate editor - var $last; - $(document).on('mousedown', function (event) { - var $target = $(event.target); - var $editable = $target.closest('.o_editable'); - - if (!$editable.size()) { - return; - } - - if ($last && (!$editable.size() || $last[0] != $editable[0])) { - var $destroy = $last; - setTimeout(function () {$destroy.destroy();},150); // setTimeout to remove flickering when change to editable zone (re-create an editor) - $last = null; - } - if ($editable.size() && (!$last || $last[0] != $editable[0]) && - ($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') { - $editable.summernote(self._config()); - $editable.data('NoteHistory', self.history); - $editable.data('rte', self); - $last = $editable; - - // firefox & IE fix - try { - document.execCommand('enableObjectResizing', false, false); - document.execCommand('enableInlineTableEditing', false, false); - document.execCommand( '2D-position', false, false); - } catch (e) {} - document.body.addEventListener('resizestart', function (evt) {evt.preventDefault(); return false;}); - document.body.addEventListener('movestart', function (evt) {evt.preventDefault(); return false;}); - document.body.addEventListener('dragstart', function (evt) {evt.preventDefault(); return false;}); - - if (!range.create()) { - range.create($editable[0],0).select(); - } - - $target.trigger('mousedown'); // for activate selection on picture - setTimeout(function () { - self.historyRecordUndo($editable, true); - },0); - } - }); - - $('.o_not_editable').attr("contentEditable", false); - - $('#wrapwrap [data-oe-model]') - .not('.o_not_editable') - .filter(function () { - return !$(this).closest('.o_not_editable').length; - }) - .not('link, script') - .not('[data-oe-readonly]') - .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]') - .not('.oe_snippet_editor') - .addClass('o_editable'); - - $('.o_editable').each(function () { - var node = this; - var $node = $(node); - - // add class to display inline-block for empty t-field - if(window.getComputedStyle(node).display === "inline" && $node.data('oe-type') !== "image") { - $node.addClass('o_is_inline_editable'); - } - - // start element observation - $(node).one('content_changed', function () { - $node.addClass('o_dirty'); - }); - $(node).on('content_changed', function () { - self.trigger('change'); - }); - }); - - $(document).trigger('mousedown'); - - if (!restart) { - $('#wrapwrap, .o_editable').on('click', '*', function (event) { - event.preventDefault(); - }); - - $('body').addClass("editor_enable"); - - $(document) - .tooltip({ - selector: '[data-oe-readonly]', - container: 'body', - trigger: 'hover', - delay: { "show": 1000, "hide": 100 }, - placement: 'bottom', - title: _t("Readonly field") - }) - .on('click', function () { - $(this).tooltip('hide'); - }); - - self.trigger('rte:ready'); - } - }, - - _config: function () { - return { - airMode : true, - focus: false, - airPopover: [ - ['style', ['style']], - ['font', ['bold', 'italic', 'underline', 'clear']], - ['fontsize', ['fontsize']], - ['color', ['color']], - ['para', ['ul', 'ol', 'paragraph']], - ['table', ['table']], - ['insert', ['link', 'picture']], - ['history', ['undo', 'redo']], - ], - oninit: function() { - }, - styleWithSpan: false, - inlinemedia : ['p'], - lang: "odoo" - }; +website.TopBar.include({ + start: function () { + this.customize_menu = new website.TopBarCustomize(); + var def = this.customize_menu.attachTo($('#customize-menu')); + return $.when(this._super(), def); } }); /* ----- EDITOR: LINK & MEDIA ---- */ -var Dialog = Widget.extend({ - events: { - 'hidden.bs.modal': 'destroy', - 'click button.save': 'save', - 'click button[data-dismiss="modal"]': 'cancel', - }, - init: function () { - this._super(); - }, - start: function () { - var sup = this._super(); - this.$el.modal({backdrop: 'static'}); - this.$('input:first').focus(); - return sup; - }, - save: function () { - this.close(); - this.trigger("saved"); - }, - cancel: function () { - this.trigger("cancel"); - }, - close: function () { - this.$el.modal('hide'); - }, - destroy: function () { - this.$el.modal('hide').remove(); - if($(".modal.in").length>0){ - $('body').addClass('modal-open'); - } - }, -}); - -var LinkDialog = Dialog.extend({ - template: 'website.editor.dialog.link', - events: _.extend({}, Dialog.prototype.events, { - 'change :input.url-source': 'changed', - 'keyup :input.url': 'onkeyup', - 'keyup :input': 'preview', - 'click button.remove': 'remove_link', - 'change input#link-text': function (e) { - this.text = $(e.target).val(); - }, - 'change .link-style': function (e) { - this.preview(); - }, - }), - init: function (editable, linkInfo) { - this._super(editable, linkInfo); - this.editable = editable; - this.data = linkInfo || {}; - - this.data.className = ""; - if (this.data.range) { - this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || ""; - this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' '); - - var is_link = this.data.range.isOnAnchor(); - var r = this.data.range; - - var sc = r.sc; - var so = r.so; - var ec = r.ec; - var eo = r.eo; - - var nodes; - if (!is_link) { - if (sc.tagName) { - sc = dom.firstChild(so ? sc.childNodes[so] : sc); - so = 0; - } else if (so !== sc.textContent.length) { - if (sc === ec) { - ec = sc = sc.splitText(so); - eo -= so; - } else { - sc = sc.splitText(so); - } - so = 0; - } - if (ec.tagName) { - ec = dom.lastChild(eo ? ec.childNodes[eo-1] : ec); - eo = ec.textContent.length; - } else if (eo !== ec.textContent.length) { - ec.splitText(eo); - } - - nodes = dom.listBetween(sc, ec); - - // browsers can't target a picture or void node - if (dom.isVoid(sc) || dom.isImg(sc)) { - so = dom.listPrev(sc).length-1; - sc = sc.parentNode; - } - if (dom.isBR(ec)) { - eo = dom.listPrev(ec).length-1; - ec = ec.parentNode; - } else if (dom.isVoid(ec) || dom.isImg(sc)) { - eo = dom.listPrev(ec).length; - ec = ec.parentNode; - } - - this.data.range = range.create(sc, so, ec, eo); - this.data.range.select(); - } else { - nodes = dom.ancestor(sc, dom.isAnchor).childNodes; - } - - if (dom.isImg(sc) && nodes.indexOf(sc) === -1) { - nodes.push(sc); - } - if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) { - var text = ""; - this.data.images = []; - for (var i=0; i<nodes.length; i++) { - if (dom.ancestor(nodes[i], dom.isImg)) { - this.data.images.push(dom.ancestor(nodes[i], dom.isImg)); - text += '[IMG]'; - } else if (!is_link && i===0) { - text += nodes[i].textContent.slice(so, Infinity); - } else if (!is_link && i===nodes.length-1) { - text += nodes[i].textContent.slice(0, eo); - } else { - text += nodes[i].textContent; - } - } - this.data.text = text; - } - } - - this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' '); - - // Store last-performed request to be able to cancel/abort it. - this.page_exists_req = null; - this.search_pages_req = null; - this.bind_data(); - }, - start: function () { +widget.LinkDialog.include({ + bind_data: function () { var self = this; + var href = this.data.url; var last; + this.$('#link-page').select2({ minimumInputLength: 1, placeholder: _t("New or existing page"), @@ -1555,37 +145,24 @@ var LinkDialog = Dialog.extend({ }); }, }); - return this._super().then(this.proxy('bind_data')); - }, - get_data: function (test) { - var def = new $.Deferred(), - $e = this.$('.active input.url-source').filter(':input'), - val = $e.val(), - label = this.$('#link-text').val() || val; - - if (label && this.data.images) { - for(var i=0; i<this.data.images.length; i++) { - label = label.replace(/</, "<").replace(/>/, ">").replace(/\[IMG\]/, this.data.images[i].outerHTML); - } - } - if (!test && (!val || !$e[0].checkValidity())) { - // FIXME: error message - $e.closest('.form-group').addClass('has-error'); - $e.focus(); - def.reject(); + if (href) { + this.page_exists(href).then(function (exist) { + if (exist) { + self.$('#link-page').select2('data', {'id': href, 'text': href}).change(); + } else { + self.$('input.url').val(href).change(); + self.$('input.window-new').closest("div").show(); + } + }); } - - var style = this.$("input[name='link-style-type']:checked").val() || ''; - var size = this.$("input[name='link-style-size']:checked").val() || ''; - var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size; - var isNewWindow = this.$('input.window-new').prop('checked'); - - if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) { - def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes); - } else if ($e.val() && $e.val().length && $e.hasClass('page')) { + return this._super(); + }, + get_data_buy_url: function (def, $e, isNewWindow, label, classes) { + var val = $e.val(); + if (val && val.length && $e.hasClass('page')) { var data = $e.select2('data'); - if (test || !data.create) { + if (!data.create) { def.resolve(data.id, isNewWindow, label || data.text, classes); } else { // Create the page, get the URL back @@ -1598,76 +175,12 @@ var LinkDialog = Dialog.extend({ } else { def.resolve(val, isNewWindow, label, classes); } - return def; - }, - save: function () { - var self = this; - var _super = this._super.bind(this); - return this.get_data() - .then(function (url, new_window, label, classes) { - self.data.url = url; - self.data.newWindow = new_window; - self.data.text = label; - self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, ''); - - self.trigger("save", self.data); - }).then(_super); - }, - bind_data: function () { - var self = this; - var href = this.data.url; - var new_window = this.data.isNewWindow; - var text = this.data.text; - var classes = this.data.iniClassName; - - this.$('input#link-text').val(text); - this.$('input.window-new').prop('checked', new_window); - - if (classes) { - this.$('input[value!=""]').each(function () { - var $option = $(this); - if (classes.indexOf($option.val()) !== -1) { - $option.attr("checked", "checked"); - } - }); - } - - var match, $control; - if (href && (match = /mailto:(.+)/.exec(href))) { - this.$('input.email-address').val(match[1]).change(); - } - if (href && !$control) { - this.page_exists(href).then(function (exist) { - if (exist) { - self.$('#link-page').select2('data', {'id': href, 'text': href}); - } else { - self.$('input.url').val(href).change(); - self.$('input.window-new').closest("div").show(); - } - }); - } - - this.page_exists(href).then(function (exist) { - if (exist) { - self.$('#link-page').select2('data', {'id': href, 'text': href}); - } else { - self.$('input.url').val(href).change(); - self.$('input.window-new').closest("div").show(); - } - }); - - this.preview(); }, changed: function (e) { - var $e = $(e.target); - this.$('.url-source').filter(':input').not($e).val('') + this.$('.url-source').filter(':input').not($(e.target)).val('') .filter(function () { return !!$(this).data('select2'); }) .select2('data', null); - $e.closest('.list-group-item') - .addClass('active') - .siblings().removeClass('active') - .addBack().removeClass('has-error'); - this.preview(); + this._super(e); }, call: function (method, args, kwargs) { var self = this; @@ -1684,859 +197,15 @@ var LinkDialog = Dialog.extend({ }, page_exists: function (term) { return this.call('page_exists', [null, term], { - context: website.get_context(), + context: base.get_context(), }); }, fetch_pages: function (term) { return this.call('search_pages', [null, term], { limit: 9, - context: website.get_context(), + context: base.get_context(), }); }, - onkeyup: function (e) { - var $e = $(e.target); - var is_link = ($e.val()||'').length && $e.val().indexOf("@") === -1; - this.$('input.window-new').closest("div").toggle(is_link); - this.preview(); - }, - preview: function () { - var $preview = this.$("#link-preview"); - this.get_data(true).then(function (url, new_window, label, classes) { - $preview.attr("target", new_window ? '_blank' : "") - .attr("href", url && url.length ? url : "#") - .html((label && label.length ? label : url)) - .attr("class", classes.replace(/pull-\w+/, '')); - }); - } }); -/** - * alt widget. Lets users change a alt & title on a media - */ -var alt = Dialog.extend({ - template: 'website.editor.dialog.alt', - init: function ($editable, media) { - this.$editable = $editable; - this.media = media; - this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"'); - this.title = ($(this.media).attr('title') || "").replace(/"/g, '"'); - return this._super(); - }, - save: function () { - var self = this; - range.createFromNode(self.media).select(); - this.$editable.data('NoteHistory').recordUndo(); - var alt = this.$('#alt').val(); - var title = this.$('#title').val(); - $(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null); - setTimeout(function () { - click_event(self.media, "mouseup"); - },0); - return this._super(); - }, -}); - -var click_event = function(el, type) { - var evt = document.createEvent("MouseEvents"); - evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); - el.dispatchEvent(evt); -}; - -/** - * MediaDialog widget. Lets users change a media, including uploading a - * new image, font awsome or video and can change a media into an other - * media - * - * options: select_images: allow the selection of more of one image - */ -var MediaDialog = Dialog.extend({ - template: 'website.editor.dialog.media', - events : _.extend({}, Dialog.prototype.events, { - 'input input#icon-search': 'search', - }), - init: function ($editable, media, options) { - this._super(); - if ($editable) { - this.$editable = $editable; - this.rte = this.$editable.rte || this.$editable.data('rte'); - } - this.options = options || {}; - this.old_media = media; - this.media = media; - this.isNewMedia = !media; - this.range = range.create(); - }, - start: function () { - var self = this; - - this.only_images = this.options.only_images || this.options.select_images || (this.media && $(this.media).parent().data("oe-field") === "image"); - if (this.only_images) { - this.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); - } - - if (this.media) { - if (this.media.nodeName === "IMG") { - this.$('[href="#editor-media-image"]').tab('show'); - } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) { - this.$('[href="#editor-media-video"]').tab('show'); - } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) { - this.media = this.media.parentNode; - this.$('[href="#editor-media-video"]').tab('show'); - } else if (this.media.className.match(/(^|\s)fa($|\s)/)) { - this.$('[href="#editor-media-icon"]').tab('show'); - } - } - - this.imageDialog = new ImageDialog(this, this.media, this.options); - this.imageDialog.appendTo(this.$("#editor-media-image")); - this.iconDialog = new FontIconsDialog(this, this.media, this.options); - this.iconDialog.appendTo(this.$("#editor-media-icon")); - this.videoDialog = new VideoDialog(this, this.media, this.options); - this.videoDialog.appendTo(this.$("#editor-media-video")); - - this.active = this.imageDialog; - - $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) { - if ($(event.target).is('[href="#editor-media-image"]')) { - self.active = self.imageDialog; - self.$('li.search, li.previous, li.next').removeClass("hidden"); - } else if ($(event.target).is('[href="#editor-media-icon"]')) { - self.active = self.iconDialog; - self.$('li.search, li.previous, li.next').removeClass("hidden"); - self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden"); - } else if ($(event.target).is('[href="#editor-media-video"]')) { - self.active = self.videoDialog; - self.$('.nav-tabs li.search').addClass("hidden"); - } - }); - - return this._super(); - }, - save: function () { - if (this.options.select_images) { - this.trigger("saved", this.active.save()); - this.close(); - return; - } - if(this.rte) { - this.range.select(); - this.rte.historyRecordUndo(this.media); - } - - var self = this; - if (self.media) { - this.media.innerHTML = ""; - if (this.active !== this.imageDialog) { - this.imageDialog.clear(); - } - if (this.active !== this.iconDialog) { - this.iconDialog.clear(); - } - if (this.active !== this.videoDialog) { - this.videoDialog.clear(); - } - } else { - this.media = document.createElement("img"); - this.range.insertNode(this.media, true); - this.active.media = this.media; - } - this.active.save(); - - if (this.active.add_class) { - $(this.media).addClass(this.active.add_class); - } - - $(document.body).trigger("media-saved", [self.active.media, self.old_media]); - self.trigger("saved", [self.active.media, self.old_media]); - setTimeout(function () { - if (!self.active.media.parentNode) { - return; - } - range.createFromNode(self.active.media).select(); - click_event(self.active.media, "mousedown"); - if (!this.only_images) { - setTimeout(function () { - if($(self.active.media).parent().data("oe-field") !== "image") { - click_event(self.active.media, "click"); - } - click_event(self.active.media, "mouseup"); - },0); - } - },0); - - this.close(); - }, - searchTimer: null, - search: function () { - var self = this; - var needle = this.$("input#icon-search").val(); - clearTimeout(this.searchTimer); - this.searchTimer = setTimeout(function () { - self.active.search(needle || ""); - },250); - } -}); - -/** - * ImageDialog widget. Lets users change an image, including uploading a - * new image in OpenERP or selecting the image style (if supported by - * the caller). - */ -var IMAGES_PER_ROW = 6; -var IMAGES_ROWS = 2; -var ImageDialog = Widget.extend({ - template: 'website.editor.dialog.image', - events: _.extend({}, Dialog.prototype.events, { - 'change .url-source': function (e) { - this.changed($(e.target)); - }, - 'click button.filepicker': function () { - var filepicker = this.$('input[type=file]'); - if (!_.isEmpty(filepicker)){ - filepicker[0].click(); - } - }, - 'click .js_disable_optimization': function () { - this.$('input[name="disable_optimization"]').val('1'); - var filepicker = this.$('button.filepicker'); - if (!_.isEmpty(filepicker)){ - filepicker[0].click(); - } - }, - 'change input[type=file]': 'file_selection', - 'submit form': 'form_submit', - 'change input.url': "change_input", - 'keyup input.url': "change_input", - //'change select.image-style': 'preview_image', - 'click .existing-attachments img': 'select_existing', - 'click .existing-attachment-remove': 'try_remove', - }), - init: function (parent, media, options) { - this._super(); - this.options = options || {}; - this.parent = parent; - this.media = media; - this.images = []; - this.page = 0; - }, - start: function () { - this.$preview = this.$('.preview-container').detach(); - var self = this; - var res = this._super(); - var o = { url: null, alt: null }; - // avoid typos, prevent addition of new properties to the object - Object.preventExtensions(o); - - if ($(this.media).is("img")) { - o.url = this.media.getAttribute('src'); - } else { - this.add_class = "img-responsive pull-left"; - } - this.parent.$(".pager > li").click(function (e) { - e.preventDefault(); - var $target = $(e.currentTarget); - if ($target.hasClass('disabled')) { - return; - } - self.page += $target.hasClass('previous') ? -1 : 1; - self.display_attachments(); - }); - this.set_image(o.url, o.alt); - this.fetch_existing(); - return res; - }, - push: function (url, alt, id) { - if (this.options.select_images) { - var img = _.select(this.images, function (v) { return v.url == url;}); - if (img.length) { - this.images.splice(this.images.indexOf(img[0]),1); - return; - } - } else { - this.images = []; - } - this.images.push({'url': url, 'alt': alt, 'id': id}); - }, - save: function () { - if (this.options.select_images) { - this.parent.trigger("save", this.images); - return this.images; - } - this.parent.trigger("save", this.media); - - var img = this.images[0] || { - 'url': this.$(".existing-attachments img:first").attr('src'), - 'alt': this.$(".existing-attachments img:first").attr('alt') - }; - - if (this.media.tagName !== "IMG") { - var media = document.createElement('img'); - $(this.media).replaceWith(media); - this.media = media; - } - - $(this.media).attr('src', img.url).attr('alt', img.alt); - - var style = this.style; - this.media.setAttribute('src', img.url); - if (style) { this.media.addClass(style); } - - return this.media; - }, - clear: function () { - this.media.className = this.media.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' '); - }, - cancel: function () { - this.trigger('cancel'); - }, - change_input: function (e) { - var $input = $(e.target); - var $button = $input.parent().find("button"); - if ($input.val() === "") { - $button.addClass("btn-default").removeClass("btn-primary"); - } else { - $button.removeClass("btn-default").addClass("btn-primary"); - } - }, - search: function (needle) { - var self = this; - this.fetch_existing(needle).then(function () { - self.selected_existing(); - }); - }, - set_image: function (url, alt) { - var self = this; - if (url) { - this.push(url, alt); - } - this.$('input.url').val(''); - this.fetch_existing().then(function () { - self.selected_existing(); - }); - }, - form_submit: function (event) { - var self = this; - var $form = this.$('form[action="/website/attach"]'); - if (!$form.find('input[name="upload"]').val().length) { - $form.find('input[name="url"]').val(); - if (this.selected_existing().size()) { - event.preventDefault(); - return false; - } - } - $form.find('.well > div').hide().last().after('<span class="fa fa-spin fa-3x fa-refresh"/>'); - - var callback = _.uniqueId('func_'); - this.$('input[name=func]').val(callback); - window[callback] = function (attachments, error) { - delete window[callback]; - $form.find('.well > span').remove(); - $form.find('.well > div').show(); - if (error || !attachments.length) { - self.file_selected(null, error || !attachments.length); - } - for (var i=0; i<attachments.length; i++) { - self.file_selected(attachments[i]['website_url'], error); - } - }; - }, - file_selection: function () { - this.$el.addClass('nosave'); - this.$('form').removeClass('has-error').find('.help-block').empty(); - this.$('button.filepicker').removeClass('btn-danger btn-success'); - this.$('form').submit(); - }, - file_selected: function(url, error) { - var $button = this.$('button.filepicker'); - if (!error) { - $button.addClass('btn-success'); - } else { - url = null; - this.$('form').addClass('has-error') - .find('.help-block').text(error); - $button.addClass('btn-danger'); - } - this.set_image(url, null, error); - - if (!this.options.select_images) { - // auto save and close popup - this.parent.save(); - } - }, - fetch_existing: function (needle) { - var domain = [['res_model', '=', 'ir.ui.view'], '|', - ['mimetype', '=', false], ['mimetype', '=like', 'image/%']]; - if (needle && needle.length) { - domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]); - } - return ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'ir.attachment', - method: 'search_read', - args: [], - kwargs: { - domain: domain, - fields: ['name', 'website_url'], - order: 'id desc', - context: website.get_context() - } - }).then(this.proxy('fetched_existing')); - }, - fetched_existing: function (records) { - this.records = records; - this.display_attachments(); - }, - display_attachments: function () { - this.$('.help-block').empty(); - var per_screen = IMAGES_PER_ROW * IMAGES_ROWS; - - var from = this.page * per_screen; - var records = this.records; - - // Create rows of 3 records - var rows = _(records).chain() - .slice(from, from + per_screen) - .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); }) - .values() - .value(); - this.$('.existing-attachments').replaceWith( - core.qweb.render( - 'website.editor.dialog.image.existing.content', {rows: rows})); - this.parent.$('.pager') - .find('li.previous').toggleClass('disabled', (from === 0)).end() - .find('li.next').toggleClass('disabled', (from + per_screen >= records.length)); - - this.selected_existing(); - }, - select_existing: function (e) { - var $img = $(e.currentTarget); - this.push($img.attr('src'), $img.attr('alt'), $img.data('id')); - this.selected_existing(); - }, - selected_existing: function () { - var self = this; - this.$('.existing-attachment-cell.media_selected').removeClass("media_selected"); - var $select = this.$('.existing-attachment-cell img').filter(function () { - var $img = $(this); - var url = $img.attr("src"); - return !!_.select(self.images, function (v) { - if (v.url === url) { - if (!v.id) { - v.id = $img.data('id'); - v.alt = $img.attr('alt'); - } - return true; - } - }).length; - }); - $select.parent().addClass("media_selected"); - return $select; - }, - try_remove: function (e) { - var $help_block = this.$('.help-block').empty(); - var self = this; - var $a = $(e.target); - var id = parseInt($a.data('id'), 10); - var attachment = _.findWhere(this.records, {id: id}); - var $both = $a.parent().children(); - - $both.css({borderWidth: "5px", borderColor: "#f00"}); - - return ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'ir.attachment', - method: 'try_remove', - args: [], - kwargs: { - ids: [id], - context: website.get_context() - } - }).then(function (prevented) { - if (_.isEmpty(prevented)) { - self.records = _.without(self.records, attachment); - self.display_attachments(); - return; - } - $both.css({borderWidth: "", borderColor: ""}); - $help_block.replaceWith(core.qweb.render( - 'website.editor.dialog.image.existing.error', { - views: prevented[id] - } - )); - }); - }, -}); - - -var cacheCssSelectors = {}; -function getCssSelectors(filter) { - var css = []; - if (cacheCssSelectors[filter]) { - return cacheCssSelectors[filter]; - } - var sheets = document.styleSheets; - for(var i = 0; i < sheets.length; i++) { - try { - var rules = sheets[i].rules || sheets[i].cssRules; - } catch(e) { - continue; - } - if (rules) { - for(var r = 0; r < rules.length; r++) { - var selectorText = rules[r].selectorText; - if (selectorText) { - var match = selectorText.match(filter); - if (match) { - css.push([match[1], rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, '')]); - } - } - } - } - } - return cacheCssSelectors[filter] = css; -} - -function computeFonts() { - _.each(fontIcons, function (data) { - data.icons = _.map(getCssSelectors(data.parser), function (css) { - return css[0].slice(1, css[0].length).replace(/::?before$/, ''); - }); - }); -} - -/* list of font icons to load by editor. The icons are displayed in the media editor and - * identified like font and image (can be colored, spinned, resized with fa classes). - * To add font, push a new object {base, parser} - * - base: class who appear on all fonts (eg: fa fa-refresh) - * - parser: regular expression used to select all font in css style sheets - */ -var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}]; - - -/** - * FontIconsDialog widget. Lets users change a font awsome, suport all - * font awsome loaded in the css files. - */ -var FontIconsDialog = Widget.extend({ - template: 'website.editor.dialog.font-icons', - events : _.extend({}, Dialog.prototype.events, { - 'click .font-icons-icon': function (e) { - e.preventDefault(); - e.stopPropagation(); - - this.$('#fa-icon').val(e.target.getAttribute('data-id')); - $(".font-icons-icon").removeClass("font-icons-selected"); - $(event.target).addClass("font-icons-selected"); - }, - }), - - // extract list of font (like awsome) from the cheatsheet. - renderElement: function() { - this.iconsParser = fontIcons; - this.icons = _.flatten(_.map(fontIcons, function (data) { - return data.icons; - })); - this._super(); - }, - - init: function (parent, media) { - this._super(); - this.parent = parent; - this.media = media; - }, - start: function () { - return this._super().then(this.proxy('load_data')); - }, - search: function (needle) { - var iconsParser = this.iconsParser; - if (needle) { - var parser = []; - var fontIcon; - - for (var k=0; k<fontIcons.length; k++) { - fontIcon = fontIcons[k]; - var icons = _(fontIcon.icons).filter(function (icon) { - return icon.indexOf(needle) !== -1; - }); - if (icons.length) { - parser.push({ - base: fontIcon.base, - icons: icons - }); - } - } - iconsParser = parser; - } - this.$('div.font-icons-icons').html( - core.qweb.render('website.editor.dialog.font-icons.icons', {'iconsParser': iconsParser})); - }, - /** - * Removes existing FontAwesome classes on the bound element, and sets - * all the new ones if necessary. - */ - save: function () { - var self = this; - this.parent.trigger("save", this.media); - var icons = this.icons; - var style = this.media.attributes.style ? this.media.attributes.style.value : ''; - var classes = (this.media.className||"").split(/\s+/); - var non_fa_classes = _.reject(classes, function (cls) { - return self.getFont(cls); - }); - var final_classes = non_fa_classes.concat(this.get_fa_classes()); - if (this.media.tagName !== "SPAN") { - var media = document.createElement('span'); - $(this.media).replaceWith(media); - this.media = media; - style = style.replace(/\s*width:[^;]+/, ''); - } - $(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style); - }, - /** - * return the data font object (with base, parser and icons) or null - */ - getFont: function (classNames) { - if (!(classNames instanceof Array)) { - classNames = (classNames||"").split(/\s+/); - } - var fontIcon, className; - for (var i=0; i<classNames.length; i++) { - className = classNames[i]; - for (var k=0; k<fontIcons.length; k++) { - fontIcon = fontIcons[k]; - if (className === fontIcon.base || fontIcon.icons.indexOf(className) !== -1) { - return { - 'base': fontIcon.base, - 'parser': fontIcon.parser, - 'icons': fontIcon.icons, - 'font': className - }; - } - } - } - return null; - }, - - /** - * Looks up the various FontAwesome classes on the bound element and - * sets the corresponding template/form elements to the right state. - * If multiple classes of the same category are present on an element - * (e.g. fa-lg and fa-3x) the last one occurring will be selected, - * which may not match the visual look of the element. - */ - load_data: function () { - var classes = (this.media&&this.media.className||"").split(/\s+/); - for (var i = 0; i < classes.length; i++) { - var cls = classes[i]; - switch(cls) { - case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x': - // size classes - this.$('#fa-size').val(cls); - continue; - case 'fa-spin': - case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270': - case 'fa-flip-horizontal':case 'fa-rotate-vertical': - this.$('#fa-rotation').val(cls); - continue; - case 'fa-fw': - continue; - case 'fa-border': - this.$('#fa-border').prop('checked', true); - continue; - case '': continue; - default: - $(".font-icons-icon").removeClass("font-icons-selected").filter("."+cls).addClass("font-icons-selected"); - for (var k=0; k<this.icons.length; k++) { - if (this.icons.indexOf(cls) !== -1) { - this.$('#fa-icon').val(cls); - break; - } - } - } - } - }, - /** - * Serializes the dialog to an array of FontAwesome classes. Includes - * the base ``fa``. - */ - get_fa_classes: function () { - var font = this.getFont(this.$('#fa-icon').val()); - return [ - font ? font.base : 'fa', - font ? font.font : "", - this.$('#fa-size').val(), - this.$('#fa-rotation').val(), - this.$('#fa-border').prop('checked') ? 'fa-border' : '' - ]; - }, - clear: function () { - this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' '); - }, -}); - - -function createVideoNode(url) { - // video url patterns(youtube, instagram, vimeo, dailymotion, youku) - var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; - var ytMatch = url.match(ytRegExp); - - var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/; - var igMatch = url.match(igRegExp); - - var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/; - var vMatch = url.match(vRegExp); - - var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/; - var vimMatch = url.match(vimRegExp); - - var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/; - var dmMatch = url.match(dmRegExp); - - var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/; - var youkuMatch = url.match(youkuRegExp); - - var $video = $('<iframe>'); - if (ytMatch && ytMatch[1].length === 11) { - var youtubeId = ytMatch[1]; - $video = $('<iframe>') - .attr('src', '//www.youtube.com/embed/' + youtubeId) - .attr('width', '640').attr('height', '360'); - } else if (igMatch && igMatch[0].length) { - $video = $('<iframe>') - .attr('src', igMatch[0] + '/embed/') - .attr('width', '612').attr('height', '710') - .attr('scrolling', 'no') - .attr('allowtransparency', 'true'); - } else if (vMatch && vMatch[0].length) { - $video = $('<iframe>') - .attr('src', vMatch[0] + '/embed/simple') - .attr('width', '600').attr('height', '600') - .attr('class', 'vine-embed'); - } else if (vimMatch && vimMatch[3].length) { - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') - .attr('src', '//player.vimeo.com/video/' + vimMatch[3]) - .attr('width', '640').attr('height', '360'); - } else if (dmMatch && dmMatch[2].length) { - $video = $('<iframe>') - .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2]) - .attr('width', '640').attr('height', '360'); - } else if (youkuMatch && youkuMatch[1].length) { - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') - .attr('height', '498') - .attr('width', '510') - .attr('src', '//player.youku.com/embed/' + youkuMatch[1]); - } else { - // this is not a known video link. Now what, Cat? Now what? - } - - $video.attr('frameborder', 0); - - return $video; -} - -/** - * VideoDialog widget. Lets users change a video, support all summernote - * video, and embled iframe - */ -var VideoDialog = Widget.extend({ - template: 'website.editor.dialog.video', - events : _.extend({}, Dialog.prototype.events, { - 'click input#urlvideo ~ button': 'get_video', - 'click input#embedvideo ~ button': 'get_embed_video', - 'change input#urlvideo': 'change_input', - 'keyup input#urlvideo': 'change_input', - 'change input#embedvideo': 'change_input', - 'keyup input#embedvideo': 'change_input' - }), - init: function (parent, media) { - this._super(); - this.parent = parent; - this.media = media; - }, - start: function () { - this.$preview = this.$('.preview-container').detach(); - this.$iframe = this.$("iframe"); - var $media = $(this.media); - if ($media.hasClass("media_iframe_video")) { - var src = $media.data('src'); - this.$("input#urlvideo").val(src); - this.$("#autoplay").attr("checked", src.indexOf('autoplay=1') != -1); - this.get_video(); - } else { - this.add_class = "pull-left"; - } - return this._super(); - }, - change_input: function (e) { - var $input = $(e.target); - var $button = $input.parent().find("button"); - if ($input.val() === "") { - $button.addClass("btn-default").removeClass("btn-primary"); - } else { - $button.removeClass("btn-default").addClass("btn-primary"); - } - }, - get_embed_video: function (event) { - event.preventDefault(); - var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/); - if (embedvideo) { - this.$("input#urlvideo").val(embedvideo[1]); - this.get_video(event); - } - return false; - }, - get_video: function (event) { - if (event) event.preventDefault(); - var $video = createVideoNode(this.$("input#urlvideo").val()); - this.$iframe.replaceWith($video); - this.$iframe = $video; - return false; - }, - save: function () { - this.parent.trigger("save", this.media); - var video_id = this.$("#video_id").val(); - if (!video_id) { - this.$("button.btn-primary").click(); - video_id = this.$("#video_id").val(); - } - var video_type = this.$("#video_type").val(); - var $iframe = $( - '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+ - '<div class="css_editable_mode_display"> </div>'+ - '<div class="media_iframe_video_size" contentEditable="false"> </div>'+ - '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+ - '</div>'); - $(this.media).replaceWith($iframe); - this.media = $iframe[0]; - }, - clear: function () { - if (this.media.dataset.src) { - try { - delete this.media.dataset.src; - } catch(e) { - this.media.dataset.src = undefined; - } - } - - this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' '); - }, -}); - -var editor_bar = new EditorBar(); -function init_editor() { - var $body = $(document.body); - editor_bar.prependTo($body).then(function () { - if (location.search.indexOf("enable_editor") >= 0) { - editor_bar.edit(); - } - }); -}; - -return { - EditorBar: EditorBar, - editor_bar: editor_bar, - LinkDialog: LinkDialog, - Dialog: Dialog, - MediaDialog: MediaDialog, - getCssSelectors: getCssSelectors, -}; - -}); +});
\ No newline at end of file diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index a9f1f41d95c..728321b7e1f 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -3,10 +3,14 @@ odoo.define('website.website', function (require) { var ajax = require('web.ajax'); var core = require('web.core'); +var Widget = require('web.Widget'); var session = require('web.session'); +var base = require('web_editor.base'); var Tour = require('web.Tour'); +var qweb = core.qweb; var _t = core._t; +base.url_translations = '/website/translations'; /* --- Set the browser into the dom for css selectors --- */ var browser; @@ -18,66 +22,24 @@ else if ($.browser.mozilla) browser = "mozilla"; browser += ","+$.browser.version; if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase())) browser += ",mobile"; document.documentElement.setAttribute('data-browser', browser); -/* ---------------------------------------------------- */ - - -var translatable = !!$('html').data('translatable'); -var data = { - id: undefined, - session: undefined, -}; /* ---------------------------------------------------- Helpers - ---------------------------------------------------- */ -function get_context(dict) { + ---------------------------------------------------- */ + +var get_context = base.get_context; +base.get_context = base.get_context = function (dict) { var html = document.documentElement; return _.extend({ - lang: html.getAttribute('lang').replace('-', '_'), - website_id: html.getAttribute('data-website-id')|0 - }, dict); -} - -function parseQS(qs) { - var match, - params = {}, - pl = /\+/g, // Regex for replacing addition symbol with a space - search = /([^&=]+)=?([^&]*)/g; - - while ((match = search.exec(qs))) { - var name = decodeURIComponent(match[1].replace(pl, " ")); - var value = decodeURIComponent(match[2].replace(pl, " ")); - params[name] = value; - } - return params; -} - -var _parsedSearch; -function parseSearch() { - if (!_parsedSearch) { - _parsedSearch = parseQS(window.location.search.substring(1)); - } - return _parsedSearch; -} - -function parseHash() { - return parseQS(window.location.hash.substring(1)); -} - -function reload() { - location.hash = "scrollTop=" + window.document.body.scrollTop; - if (location.search.indexOf("enable_editor") > -1) { - window.location.href = window.location.href.replace(/enable_editor(=[^&]*)?/g, ''); - } else { - window.location.reload(); - } -} + 'website_id': html.getAttribute('data-website-id')|0 + }, get_context(dict), dict); +}; /* ---------------------------------------------------- Widgets ---------------------------------------------------- */ -function prompt(options, qweb) { +var prompt = function (options, _qweb) { /** * A bootstrapped version of prompt() albeit asynchronous * This was built to quickly prompt the user with a single field. @@ -111,8 +73,8 @@ function prompt(options, qweb) { text: options }; } - if (_.isUndefined(qweb)) { - qweb = 'website.prompt'; + if (_.isUndefined(_qweb)) { + _qweb = 'website.prompt'; } options = _.extend({ window_title: '', @@ -127,7 +89,7 @@ function prompt(options, qweb) { options.field_name = options.field_name || options[type]; var def = $.Deferred(); - var dialog = $(core.qweb.render(qweb, options)).appendTo("body"); + var dialog = $(qweb.render(_qweb, options)).appendTo("body"); options.$dialog = dialog; var field = dialog.find(options.field_type).first(); field.val(options['default']); // dict notation for IE<9 @@ -168,34 +130,34 @@ function prompt(options, qweb) { }); } return def; -} +}; -function error(data, url) { - var $error = $(core.qweb.render('website.error_dialog', { +var error = function(data, url) { + var $error = $(qweb.render('website.error_dialog', { 'title': data.data ? data.data.arguments[0] : "", 'message': data.data ? data.data.arguments[1] : data.statusText, 'backend_url': url })); $error.appendTo("body"); $error.modal('show'); -} +}; -function form(url, method, params) { - var htmlform = document.createElement('form'); - htmlform.setAttribute('action', url); - htmlform.setAttribute('method', method); +var form = function (url, method, params) { + var form = document.createElement('form'); + form.setAttribute('action', url); + form.setAttribute('method', method); _.each(params, function (v, k) { var param = document.createElement('input'); param.setAttribute('type', 'hidden'); param.setAttribute('name', k); param.setAttribute('value', v); - htmlform.appendChild(param); + form.appendChild(param); }); - document.body.appendChild(htmlform); - htmlform.submit(); -} + document.body.appendChild(form); + form.submit(); +}; -function init_kanban($kanban) { +var init_kanban = function ($kanban) { $('.js_kanban_col', $kanban).each(function () { var $col = $(this); var $pagination = $('.pagination', $col); @@ -223,7 +185,7 @@ function init_kanban($kanban) { } var page = +$a.attr("href").split(",").pop().split('-')[1]; - data.page = page; + data['page'] = page; $.post('/website/kanban', data, function (col) { $col.find("> .thumbnail").remove(); @@ -250,43 +212,9 @@ function init_kanban($kanban) { }); }); -} - -/* ---------------------------------------------------- - Async Ready and Template loading - ---------------------------------------------------- */ -var templates_def = $.Deferred().resolve(); -function add_template_file(template) { - var def = $.Deferred(); - templates_def = templates_def.then(function() { - core.qweb.add_template(template, function(err) { - if (err) { - def.reject(err); - } else { - def.resolve(); - } - }); - return def; - }); - return def; -} - -add_template_file('/website/static/src/xml/website.xml'); +}; -var dom_ready = $.Deferred(); -$(document).ready(function () { - dom_ready.resolve(); - // fix for ie - if($.fn.placeholder) $('input, textarea').placeholder(); - $(".oe_search_box").on('input', function() { - $(this).next('span').toggle(!!$(this).val()); - }); - $(".oe_search_clear").on('click', function() { - $(this).prev('input').val('').focus(); - $(".oe_search_button").trigger("click"); - }); - $(".oe_search_clear").toggle(!!$('.oe_search_box').val()); -}); +ajax.loadXML('/website/static/src/xml/website.xml', qweb); /** * Execute a function if the dom contains at least one element matched @@ -295,73 +223,38 @@ $(document).ready(function () { * @param {String} selector A jQuery selector used to match the element(s) * @param {Function} fn Callback to execute if at least one element has been matched */ -function if_dom_contains(selector, fn) { - dom_ready.then(function () { +var if_dom_contains = function(selector, fn) { + base.dom_ready.then(function () { var elems = $(selector); if (elems.length) { fn(elems); } }); -} +}; + +/** + * Cancel the auto run of Tour (test) and launch the tour after tob bar all bind events + */ + +Tour.autoRunning = false; +base.ready().then(function () { + data.topBar = new TopBar(); + data.topBar.attachTo($("#oe_main_menu_navbar")) + .then(function () { + setTimeout(Tour.running,0); + }); +}); -var all_ready = null; /** * Returns a deferred resolved when the templates are loaded * and the Widgets can be instanciated. */ -function ready() { - if (!all_ready) { - all_ready = dom_ready.then(function () { - return templates_def; - }).then(function () { - odoo.init(); - - // display button if they are at least one editable zone in the page (check the branding) - if ($('[data-oe-model]').size()) { - $("#oe_editzone").show(); - } - - if ($('html').data('website-id')) { - data.id = $('html').data('website-id'); - data.session = session; - - return ajax.jsonRpc('/website/translations', 'call', {'lang': get_context().lang}) - .then(function(trans) { - _t.database.set_bundle(trans);}); - } - }).then(function () { - var templates = core.qweb.templates; - var keys = _.keys(templates); - for (var i = 0; i < keys.length; i++){ - treat_node(templates[keys[i]]); - } - }).promise(); - } - return all_ready; -} - -function treat_node(node){ - if(node.nodeType === 3) { - if(node.nodeValue.match(/\S/)){ - var text_value = $.trim(node.nodeValue); - var spaces = node.nodeValue.split(text_value); - node.nodeValue = spaces[0] + _t(text_value) + spaces[1]; - } - } - else if(node.nodeType === 1 && node.hasChildNodes()) { - _.each(node.childNodes, function(subnode) {treat_node(subnode);}); - } -}; - - -function inject_tour() { - // if a tour is active inject tour js -} - -dom_ready.then(function () { + +base.dom_ready.then(function () { /* ----- PUBLISHING STUFF ---- */ $(document).on('click', '.js_publish_management .js_publish_btn', function () { var $data = $(this).parents(".js_publish_management:first"); + var self=this; ajax.jsonRpc($data.data('controller') || '/website/publish', 'call', {'id': +$data.data('id'), 'object': $data.data('object')}) .then(function (result) { $data.toggleClass("css_unpublished css_published"); @@ -389,7 +282,7 @@ dom_ready.then(function () { // retrieve the hash before the redirect var redirect = { lang: self.data('lang'), - url: self.attr('href'), + url: encodeURIComponent(self.attr('href').replace(/[&?]edit_translations[^&?]+/, '')), hash: location.hash }; location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect); @@ -412,48 +305,37 @@ dom_ready.then(function () { window.document.body.scrollTop = +location.hash.match(/scrollTop=([0-9]+)/)[1]; } },0); +}); - /* ----- WEBSITE TOP BAR ---- */ - var $collapse = $('#oe_applications ul.dropdown-menu').clone() - .attr("id", "oe_applications_collapse") - .attr("class", "nav navbar-nav navbar-left navbar-collapse collapse"); - $('#oe_applications').before($collapse); - $collapse.wrap('<div class="visible-xs"/>'); - $('[data-target="#oe_applications"]').attr("data-target", "#oe_applications_collapse"); - }); - Tour.autoRunning = false; - ready().then(function () { - setTimeout(Tour.running,0); +/** + * Object who contains all method and bind for the top bar, the template is create server side. + */ + +var TopBar = Widget.extend({ + start: function () { + var $collapse = this.$('#oe_applications ul.dropdown-menu').clone() + .attr("id", "oe_applications_collapse") + .attr("class", "nav navbar-nav navbar-left navbar-collapse collapse"); + this.$('#oe_applications').before($collapse); + $collapse.wrap('<div class="visible-xs"/>'); + this.$('[data-target="#oe_applications"]').attr("data-target", "#oe_applications_collapse"); + + return this._super(); + } }); -return { - translatable: translatable, - get_context: get_context, - parseQS: parseQS, - parseSearch: parseSearch, - parseHash: parseHash, - reload: reload, + +var data = { prompt: prompt, - error: error, form: form, - init_kanban: init_kanban, - add_template_file: add_template_file, - dom_ready: dom_ready, if_dom_contains: if_dom_contains, - ready: ready, - inject_tour: inject_tour, - - data: data, + TopBar: TopBar, }; +return data; }); -odoo.define('web.session', function (require) { -"use strict"; - -var Session = require('web.Session'); - -return new Session(null, null, {modules: ['website']}); - +$(function () { + odoo.init(); }); diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index 0b1c7befaa1..fa65484aff5 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -25,42 +25,6 @@ function load_called_template () { } } -function start_animation(editable_mode, $target) { - for (var k in animationRegistry) { - var Animation = animationRegistry[k]; - var selector = ""; - if (Animation.prototype.selector) { - if (selector !== "") selector += ", "; - selector += Animation.prototype.selector; - } - if ($target) { - if ($target.is(selector)) selector = $target; - else continue; - } - - $(selector).each(function () { - var $snipped_id = $(this); - if ( !$snipped_id.parents("#oe_snippets").length && - !$snipped_id.parent("body").length && - !$snipped_id.data("snippet-view")) { - readyAnimation.push($snipped_id); - $snipped_id.data("snippet-view", new Animation($snipped_id, editable_mode)); - } else if ($snipped_id.data("snippet-view")) { - $snipped_id.data("snippet-view").start(editable_mode); - } - }); - } -} - -function stop_animation() { - $(readyAnimation).each(function() { - var $snipped_id = $(this); - if ($snipped_id.data("snippet-view")) { - $snipped_id.data("snippet-view").stop(); - } - }); -} - $(document).ready(function () { load_called_template(); // if asset is placed into head, move this call into $(document).ready @@ -68,35 +32,9 @@ $(document).ready(function () { // load gallery modal template website.add_template_file('/website/static/src/xml/website.gallery.xml'); } - - start_animation(); }); - -var Animation = core.Class.extend({ - selector: false, - $: function () { - return this.$el.find.apply(this.$el, arguments); - }, - init: function (dom, editable_mode) { - this.$el = this.$target = $(dom); - this.start(editable_mode); - }, - /* - * start - * This method is called after init - */ - start: function () { - }, - /* - * stop - * This method is called to stop the animation (e.g.: when rte is launch) - */ - stop: function () { - }, -}); - animationRegistry.slider = Animation.extend({ selector: ".carousel", start: function () { @@ -272,7 +210,6 @@ animationRegistry.gallery = Animation.extend({ } } // click_handler }); - animationRegistry.gallery_slider = Animation.extend({ selector: ".o_slideshow", start: function() { @@ -321,13 +258,4 @@ animationRegistry.gallery_slider = Animation.extend({ } }); -return { - Animation: Animation, - readyAnimation: readyAnimation, - start_animation: start_animation, - stop_animation: stop_animation, - registry: animationRegistry, -}; - }); - diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js index 3be81da11ce..d9e41badd8e 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -1,1066 +1,19 @@ odoo.define('website.snippets.editor', function (require) { 'use strict'; -var ajax = require('web.ajax'); -var Class = require('web.Class'); -var core = require('web.core'); -var Widget = require('web.Widget'); -var editor = require('website.editor'); +var widget = require('web_editor.widget'); +var animation = require('web_editor.snippets.animation'); +var options = require('web_editor.snippets.options'); +var snippet_editor = require('web_editor.snippet.editor'); -var animation = require('website.snippets.animation'); -var website = require('website.website'); - -var dummy = function () {}; - -var options = {}; - -var templateOptions = []; -var globalSelector = { - closest: function () { return $(); }, - all: function () { return $(); }, - is: function () { return false; }, -}; -var selector = []; - -website.add_template_file('/website/static/src/xml/website.snippets.xml'); - -editor.EditorBar.include({ - start: function () { - var self = this; - $(document).on('click', '.o_editable', 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; - animation.stop_animation(); - this.on('rte:ready', this, function () { - var $editable = $(".o_editable"); - window.snippets = this.snippets = new BuildingBlock(this, $editable); - this.snippets.insertAfter(this.$el); - animation.start_animation(true); - $editable.find("*").off('mousedown mouseup click'); - this.snippets.on("snippets:ready", this, function () { - self.trigger("snippets:ready"); - }); - }); - - return this._super.apply(this, arguments); - }, - save: function () { - this.snippets.clean_for_save(); - this._super(); - }, -}); - -/* ----- SNIPPET SELECTOR ---- */ - -$.extend($.expr[':'],{ - o_editable: function(node,i,m){ - while (node) { - if (node.className) { - if (node.className.indexOf('o_not_editable')!==-1 ) { - return false; - } - if (node.className.indexOf('o_editable')!==-1 ) { - return true; - } - } - node = node.parentNode; - } - return false; - }, - hasData: function(node,i,m){ - return !!_.toArray(node.dataset).length; - }, - data: function(node,i,m){ - return $(node).data(m[3]); - } -}); - - -var BuildingBlock = Widget.extend({ - template: 'website.snippets', - activeSnippets: [], - init: function (parent, $editable) { - this.parent = parent; - this.$editable = $editable; - - this._super.apply(this, arguments); - if(!$('#oe_manipulators').length){ - $("<div id='oe_manipulators'></div>").appendTo('body'); - } - this.$active_snipped_id = false; - this.snippets = []; - }, - start: function() { - var self = this; - this.$snippet = $("#oe_snippets"); - - this.$el - .on("mouseenter", function () { self.show(); }) - .on("mouseleave", function (event) { if (event.clientX>0 && event.clientY>0) self.hide(); }); - - $(window).resize(function () { - setTimeout('$(document).click()',0); - }); - - this.fetch_snippet_templates(); - this.bind_snippet_click_editor(); - - $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) { - e.preventDefault(); - }); - - var _isNotBreakable = $.summernote.core.dom.isNotBreakable; - $.summernote.core.dom.isNotBreakable = function (node, sc, so, ec, eo) { - return _isNotBreakable(node, sc, so, ec, eo) || $(node).is('div') || globalSelector.is($(node)); - }; - - $(window).on('resize', function () { - if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { - self.cover_target(self.$active_snipped_id.data("snippet-editor").$overlay, self.$active_snipped_id); - } - }); - - $(document).on('mousemove', function () { - if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { - self.$active_snipped_id.data("snippet-editor").$overlay.removeClass('o_keypress'); - } - }); - $(document).on('keydown', function (event) { - if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) { - self.$active_snipped_id.data("snippet-editor").$overlay.addClass('o_keypress'); - } - if ((event.metaKey || (event.ctrlKey && !event.altKey)) && event.shiftKey && event.keyCode >= 48 && event.keyCode <= 57) { - self.$snippet.find('.scroll:first > ul li:eq('+(event.keyCode-49)+') a').trigger("click"); - self.show(); - event.preventDefault(); - } - }); - }, +snippet_editor.Class.include({ _get_snippet_url: function () { return '/website/snippets'; - }, - _add_check_selector : function (selector, no_check) { - var self = this; - selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)"; - - if (no_check) { - return { - closest: function ($from, parentNode) { - return $from.closest(selector, parentNode); - }, - all: function ($from) { - return $from ? $from.find(selector) : $(selector); - }, - is: function ($from) { - return $from.is(selector); - } - }; - } else { - selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable"; - return { - closest: function ($from, parentNode) { - var parents = self.$editable.get(); - return $from.closest(selector, parentNode).filter(function () { - var node = this; - while (node.parentNode) { - if (parents.indexOf(node)!==-1) { - return true; - } - node = node.parentNode; - } - return false; - }); - }, - all: function ($from) { - return $from ? $from.find(selector) : self.$editable.parent().find(selector); - }, - is: function ($from) { - return $from.is(selector); - } - }; - } - }, - - fetch_snippet_templates: function () { - var self = this; - var url = this._get_snippet_url(); - if (!url || !url.length) { - this.$snippet.detach(); - return; - } - return ajax.jsonRpc(url, 'call', {}).then(function (html) { - self.compute_snippet_templates(html); - self.trigger("snippets:ready"); - }); - }, - compute_snippet_templates: function (html) { - var self = this; - var $html = $(html); - var $left_bar = this.$el.find("#o_left_bar"); - var $ul = $html.siblings("ul"); - var $scroll = $html.siblings("#o_scroll"); - - if (!$scroll.length) { - throw new Error("Wrong snippets xml definition"); - } - - // t-snippet - $html.find('[data-oe-type="snippet"][data-oe-name]').each(function () { - $('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name')); - }); - // end - - 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() - }; - templateOptions.push(option); - selector.push(option.selector); - }); - $styles.addClass("hidden"); - _.extend(globalSelector, { - closest: function ($from) { - var $temp; - var $target; - var len = selector.length; - for (var i = 0; i<len; i++) { - $temp = selector[i].closest($from, $target && $target[0]); - if (!$target || $temp.length) { - $target = $temp; - } - } - return $target; - }, - all: function ($from) { - var $target; - var len = selector.length; - for (var i = 0; i<len; i++) { - if (!$target) $target = selector[i].all($from); - else $target = $target.add(selector[i].all($from)); - } - return $target; - }, - is: function ($from) { - var len = selector.length; - for (var i = 0; i<len; i++) { - if (selector[i].is($from)) { - return true; - } - } - return false; - }, - }); - - // oe_snippet_body - self.$snippets = $scroll.find(".o_panel_body").children() - .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'); - } - }); - - // select all default text to edit (if snippet default text) - self.$snippets.find('.oe_snippet_body, .oe_snippet_body *') - .contents() - .filter(function() { - return this.nodeType === 3 && this.textContent.match(/\S/); - }).parent().addClass("o_default_snippet_text"); - $(document).on("mouseup", ".o_default_snippet_text", function (event) { - $(event.target).selectContent(); - }); - $(document).on("keyup", function () { - var r = $.summernote.core.range.create(); - $(r && r.sc).closest(".o_default_snippet_text").removeClass("o_default_snippet_text"); - }); - // end - - // 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 - - $html.find('.o_not_editable').attr("contentEditable", false); - - $left_bar.append($html); - - // animate for list of snippet blocks - $left_bar.on('click', '.scroll-link', function (event) { - event.preventDefault(); - var targetOffset = $($(this).attr("href")).position().top - $ul.outerHeight() + $scroll[0].scrollTop; - $scroll.animate({'scrollTop': targetOffset}, 750); - }); - $scroll.on('scroll', function () { - var middle = $scroll.height()/4; - var $li = $ul.find("a").parent().removeClass('active'); - var last; - for (var k=0; k<$li.length; k++) { - var li = $($li[k]); - if (!li.data('target')) { - li.data('target', $($("a", li).attr("href"))); - } - if (li.data('target').position().top > middle) { - break; - } - last = $li[k]; - } - $(last).addClass("active"); - }); - // end - - // display scrollbar - $(window).on('resize', function () { - $scroll.css("overflow", ""); - var height = $left_bar.height() - $ul.outerHeight(); - $scroll.css("height", height); - var $last = $scroll.children(":visible").last().children(".o_panel_body"); - $last.css({'min-height': (height-$last.prev().outerHeight())+'px'}); - if ($scroll[0].scrollHeight + $ul[0].scrollHeight > document.body.clientHeight) { - $scroll.css("overflow", "auto").css("width", "226px"); - } else { - $scroll.css("width", ""); - } - }).trigger('resize'); - // end - - self.make_snippet_draggable(self.$snippets); - }, - - cover_target: function ($el, $target){ - if($el.data('not-cover_target')) return; - var pos = $target.offset(); - var mt = parseInt($target.css("margin-top") || 0); - var mb = parseInt($target.css("margin-bottom") || 0); - var width = $target.outerWidth(); - var bigger = pos.left+width > $("body").outerWidth()-8; - $el.css({ - 'width': width, - 'top': pos.top - mt - 5, - 'left': pos.left-1 - }); - $el.find(".oe_handle.e,.oe_handle.w").css({'height': $target.outerHeight() + mt + mb+1}); - if (bigger) { - $el.find(".oe_handle.e").css({right: 0, margin: 0}) - .find("div").css({right: 0, left: 'auto'}); - } else { - $el.find(".oe_handle.e").css({right: "", margin: ""}) - .find("div").css({right: "", left: ""}); - } - $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': width-2}); - }, - - show_blocks: function () { - var cache = {}; - this.$snippet.find(".tab-pane").each(function () { - var catcheck = false; - var $category = $(this); - $category.find(".oe_snippet_body").each(function () { - var $snippet = $(this); - - var check = false; - - for (var k in templateOptions) { - var option = templateOptions[k]; - if ($snippet.is(option.base_selector)) { - - cache[k] = cache[k] || { - 'drop-near': option['drop-near'] ? option['drop-near'].all() : [], - 'drop-in': option['drop-in'] ? option['drop-in'].all() : [] - }; - - if (cache[k]['drop-near'].length || cache[k]['drop-in'].length) { - catcheck = true; - check = true; - break; - } - } - } - - if (check) { - $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(catcheck); - }); - }, - show: function () { - this.make_active(false); - this.$el.addClass("o_open"); - this.show_blocks(); - }, - hide: function () { - this.$el.removeClass("o_open"); - }, - bind_snippet_click_editor: function () { - var self = this; - var snipped_event_flag; - $(document).on('click', '*', function (event) { - var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target) || event.target); - if (self.editor_busy || snipped_event_flag===srcElement || !srcElement) { - return; - } - snipped_event_flag = srcElement; - - setTimeout(function () {snipped_event_flag = false;}, 0); - var $target = $(srcElement); - - if ($target.closest(".oe_overlay, .note-popover").length) { - return; - } - - if (!globalSelector.is($target)) { - $target = globalSelector.closest($target); - } - - 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 template = templateOptions; - for (var k in template) { - var Option = options[template[k]['option']]; - if (Option && Option.prototype.clean_for_save !== dummy) { - template[k].selector.all().each(function () { - new Option(self, null, $(this), k).clean_for_save(); - }); - } - } - this.$editable.find("*[contentEditable], *[attributeEditable]") - .removeAttr('contentEditable') - .removeProp('contentEditable') - .removeAttr('attributeEditable') - .removeProp('attributeEditable'); - }, - 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 SnippetEditor(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; - - $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_siblings = $(); - var $selector_children = $(); - var temp = templateOptions; - for (var k in temp) { - if ($base_body.is(temp[k].base_selector)) { - if (temp[k]['drop-near']) { - if (!$selector_siblings) $selector_siblings = temp[k]['drop-near'].all(); - else $selector_siblings = $selector_siblings.add(temp[k]['drop-near'].all()); - } - if (temp[k]['drop-in']) { - if (!$selector_children) $selector_children = temp[k]['drop-in'].all(); - else $selector_children = $selector_children.add(temp[k]['drop-in'].all()); - } - } - } - - $toInsert = $base_body.clone(); - - 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($selector_siblings, $selector_children); - - $('.oe_drop_zone').droppable({ - over: function(){ - dropped = true; - $(this).first().after($toInsert); - }, - out: function(){ - var prev = $toInsert.prev(); - if(this === prev[0]){ - dropped = false; - $toInsert.detach(); - } - } - }); - }, - stop: function(ev, ui){ - $toInsert.removeClass('oe_snippet_body'); - - if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3) { - var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first(); - if (el.length) { - el.after($toInsert); - dropped = true; - } - } - - self.$editable.find('.oe_drop_zone').droppable('destroy').remove(); - - if (dropped) { - - var prev = $toInsert.first()[0].previousSibling; - var next = $toInsert.last()[0].nextSibling; - var rte = self.parent.rte; - - if (prev) { - $toInsert.detach(); - rte.historyRecordUndo($(prev)); - $toInsert.insertAfter(prev); - } else if (next) { - $toInsert.detach(); - rte.historyRecordUndo($(next)); - $toInsert.insertBefore(next); - } else { - var $parent = $toInsert.parent(); - $toInsert.detach(); - rte.historyRecordUndo($parent); - $parent.prepend($toInsert); - } - - var $target = false; - $target = $toInsert; - - setTimeout(function () { - self.$snippet.trigger('snippet-dropped', $target); - - animation.start_animation(true, $target); - - // 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 templateOptions) { - templateOptions[k].selector.all($target).each(function () { - var $snippet = $(this); - self.create_overlay($snippet); - if ($snippet.data("snippet-editor")) { - $snippet.data("snippet-editor").drop_and_build_snippet(); - } - }); - } - $target.closest(".o_editable").trigger("content_changed"); - // 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_siblings, $selector_children){ - var self = this; - var zone_template = $("<div class='oe_drop_zone oe_insert'></div>"); - - if ($selector_children) { - $selector_children.each(function (){ - var $zone = $(this); - var css = window.getComputedStyle(this); - var float = css.float || css.cssFloat; - var $drop = zone_template.clone(); - - $zone.append($drop); - var node = $drop[0].previousSibling; - var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); - if (test) { - $drop.addClass("oe_vertical oe_vertical_text").css({ - 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), - 'float': 'none', - 'display': 'inline-block' - }); - } else if (float === "left" || float === "right") { - $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30)); - } - - $drop = $drop.clone(); - - $zone.prepend($drop); - node = $drop[0].nextSibling; - test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); - if (test) { - $drop.addClass("oe_vertical oe_vertical_text").css({ - 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), - 'float': 'none', - 'display': 'inline-block' - }); - } else if (float === "left" || float === "right") { - $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30)); - } - if (test) { - $drop.css({'float': 'none', 'display': 'inline-block'}); - } - }); - - // add children near drop zone - $selector_siblings = $(_.uniq(($selector_siblings || $()).add($selector_children.children()).get())); - } - - if ($selector_siblings) { - $selector_siblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function (){ - var $zone = $(this); - var $drop; - var css = window.getComputedStyle(this); - var float = css.float || css.cssFloat; - - if($zone.prev('.oe_drop_zone:visible').length === 0){ - $drop = zone_template.clone(); - if (float === "left" || float === "right") { - $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30)); - } - $zone.before($drop); - } - if($zone.next('.oe_drop_zone:visible').length === 0){ - $drop = zone_template.clone(); - if (float === "left" || float === "right") { - $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30)); - } - $zone.after($drop); - } - }); - } - - var count; - do { - count = 0; - $zones = self.$editable.find('.oe_drop_zone > .oe_drop_zone').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.$editable.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 (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) { - 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($targets){ - 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 = $(core.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); - - var timer; - $target.closest('.o_editable').on("content_changed", function () { - if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) { - clearTimeout(timer); - timer = setTimeout(function () { - self.cover_target($target.data('overlay'), $target); - },50); - } - }); - - 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; } }); - -var Option = Class.extend({ - // initialisation (don't overwrite) - init: function (BuildingBlock, editor, $target, option_id) { - this.BuildingBlock = BuildingBlock; - this.editor = editor; - this.$target = $target; - var option = 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(); - }, - - // helper for this.$target.find - $: function (selector) { - return this.$target(selector); - }, - - _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.reset, this)); - - this.$el - .off('mouseleave', "ul") - .on('mouseleave', "ul", _.bind(this.reset, this)); - - this.reset_methods = []; - }, - - /** - * 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.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"); - }, - - start: function () { - this.set_active(); - this.$target.on('snippet-option-reset', _.bind(this.set_active, this)); - this._bind_li_menu(); - }, - - on_focus : function () { - this._bind_li_menu(); - }, - - on_blur : function () { - }, - - on_clone: function () { - }, - - on_move: function () { - }, - - on_remove: function () { - }, - - drop_and_build_snippet: function () { - }, - - reset: function () { - 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.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 (data-only can be used) - select: function (type, $li) { - var self = this; - var $methods = []; - var el = $li[0]; - - if ($li.data('only') && type !== $li.data('only')) { - return; - } - - if (type==="click") { - this.reset(); - this.BuildingBlock.parent.rte.historyRecordUndo(this.$target); - } - - function filter (k) { return k !== 'oeId' && k !== 'oeModel' && k !== 'oeField' && k !== 'oeXpath' && k !== 'oeSourceId' && k !== 'only';} - 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.error("'"+self.option+"' snippet have not method '"+k+"'"); - } - } - }); - }, - - // default method for snippet - toggle_class: function (type, value) { - 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) { - 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 -}); - - var preventParentEmpty = { hide_remove_button: function() { this.$overlay.find('.oe_snippet_remove, .oe_snippet_move').toggleClass("hidden", !this.$target.siblings().length); @@ -1080,119 +33,7 @@ var preventParentEmpty = { }; -options.background = Option.extend({ - start: function ($change_target) { - this.$bg = $change_target || this.$target; - this._super(); - var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, ""); - if (this.$bg.hasClass('oe_custom_bg')) { - this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src); - } - }, - background: function(type, value) { - if (value && value.length) { - this.$bg.attr("style", 'background-image: url(' + value + ')' + (this.$bg.attr("style") || '').replace(/background-image:[^;]+/, '') ); - this.$bg.addClass("oe_img_bg"); - } else { - this.$bg.css("background-image", ""); - this.$bg.removeClass("oe_img_bg").removeClass("oe_custom_bg"); - } - }, - 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.$bg); - - var _editor = new editor.MediaDialog(null, $image[0]); - _editor.appendTo(document.body); - _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); - - _editor.on('saved', self, function () { - var value = $image.attr("src"); - $image.remove(); - self.$el.find('li[data-choose_image]').data("background", value).attr("data-background", value); - self.background(type, value,$li); - self.$bg.addClass('oe_custom_bg'); - self.$bg.trigger("snippet-option-change", [self]); - self.set_active(); - }); - _editor.on('cancel', self, function () { - $image.remove(); - }); - }, - set_active: function () { - var src = this.$bg.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, ""); - this._super(); - - 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[data-background].active').size()) { - this.$el.find('li[data-background=""]:not([data-choose_image])').addClass("active"); - } else { - this.$el.find('li[data-background=""]:not([data-choose_image])').removeClass("active"); - } - } -}); - -options.colorpicker = Option.extend({ - start: function () { - var self = this; - var res = this._super(); - - if (!this.$el.find('.colorpicker').length) { - this.$el.find('li').append( core.qweb.render('website.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(".colorpicker button").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(".colorpicker td"); - var $colors = this.$el.find(".colorpicker button"); - $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"); - }); - } -}); - -options.slider = Option.extend({ +options.registry.slider = options.Class.extend({ drop_and_build_snippet: function() { this.id = "myCarousel" + new Date().getTime(); this.$target.attr("id", this.id); @@ -1201,7 +42,7 @@ options.slider = Option.extend({ this.rebind_event(); }, on_clone: function ($clone) { - this.id = "myCarousel" + new Date().getTime(); + var id = "myCarousel" + new Date().getTime(); $clone.attr("id", id); $clone.find("[data-slide]").attr("href", "#" + id); $clone.find("[data-slide-to]").attr("data-target", "#" + id); @@ -1220,6 +61,7 @@ options.slider = Option.extend({ .first().addClass("active"); }, start : function () { + var self = this; this._super(); this.$target.carousel({interval: false}); this.id = this.$target.attr("id"); @@ -1228,7 +70,7 @@ options.slider = Option.extend({ this.$target.carousel('pause'); this.rebind_event(); }, - add_slide: function (type) { + add_slide: function (type, value) { if(type !== "click") return; var self = this; @@ -1239,7 +81,7 @@ options.slider = Option.extend({ this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>'); // clone the best candidate from template to use new features - var $snippets = this.BuildingBlock.$snippets.find('.oe_snippet_body.carousel'); + var $snippets = this.buildingBlock.$snippets.find('.oe_snippet_body.carousel'); var point = 0; var selection; var className = _.compact(this.$target.attr("class").split(" ")); @@ -1260,13 +102,14 @@ options.slider = Option.extend({ },0); return $clone; }, - remove_slide: function (type) { + 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(); @@ -1299,7 +142,7 @@ options.slider = Option.extend({ }, }); -options.carousel = options.slider.extend({ +options.registry.carousel = options.registry.slider.extend({ getSize: function () { this.grid = this._super(); this.grid.size = 8; @@ -1317,6 +160,11 @@ options.carousel = options.slider.extend({ 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.$bg = self.$target.find(".item.active"); @@ -1356,7 +204,7 @@ options.carousel = options.slider.extend({ }, }); -options.marginAndResize = Option.extend({ +options.registry.marginAndResize = options.Class.extend({ start: function () { var self = this; this._super(); @@ -1419,7 +267,7 @@ options.marginAndResize = Option.extend({ var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g'); } - self.BuildingBlock.editor_busy = true; + self.buildingBlock.editor_busy = true; var cursor = $handle.css("cursor")+'-important'; var $body = $(document.body); @@ -1434,7 +282,7 @@ options.marginAndResize = Option.extend({ 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); + self.buildingBlock.cover_target(self.$overlay, self.$target); return; } var dd = event['page'+XY] - xy + resize[1][begin]; @@ -1457,7 +305,7 @@ options.marginAndResize = Option.extend({ if (change) { self.on_resize(compass, beginClass, current); - self.BuildingBlock.cover_target(self.$overlay, self.$target); + self.buildingBlock.cover_target(self.$overlay, self.$target); } }; @@ -1466,19 +314,19 @@ options.marginAndResize = Option.extend({ $body.unbind('mouseup', body_mouseup); $body.removeClass(cursor); setTimeout(function () { - self.BuildingBlock.editor_busy = false; + self.buildingBlock.editor_busy = false; + self.$target.closest(".o_editable").trigger("content_changed"); },0); self.$target.removeClass("resize_editor_busy"); - self.$target.closest(".o_editable").trigger("content_changed"); }; $body.mousemove(body_mousemove); $body.mouseup(body_mouseup); }); - this.$overlay.find(".oe_handle.size .auto_size").on('click', function (){ + 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); - self.$target.closest(".o_editable").trigger("content_changed"); + self.buildingBlock.cover_target(self.$overlay, self.$target); + self.$target.closest(".o_editable").trigger("content_changed"); return false; }); }, @@ -1518,7 +366,7 @@ options.marginAndResize = Option.extend({ 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' @@ -1530,7 +378,7 @@ options.marginAndResize = Option.extend({ } }); -options["margin-y"] = options.marginAndResize.extend({ +options.registry["margin-y"] = options.registry.marginAndResize.extend({ getSize: function () { this.grid = this._super(); var grid = [0,4,8,16,32,48,64,92,128]; @@ -1545,7 +393,7 @@ options["margin-y"] = options.marginAndResize.extend({ }, }); -options["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend({ +options.registry["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend({ getSize: function () { this.grid = this._super(); var width = this.$target.parents(".row:first").first().outerWidth(); @@ -1553,7 +401,7 @@ options["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend( 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;})]; - 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]; + 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; @@ -1616,7 +464,7 @@ options["margin-x"] = options.marginAndResize.extend(preventParentEmpty).extend( }, }); -options.resize = options.marginAndResize.extend({ +options.registry.resize = options.registry.marginAndResize.extend({ getSize: function () { this.grid = this._super(); this.grid.size = 8; @@ -1624,7 +472,7 @@ options.resize = options.marginAndResize.extend({ }, }); -options.parallax = Option.extend({ +options.registry.parallax = options.Class.extend({ getSize: function () { this.grid = this._super(); this.grid.size = 8; @@ -1654,6 +502,7 @@ options.parallax = Option.extend({ 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"); }, @@ -1665,7 +514,7 @@ options.parallax = Option.extend({ } }); -options.transform = Option.extend({ +options.registry.transform = options.Class.extend({ start: function () { var self = this; this._super(); @@ -1703,12 +552,12 @@ options.transform = Option.extend({ } }); }, - style: function (type) { + 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) { + clear_style: function (type, value) { if (type !== 'click') return; this.$target.removeClass("fa-spin").attr("style", ""); this.resetTransfo(); @@ -1775,42 +624,16 @@ options.transform = Option.extend({ } }); -options.media = Option.extend({ +options.registry.ul = options.Class.extend({ start: function () { - this._super(); - animation.start_animation(true, this.$target); - }, - edition: function (type) { - if(type !== "click") return; var self = this; - var dialog = new editor.MediaDialog(this.$target.closest('.o_editable'), this.$target[0]); - dialog.appendTo(document.body); - dialog.on('saved', this, function (item) { - self.editor.on_blur(); - self.BuildingBlock.make_active(false); - if (self.$target.parent().data("oe-field") !== "image") { - setTimeout(function () { - self.BuildingBlock.make_active($(item)); - },0); - } - }); - }, - on_focus : function () { - var self = this; - var $parent = this.$target.parent(); - - if ($parent.data("oe-field") === "image" && $parent.hasClass('o_editable')) { - this.$overlay.addClass("hidden"); - self.edition('click', null); - self.BuildingBlock.make_active(false); - } - } -}); - -options.ul = Option.extend({ - start: function () { this._super(); this.$target.data("snippet-view", new animation.registry.ul(this.$target, true)); + this.$target.on('mouseup', '.o_ul_toggle_self, .o_ul_toggle_next', function () { + setTimeout(function () { + self.buildingBlock.cover_target(self.$overlay, self.$target); + },0); + }); }, reset_ul: function () { this.$target.find('.o_ul_toggle_self, .o_ul_toggle_next').remove(); @@ -1851,12 +674,12 @@ options.ul = Option.extend({ }); -options.collapse = Option.extend(preventParentEmpty).extend({ +options.registry.collapse = Option.extend(preventParentEmpty).extend({ start: function () { var self = this; this._super(); this.$target.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () { - self.BuildingBlock.cover_target(self.$overlay, self.$target); + self.buildingBlock.cover_target(self.$overlay, self.$target); }); }, create_ids: function ($target) { @@ -1911,403 +734,5 @@ options.collapse = Option.extend(preventParentEmpty).extend({ }); -var SnippetEditor = 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"); - - var $selector_siblings; - for (var i=0; i<self.selector_siblings.length; i++) { - if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all(); - else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all()); - } - var $selector_children; - for (i=0; i<self.selector_children.length; i++) { - if (!$selector_children) $selector_children = self.selector_children[i].all(); - else $selector_children = $selector_children.add(self.selector_children[i].all()); - } - - self.BuildingBlock.activate_insertion_zones($selector_siblings, $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; - var $dropzone = this.$target.prev(); - var prev = $dropzone.length && $dropzone[0].previousSibling; - var next = this.$target.last()[0].nextSibling; - var $parent = this.$target.parent(); - - $(".oe_drop_clone").after(this.$target); - - this.$overlay.removeClass("hidden"); - $("body").removeClass('move-important'); - $('.oe_drop_zone').droppable('destroy').remove(); - $(".oe_drop_clone, .oe_drop_to_remove").remove(); - - if (this.dropped) { - this.BuildingBlock.parent.rte.historyRecordUndo(this.$target); - - if (prev) { - this.$target.insertAfter(prev); - } else if (next) { - this.$target.insertBefore(next); - } else { - $parent.prepend(this.$target); - } - - for (var i in this.styles){ - this.styles[i].on_move(); - } - } - - 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(templateOptions, function (val, option_id) { - if (!val.selector.is(self.$target)) { - 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 = options[option] || Option; - var editor = self.styles[option] = new Editor(self.BuildingBlock, self, self.$target, option_id); - $ul.append(editor.$el.addClass("snippet-option-" + option)); - editor.start(); - }); - - if (!this.selector_siblings.length && !this.selector_children.length) { - 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 = globalSelector.closest(this.$target.parent()); - 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 () { - if (!this.$target.parent().is(':o_editable')) { - this.$overlay.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove(); - } else { - 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 (event) { - event.preventDefault(); - var $clone = this.$target.clone(false); - - this.BuildingBlock.parent.rte.historyRecordUndo(this.$target); - - this.$target.after($clone); - for (var i in this.styles){ - this.styles[i].on_clone($clone); - } - this.BuildingBlock.create_overlay(this.$target); - return false; - }, - - on_remove: function (event) { - event.preventDefault(); - this.on_blur(); - - this.BuildingBlock.parent.rte.historyRecordUndo(this.$target); - - 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 node = this.$target.parent()[0]; - - this.$target.remove(); - this.$overlay.remove(); - - if (node && node.firstChild) { - $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1); - if (!node.firstChild.tagName && node.firstChild.textContent === " ") { - node.firstChild.parentNode.removeChild(node.firstChild); - } - } - - // clean editor if they are image or table in deleted content - $(".note-control-selection").hide(); - $('.o_table_handler').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'); - }, -}); - -/* t-field options */ - -options.many2one = Option.extend({ - start: function () { - var self = this; - - this.Model = this.$target.data('oe-many2one-model'); - this.ID = +this.$target.data('oe-many2one-id'); - - // create search button and bind search bar - this.$btn = $(core.qweb.render("website.many2one.button")) - .insertAfter(this.$overlay.find('.oe_options')); - - this.$ul = this.$btn.find("ul"); - this.$search = this.$ul.find('li:first'); - this.$search.find('input').on('mousedown click mouseup keyup keydown', function (e) { - e.stopPropagation(); - }); - - // move menu item - setTimeout(function () { - if (self.$overlay.find('.oe_options').hasClass('hidden')) { - self.$btn.css('height', '0').find('> a').addClass('hidden'); - self.$ul.show().css({ - 'top': '-24px', 'margin': '0', 'padding': '2px 0', 'position': 'relative' - }); - } else { - self.$btn.find('a').on('click', function () { - self.clear(); - }); - } - },0); - - // bind search input - this.$search.find('input') - .focus() - .on('keyup', function() { - self.find_existing($(this).val()); - }); - - // bind result - this.$ul.on('click', "li:not(:first) a", function () { - self.select_record(this); - }); - }, - - on_focus: function () { - this.$target.attr('contentEditable', 'false'); - this.clear(); - this._super(); - }, - clear: function () { - var self = this; - this.$search.siblings().remove(); - self.$search.find('input').val(""); - setTimeout(function () { - self.$search.find('input').focus(); - },0); - }, - - find_existing: function (name) { - var self = this; - var domain = []; - if (!name || !name.length) { - self.$search.siblings().remove(); - return; - } - if (isNaN(+name)) { - if (this.Model === "res.partner") { - domain.push(['name', 'ilike', name]); - } else { - domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]); - } - } else { - domain.push(['id', '=', name]); - } - - ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: this.Model, - method: 'search_read', - args: [domain, this.Model === "res.partner" ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']], - kwargs: { - order: 'name DESC', - limit: 5, - context: website.get_context(), - } - }).then(function (result){ - self.$search.siblings().remove(); - self.$search.after(core.qweb.render("website.many2one.search",{contacts:result})); - }); - }, - - get_contact_rendering: function (options) { - return ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website.qweb.field.contact', - method: 'get_record_to_html', - args: [[this.ID]], - kwargs: { - options: options, - context: website.get_context(), - } - }); - }, - - select_record: function (li) { - var self = this; - - this.BuildingBlock.parent.rte.historyRecordUndo(this.$target); - - self.ID = +$(li).data("id"); - self.$target.attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID); - - if (self.$target.data('oe-type') === "contact") { - $('[data-oe-contact-options]') - .filter('[data-oe-model="'+self.$target.data('oe-model')+'"]') - .filter('[data-oe-id="'+self.$target.data('oe-id')+'"]') - .filter('[data-oe-field="'+self.$target.data('oe-field')+'"]') - .filter('[data-oe-contact-options!="'+self.$target.data('oe-contact-options')+'"]') - .add(self.$target) - .attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID) - .each(function () { - var $node = $(this); - self.get_contact_rendering($node.data('oe-contact-options')) - .then(function (html){ - $node.html(html); - }); - }); - } else { - self.$target.html($(li).data("name")); - } - - self.clear(); - } -}); - -return { - BuildingBlock: BuildingBlock, - options: options, - Option: Option, - globalSelector: globalSelector, -}; - -}); +});
\ No newline at end of file diff --git a/addons/website/static/src/js/website.theme.js b/addons/website/static/src/js/website.theme.js index b3dcbc9b0b6..8d5dcc80516 100644 --- a/addons/website/static/src/js/website.theme.js +++ b/addons/website/static/src/js/website.theme.js @@ -19,7 +19,7 @@ ajax.jsonRpc('/web/dataset/call', 'call', { ajax.jsonRpc('/web/dataset/call', 'call', { 'model': 'ir.ui.view', 'method': 'read_template', - 'args': ['website.colorpicker', website.get_context()] + 'args': ['web_editor.colorpicker', website.get_context()] }).done(function (data) { QWeb.add_template(data); }); diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js index 775012e1aa8..cb02f7346e9 100644 --- a/addons/website/static/src/js/website.translator.js +++ b/addons/website/static/src/js/website.translator.js @@ -2,229 +2,81 @@ odoo.define('website.translator', function (require) { 'use strict'; var core = require('web.core'); -var editor = require('website.editor'); +var ajax = require('web.ajax'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var translate = require('web_editor.translate'); var website = require('website.website'); -var ajax = require('ajax'); -var QWeb = core.qweb; +var qweb = core.qweb; -if (!website.translatable) { - // Temporary hack until the editor bar is moved to the web client +if (!translate.translatable) { return; } -website.add_template_file('/website/static/src/xml/website.translator.xml'); -var nodialog = 'website_translator_nodialog'; - -editor.EditorBar.include({ - do_not_translate : ['-','*','!'], - start: function () { - var self = this; - this.initial_content = {}; - return this._super.apply(this, arguments).then(function () { - var $edit_button = $("button[data-action=edit]"); - $edit_button.removeClass("hidden"); - - if(website.no_editor) { - $edit_button.removeProp('disabled'); - } else { - $edit_button.parent().after(QWeb.render('website.TranslatorAdditionalButtons')); - $('a[data-action=edit_master]').on('click', self, function(ev) { - self.edit_master(ev); - }); - } - $('.js_hide_on_translate').hide(); - }); - }, - edit: function () { - var self = this; - var mysuper = this._super; - if (!localStorage[nodialog]) { - var dialog = new website.TranslatorDialog(); - dialog.appendTo($(document.body)); - dialog.on('activate', this, function () { - localStorage[nodialog] = dialog.$('input[name=do_not_show]').prop('checked') || ''; - dialog.$el.modal('hide'); - self.translate().then(function () { - mysuper.call(self, true); - if(self.gengo_translate){ - self.translation_gengo_display() - } - }); - }); +website.TopBar.include({ + events: _.extend({}, website.TopBar.prototype.events, { + 'click [data-action="edit_master"]': 'edit_master', + 'click [data-action="translate"]': 'translate', + }), + translate: function (ev) { + ev.preventDefault(); + if (translate.edit_translations) { + translate.instance.edit(); } else { - this.translate().then(function () { - mysuper.call(self, true); - if(self.gengo_translate){ - self.translation_gengo_display() - } - }); + location.search += '&edit_translations'; } }, edit_master: function (ev) { ev.preventDefault(); - var link = $('.js_language_selector a[data-default-lang]')[0]; - if (link) { - link.search += (link.search ? '&' : '?') + 'enable_editor=1'; - var url = link.pathname + link.search + window.location.hash; - link.pathname = '/website/lang/default'; - link.search = '?' + $.param({r: url}); - window.location = link.href; - } + var $link = $('.js_language_selector a[data-default-lang]'); + $link[0].search += ($link[0].search ? '&' : '?') + 'enable_editor=1'; + $link.click(); }, - translate: function () { - var self = this; - this.translations = null; - return ajax.jsonRpc('/website/get_view_translations', 'call', { - 'xml_id': $(document.documentElement).data('view-xmlid'), - 'lang': website.get_context().lang, - }).then(function (translations) { - self.translations = translations; - self.processTranslatableNodes(); - // Disable non translatable t-fields - $('[data-oe-type][data-oe-translate="0"]').removeAttr('data-oe-type'); - }); - }, - processTranslatableNodes: function () { - var self = this; - var source_attr = 'data-oe-source-id'; - var $editables = $('[data-oe-model="ir.ui.view"]') - .not('link, script') - .not('.oe_snippets,.oe_snippet, .oe_snippet *, .navbar-toggle') - .not('[data-oe-type]') - .add($('[data-oe-translate="1"]:not([data-oe-model="ir.ui.view"])').addClass('o_editable')); +}); - $editables.each(function () { - var $node = $(this); - var source_id = $node.parents('[' + source_attr + ']:first').attr(source_attr)|0; - var view_id = $node.attr('data-oe-source-id') || source_id || $node.attr('data-oe-id'); - self.transNode(this, view_id|0); - }); - $('.oe_translatable_text').on('paste', function () { - var node = $(this); - setTimeout(function () { - self.sanitizeNode(node); - }, 0); - }); - $(document).on('blur keyup paste', '.oe_translatable_text[contenteditable]', function(ev) { - var $node = $(this); - setTimeout(function () { - // Doing stuff next tick because paste and keyup events are - // fired before the content is changed - if (ev.type == 'paste') { - self.sanitizeNode($node[0]); - } - if (self.getInitialContent($node[0]) !== $node.text()) { - $node.addClass('o_dirty').removeClass('oe_translatable_todo oe_translatable_inprogress'); - } - }, 0); - }); - }, - getInitialContent: function (node) { - return this.initial_content[node.attributes['data-oe-nodeid'].value]; - }, - sanitizeNode: function (node) { - node.text(node.text()); - }, - isTextNode: function (node) { - return node.nodeType === 3 || node.nodeType === 4; - }, - isTranslatable: function (text) { - return text && _.str.trim(text) !== ''; - }, - markTranslatableNode: function (node, view_id) { - // TODO: link nodes with same content - node.className += ' oe_translatable_text'; - node.setAttribute('data-oe-translation-view-id', view_id); - var content = $(node).text().trim(); - var trans = this.translations.filter(function (t) { - return t.res_id === view_id && t.value.trim() === content; - }); - if (trans.length) { - node.setAttribute('data-oe-translation-id', trans[0].id); - if(trans[0].gengo_translation && (trans[0].state == 'inprogress' || trans[0].state == 'to_translate')){ - node.className += ' oe_translatable_inprogress'; - } - } else { - node.className += this.do_not_translate.indexOf(node.textContent.trim()) ? ' oe_translatable_todo' : ''; + +if (!translate.edit_translations) { + return; +} + +ajax.loadXML('/website/static/src/xml/website.translator.xml', qweb); + +var nodialog = 'website_translator_nodialog'; + +var Translate = translate.Class.include({ + onTranslateReady: function () { + if(this.gengo_translate){ + this.translation_gengo_display(); } - node.contentEditable = true; - var nid = _.uniqueId(); - $(node).attr('data-oe-nodeid', nid); - this.initial_content[nid] = content; + this._super(); }, - save: function () { - var self = this; - var trans = {}; - // this._super.apply(this, arguments); - $('.oe_translatable_text.o_dirty').each(function () { - var $node = $(this); - var data = $node.data(); - if (!trans[data.oeTranslationViewId]) { - trans[data.oeTranslationViewId] = []; + edit: function () { + $("#oe_main_menu_navbar").hide(); + if (localStorage[nodialog]) { + return; + } + var dialog = new TranslatorDialog(); + dialog.appendTo($(document.body)); + dialog.on('activate', this, function () { + if (dialog.$('input[name=do_not_show]').prop('checked')) { + localStorage.removeItem(nodialog); + } else { + localStorage.setItem(nodialog, true); } - trans[data.oeTranslationViewId].push({ - initial_content: self.getInitialContent(this), - new_content: $node.text(), - translation_id: data.oeTranslationId || null - }); + dialog.$el.modal('hide'); }); - ajax.jsonRpc('/website/set_translations', 'call', { - 'data': trans, - 'lang': website.get_context()['lang'], - }).then(function () { - self._save(); - }).fail(function () { - // TODO: bootstrap alert with error message - alert("Could not save translation"); - }); - }, - transNode: function (node, view_id) { - // Mostly handling text and cdata nodes here - // so avoid jquery usage in this function - if (node.attributes['data-oe-type']) { - if (node.attributes['data-oe-translate'].value == '1') { - node.className += ' oe_translatable_field'; - } - this.markTranslatableNode(node, view_id); - } else if (node.childNodes.length === 1 - && this.isTextNode(node.childNodes[0]) - && !node.getAttribute('data-oe-model')) { - this.markTranslatableNode(node, view_id); - } else { - for (var i = 0, l = node.childNodes.length; i < l; i ++) { - var n = node.childNodes[i]; - if (this.isTextNode(n)) { - if (this.isTranslatable(n.data)) { - var container = document.createElement('span'); - node.insertBefore(container, n); - container.appendChild(n); - this.markTranslatableNode(container, view_id); - } - } else { - this.transNode(n, view_id); - } - } - } + return this._super(); }, -}); - -website.RTE.include({ - start: function () { - this._super.apply(this, arguments); - this.$el.hide(); - }, - fetch_editables: function (root) { - $(root).click(function (ev) { - ev.preventDefault(); - }); - return $('[data-oe-translate="1"]'); + cancel: function () { + $("#oe_main_menu_navbar").show(); + return this._super(); } }); -website.TranslatorDialog = core.Widget.extend({ - events: _.extend({}, website.EditorBar.prototype.events, { +var TranslatorDialog = Widget.extend({ + events: _.extend({}, website.TopBar.prototype.events, { 'hidden.bs.modal': 'destroy', 'click button[data-action=activate]': function (ev) { this.trigger('activate'); diff --git a/addons/website/static/src/xml/website.backend.xml b/addons/website/static/src/xml/website.backend.xml index 826e29e261c..5d5f8e4c1ba 100644 --- a/addons/website/static/src/xml/website.backend.xml +++ b/addons/website/static/src/xml/website.backend.xml @@ -6,10 +6,4 @@ <span class="o_stat_text oe_publish text-success">Published</span> </div> - <t t-name="FieldTextHtmlFrame"> - <div class="oe_form_field oe_form_field_html_frame oe_form_embedded_html" t-att-style="widget.node.attrs.style"> - <iframe/> - </div> - </t> - </templates> diff --git a/addons/website/static/src/xml/website.contentMenu.xml b/addons/website/static/src/xml/website.contentMenu.xml index 2256f4bbf33..d6b1fe40b72 100644 --- a/addons/website/static/src/xml/website.contentMenu.xml +++ b/addons/website/static/src/xml/website.contentMenu.xml @@ -23,7 +23,7 @@ </li> </t> <t t-name="website.contentMenu.dialog.select"> - <t t-call="website.editor.dialog"> + <t t-call="web_editor.dialog"> <t t-set="title">Select a Menu</t> <t t-set="button">Continue</t> <select class="form-control mb16" t-if="widget.roots"> @@ -34,7 +34,7 @@ </t> </t> <t t-name="website.contentMenu.dialog.edit"> - <t t-call="website.editor.dialog"> + <t t-call="web_editor.dialog"> <t t-set="title">Edit Menu</t> <select class="form-control mb16" t-if="widget.roots"> <t t-foreach="widget.roots" t-as="root"> @@ -58,7 +58,7 @@ </div> </t> </t> -<t t-name="website.contentMenu.dialog.add" t-extend="website.editor.dialog.link"> +<t t-name="website.contentMenu.dialog.add" t-extend="web_editor.dialog.link"> <t t-jquery="t[t-set='title']" t-operation="inner"> <t t-if="!widget.update_mode">Add Menu Entry</t> <t t-if="widget.update_mode">Edit Menu Entry</t> diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml index 198336650a4..ce58e3a6c1e 100644 --- a/addons/website/static/src/xml/website.editor.xml +++ b/addons/website/static/src/xml/website.editor.xml @@ -1,601 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> <templates id="template" xml:space="preserve"> - <t t-name="website.editorbar"> - <div id="website-top-navbar" class="navbar navbar-inverse navbar-fixed-top hidden-xs" style="display: none;"> - <div class="navbar-header" id="website-top-edit"> - <ul class="nav navbar-nav js_editor_placeholder" style="position: absolute; left: 180px;"> - </ul> - <form class="navbar-form text-muted"> - <button type="button" data-action="save" - class="btn btn-primary btn-xs">Save</button> - <span>or</span> - <a href="#" data-action="cancel" class="btn btn-link btn-xs">Discard</a> - </form> - </div> - </div> - </t> - - <t t-name="website.editor.dialog"> - <div class="modal note-link-dialog" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title"><t t-esc="title"/></h3> - </div> - <div class="modal-body"><t t-raw="0"/></div> - <div class="modal-footer"> - <button type="button" class="btn btn-primary save" t-if="!button">Save</button> - <button type="button" class="btn btn-primary save" t-if="button"><t t-raw="button"/></button> - <button type="button" class="btn hidden wait" disabled="disabled"/> - or - <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a> - </div> - </div> - </div> - </div> - </t> - <t t-name="website.editor.dialog.link.footer-button"> - <button type="button" class="btn pull-left remove btn-danger">Remove Link</button> - </t> - <t t-name="website.editor.dialog.link"> - <t t-call="website.editor.dialog"> - <t t-set="title">Link to</t> - <form> - <div class="list-group"> - <div class="form-group list-group-item active"> - <h3 class="list-group-item-heading"> - <label for="link-new" class="control-label"> - Link Label - </label> - </h3> - <input type="text" class="form-control" id="link-text" required="required"/> - </div> - </div> - <ul class="list-group"> - <li class="list-group-item form-group active"> - <h4 class="list-group-item-heading"> - <label for="link-page" class="control-label"> - Page - </label> - </h4> - <input class="page form-control url-source" - id="link-page" type="hidden"/> - </li> - <li class="list-group-item form-group clearfix"> - <div class="pull-right" style="display: none;"> - <label> - <input type="checkbox" class="window-new"/> - Open in new window - </label> - </div> - <h4 class="list-group-item-heading"> - <label for="link-external" class="control-label"> - URL or Email Address - </label> - </h4> - <input type="text" class="form-control url email-address url-source" - id="link-external" placeholder="http://openerp.com"/> - </li> - </ul> - <div class="form-horizontal link-style"> - <div class="form-group"> - <label class="col-sm-2 control-label"> - Style - </label> - <div class="col-sm-4"> - <div class="dropdown" style="line-height: 2em;"> - <a data-toggle="dropdown" class="btn btn-default" href="#">Color Style <span class="caret"></span></a> - <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> - <li> - <label class="btn btn-link">Link<input value="" class="link-style" type="radio" name="link-style-type" checked="checked"/></label> - </li> - <li> - <label class="btn btn-default">Basic<input value="btn-default" class="link-style" type="radio" name="link-style-type"/></label> - </li> - <li> - <label class="btn btn-success">Success<input value="btn-success" class="link-style" type="radio" name="link-style-type"/></label> - </li> - <li> - <label class="btn btn-primary">Primary<input value="btn-primary" class="link-style" type="radio" name="link-style-type"/></label> - </li> - <li> - <label class="btn btn-info">Info<input value="btn-info" class="link-style" type="radio" name="link-style-type"/></label> - </li> - <li> - <label class="btn btn-warning">Warning<input value="btn-warning" class="link-style" type="radio" name="link-style-type"/></label> - </li> - <li> - <label class="btn btn-danger">Danger<input value="btn-danger" class="link-style" type="radio" name="link-style-type"/></label> - </li> - </ul> - </div> - </div> - <div class="col-sm-4"> - <div class="dropdown" style="line-height: 2em;"> - <a data-toggle="dropdown" class="btn btn-default" href="#">Size <span class="caret"></span></a> - <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> - <li> - <label class="btn btn-default btn-xs">Extra Small<input value="btn-xs" class="link-style" type="radio" name="link-style-size"/></label> - </li> - <li> - <label class="btn btn-default btn-sm">Small<input value="btn-sm" class="link-style" type="radio" name="link-style-size"/></label> - </li> - <li> - <label class="btn btn-default">Default<input value="" class="link-style" type="radio" name="link-style-size" checked="checked"/></label> - </li> - <li> - <label class="btn btn-default btn-lg">Large<input value="btn-lg" class="link-style" type="radio" name="link-style-size"/></label> - </li> - </ul> - </div> - </div> - </div> - </div> - <div class="form-horizontal link-style"> - <div class="form-group"> - <label class="col-sm-2 control-label"> - Preview - </label> - <div class="col-sm-10" style="line-height: 2em;"> - <div style="overflow: auto; max-width: 100%; max-height: 200px;"> - <a id="link-preview" href="#"></a> - </div> - </div> - </div> - </div> - </form> - </t> - </t> - <t t-name="website.editor.dialog.media"> - <div class="modal note-image-dialog" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog select-media"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title">Select a Media</h3> - </div> - <div class="modal-body"> - <ul class="nav nav-tabs"> - <li class="active"><a href="#editor-media-image" data-toggle="tab">Image</a></li> - <li><a href="#editor-media-icon" data-toggle="tab">Pictogram</a></li> - <li><a href="#editor-media-video" data-toggle="tab">Video</a></li> - <li class="search" style="float: right;"> - <ul class="pager mb0 mt0"> - <li class="previous disabled"><a href="#">← Previous</a></li> - <li class="next disabled"><a href="#">Next →</a></li> - </ul> - </li> - <li class="search" style="float: right;"> - <form action="#"> - <div class="form-group font-icons fa fa-search mb0"> - <input type="search" class="form-control" id="icon-search"/> - </div> - </form> - </li> - </ul> - - <!-- Tab panes --> - <div class="tab-content"> - <div class="tab-pane fade in active" id="editor-media-image"> - </div> - <div class="tab-pane fade" id="editor-media-icon"> - </div> - <div class="tab-pane fade" id="editor-media-video"> - </div> - </div> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-primary save">Save</button> - <button type="button" class="btn hidden wait" disabled="disabled"/> - or - <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a> - </div> - </div> - </div> - </div> - </t> - <t t-name="website.editor.dialog.image"> - <div> - <form method="POST" - action="/website/attach" - enctype="multipart/form-data" - target="fileframe" - class="form-inline"> - <div class="well"> - <div class="form-group pull-left"> - <input type="file" name="upload" accept="image/*" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/> - <input type="hidden" name="disable_optimization" value=""/> - <div class="btn-group"> - <button type="button" class="btn btn-primary filepicker">Upload an image from your computer</button> - <button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown"> - <span class="caret"></span> - <span class="sr-only">Alternate Upload</span> - </button> - <ul class="dropdown-menu" role="menu"> - <li><a href="#" class="js_disable_optimization">Upload image without optimization</a></li> - </ul> - </div> - <button type="button" class="btn hidden wait" disabled="disabled">Uploading...</button> - </div> - <div> - <span class="text-muted"> — or — </span> - <label for="iamgeurl">Add an image URL</label> - <div class="form-group btn-group"> - <input type="text" name="url" class="form-control url pull-left" style="width: 320px;" id="iamgeurl" placeholder="http://openerp.com/logo.png"/> - <button class="btn btn-default" type="submit">Add</button> - </div> - </div> - </div> - <input type="hidden" name="func"/> - <div class="help-block"/> - <div class="existing-attachments"> - <t t-call="website.editor.dialog.image.existing"/> - </div> - </form> - </div> - <iframe src="about:blank" name="fileframe" class="hidden"/> - </t> - <t t-name="website.editor.dialog.image.existing"> - <div class="modal" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog select-image"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title">Select a Picture</h3> - </div> - <div class="modal-body has-error"> - <div class="existing-attachments"/> - <div class="help-block"/> - </div> - <div class="modal-footer"> - <a href="#" data-dismiss="modal" aria-hidden="true">Discard</a> - </div> - </div> - </div> - </div> - </t> - <t t-name="website.editor.dialog.image.existing.content"> - <div class="existing-attachments"> - <div class="row mt16" t-foreach="rows" t-as="row"> - <div class="col-sm-2 existing-attachment-cell" - t-foreach="row" t-as="attachment"> - <i class="fa fa-times existing-attachment-remove" t-att-data-id="attachment.id"/> - <img t-att-src="attachment.website_url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" class="img img-responsive"/> - </div> - </div> - </div> - </t> - <t t-name="website.editor.dialog.image.existing.error"> - <div class="help-block"> - <p>The image could not be deleted because it is used in the - following pages or views:</p> - <ul t-foreach="views" t-as="view"> - <li> - <a t-attf-href="/web#model=ir.ui.view&id=#{view.id}"> - <t t-esc="view.name"/> - </a> - </li> - </ul> - </div> - </t> - <t t-name="website.editor.table.panel"> - <table class="editorbar-panel"> - <tr t-foreach="rows"><td t-foreach="cols">​</td></tr> - </table> - </t> - <t t-name="website.editor.table"> - <table class="table table-bordered table-responsive"> - <tbody> - <tr t-foreach="rows"><td t-foreach="cols">​</td></tr> - </tbody> - </table> - </t> - - <t t-name="website.editor.discard"> - <div class="modal" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title">Discard edition</h3> - </div> - <div class="modal-body"> - <p> - If you discard the current edition, <em>all</em> - unsaved changes will be lost. - </p> - <p>You can cancel to return to the edition mode.</p> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-danger">Discard</button> - or - <a href="#" data-dismiss="modal" aria-hidden="true">Cancel</a> - </div> - </div> - </div> - </div> - </t> - - <t t-name="website.editor.hoverbutton.link"> - <button contentEditable="false" type="button" data-event="showLinkDialog" tabindex="-1" class="btn btn-primary hover-edition-button btn-xs">Change</button> - </t> - - <t t-name="website.editor.hoverbutton.media"> - <div contentEditable="false" class="hover-edition dropdown"> - <a class="btn btn-primary btn-sm" data-toggle="dropdown" href="#">Customize</a> - <ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"> - <li><a href="#" class="hover-edition-button">Change Media</a></li> - <li><a href="#" class="hover-style-button">Style</a></li> - <li class="dropdown-submenu"> - <a href="#" tabindex="-1">Rotation</a> - <ul class="dropdown-menu"> - <li data-value="fa-spin"><a>Spin</a></li> - <li data-value="fa-flip-horizontal"><a>Horizontal flip</a></li> - <li data-value="fa-flip-vertical"><a>Vertical flip</a></li> - </ul> - </li> - <li><a href="#" data-value="fa-border">border</a></li> - </ul> - </div> - </t> - <t t-name="website.editor.dialog.alt"> - <t t-call="website.editor.dialog"> - <t t-set="title">Change media Alt and Title</t> - <form action="#" class="form-horizontal"> - <div class="form-group"> - <label for="alt" class="col-sm-2 control-label">Alt:</label> - <div class="col-sm-9"><input id="alt" type="text" class="form-control" required="required" t-att-value="widget.alt"/></div> - </div> - <div class="form-group"> - <label for="title" class="col-sm-2 control-label">Title:</label> - <div class="col-sm-9"><input id="title" type="text" class="form-control" required="required" t-att-value="widget.title"/></div> - </div> - </form> + <t t-extend="web_editor.dialog.link"> + <t t-jquery=".list-group-item.form-group"> + this.removeClass('active'); </t> - </t> - - <t t-name="website.editor.dialog.font-icons"> - <form action="#"> - <input type="hidden" id="fa-icon"/> - <input type="hidden" id="fa-size" value="fa-1x"/> - <div class="font-icons-icons"> - <t t-call="website.editor.dialog.font-icons.icons"> - <t t-set="iconsParser" t-value="widget.iconsParser"/> - </t> - </div> - </form> - </t> - <t t-name="website.editor.dialog.font-icons.icons"> - <t t-foreach="iconsParser" t-as="data"> - <span t-foreach="data.icons" t-as="icon" - t-attf-class="#{data.base} font-icons-icon #{icon}" - t-att-data-id="icon"/> + <t t-jquery=".list-group-item.form-group:has(#link-external)" t-operation="before"> + <li class="list-group-item form-group"> + <h4 class="list-group-item-heading"> + <label for="link-page" class="control-label"> + Page + </label> + </h4> + <input class="page form-control url-source" id="link-page" type="hidden"/> + </li> </t> </t> - <t t-name="website.editor.dialog.video"> - <form action="#" class="form-inline"> - <div class="well"> - <div class="form-group btn-group"> - <span class="text-muted pull-right" style="margin-left:10px; line-height: 2em;">(Youtube, Vimeo, Dailymotion)</span> - <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left">Set a video URL</label> - <input type="text" - name="url" - class="form-control url pull-left" - style="width: 400px;" - id="urlvideo" - placeholder="//www.youtube.com/embed/yws1tbgNV7k" - t-translation="off"/> - <button class="btn btn-default">Preview</button> - </div> - <div class="form-group btn-group"> - <label for="urlvideo" style="width: 220px; line-height: 2em;" class="pull-left"><span class="text-muted">— or —</span> Embed Video (HTML)</label> - <input type="text" - name="embed" - class="form-control url pull-left" - style="width: 400px;" - id="embedvideo" - placeholder='<iframe src="//www.youtube.com/embed/yws1tbgNV7k"></iframe>' - t-translation="off"/> - <button class="btn btn-default">Preview</button> - </div> - </div> - - <div id="video-preview" style="width: 450px; margin: 0 auto;"> - <div class="media_iframe_video"> - <div class="media_iframe_video_size"></div> - <iframe - src="" - frameborder="0" - allowfullscreen="allowfullscreen"></iframe> - </div> - </div> - - <div class="text-center mt32"> - <div class="checkbox"> - <label><input type="checkbox" id="autoplay"/> Autoplay</label> - </div> - </div> - </form> - </t> - <t t-name="website.editor.insert.inline.media"> - <p class="inline-media-link" contenteditable="false"> - <a class="editor-insert-media fa fa-plus-circle"></a> - </p> - </t> - <t t-name="website.editor.tplButton"> - <button type="button" t-att-class="'btn btn-default btn-sm btn-small ' + - (options.className ? options.className : '') + - (options.dropdown ? 'dropdown-toggle' : '') " - t-att-data-toggle= "options.dropdown ? 'dropdown' : ''" - t-att-title="options.title ? options.title : ''" - t-att-data-event="options.event ? options.event : ''" - t-att-data-value="options.value ? options.value : ''" - t-att-tabindex="-1"><t t-raw="options.sLabel"/> - <t t-if="options.dropdown"> - <span class="caret"></span> - </t> - </button> - <t t-raw="options.dropdown or ''"/> - </t> - <t t-name="website.editor.tplAirPopover"> - <div class="popover bottom in note-air-popover" style="display: none;"> - <div class="arrow"></div> - <div class="popover-content"> - <t t-foreach="options.airPopover" t-as="group"> - <div t-attf-class="note-#{group[0]} btn-group"> - <t t-foreach="group[1]" t-as="g"> - <t t-call="website.editor.tplButton"> - <t t-set="options" t-value="buttonInfo[g](lang ,options)"/> - </t> - </t> - </div> - </t> - </div> - </div> - </t> - <t t-name="website.editor.tplLinkPopover"> - <div class="note-link-popover popover bottom in'" style="display: none;"> - <div class="arrow"></div> - <div class="popover-content"> - <a href="http://www.google.com" target="_blank">www.google.com</a> - <div class="note-insert btn-group"> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.link.edit}" data-event="showLinkDialog" tabindex="-1"> - <i class="fa fa-pencil-square-o"></i> - </button> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.link.unlink}" data-event="unlink" tabindex="-1"> - <i class="fa fa-chain-broken"></i> - </button> - </div> - </div> - </div> - </t> - <t t-name="website.editor.tplLinkImagePopover"> - <div class="note-link-image-popover popover bottom in'" style="display: none;"> - <div class="arrow"></div> - <div class="popover-content"> - <a href="http://www.google.com" target="_blank">www.google.com</a> - <div class="note-insert btn-group"> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.image}" data-event="showImageDialog" tabindex="-1"> - <i class="fa fa-picture-o"></i> - </button> - </div> - </div> - </div> - </t> - <t t-name="website.editor.tplImagePopover"> - <div class="note-image-popover popover bottom in'" style="display: none;"> - <div class="arrow"></div> - <div class="popover-content"> - <a href="http://www.google.com" target="_blank">www.google.com</a> - <div class="note-insert btn-group"> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.resizeFull}" data-event="resize" data-value="1" tabindex="-1"> - <span class="note-fontsize-10">100%</span> - </button> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.resizeHalf}" data-event="resize" data-value="0.5" tabindex="-1"> - <span class="note-fontsize-10">50%</span> - </button> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.resizeQuarter}" data-event="resize" data-value="0.25" tabindex="-1"> - <span class="note-fontsize-10">25%</span> - </button> - </div> - <div class="note-insert btn-group"> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.floatLeft}" data-event="floatMe" data-value="left" tabindex="-1"> - <i class="fa fa-align-left"></i> - </button> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.floatRight}" data-event="floatMe" data-value="right" tabindex="-1"> - <i class="fa fa-align-right"></i> - </button> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.floatNone}" data-event="floatMe" data-value="none" tabindex="-1"> - <i class="fa fa-align-justify"></i> - </button> - </div> - <div class="note-insert btn-group"> - <button type="button" class="btn btn-default btn-sm btn-small" t-attf-title="#{lang.image.remove}" data-event="removeMedia" data-value="none" tabindex="-1"> - <i class="fa fa-trash-o "></i> - </button> - </div> - </div> - </div> - </t> - <t t-name="website.editor.tplPopovers"> - <div class="note-popover"> - <t t-call="website.editor.tplLinkPopover"/> - <t t-call="website.editor.tplLinkImagePopover"/> - <t t-call="website.editor.tplImagePopover"/> - <t t-if="options.airMode"> - <t t-call="website.editor.tplAirPopover"/> - </t> - </div> - </t> - - <t t-name="website.editor.tplHandles"> - <div class="note-handle"> - <div class="note-control-selection"> - <div class="note-control-selection-bg"></div> - <div class="note-control-holder note-control-nw"></div> - <div class="note-control-holder note-control-ne"></div> - <div class="note-control-holder note-control-sw"></div> - <div class="note-control-sizing note-control-se"></div> - <div class="note-control-selection-info"></div> - </div> - </div> - </t> - <t t-name="website.editor.table"> - <ul class="dropdown-menu"> - <div class="note-dimension-picker"> - <div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"></div> - <div class="note-dimension-picker-highlighted"></div> - <div class="note-dimension-picker-unhighlighted"></div> - </div> - <div class="note-dimension-display"> 1 x 1 </div> - </ul> - </t> - <t t-name="website.editor.color"> - <ul class="dropdown-menu"> - <li> - <div class="btn-group"> - <div class="note-palette-title"><t t-raw="lang.color.background"/></div> - <div class="note-color-reset" data-event="backColor" data-value="inherit" t-att-title="lang.color.transparent"> - <t t-raw="lang.color.setTransparent"/> - </div> - <div class="note-color-palette" data-target-event="backColor"></div> - </div> - <div class="btn-group"> - <div class="note-palette-title"><t t-raw="lang.color.foreground"/></div> - <div class="note-color-reset" data-event="foreColor" data-value="inherit" t-att-title="lang.color.reset"> - <t t-raw="lang.color.resetToDefault"/> - </div> - <div class="note-color-palette" data-target-event="foreColor"></div> - </div> - </li> - </ul> - </t> - <t t-name="website.editor.paragraph"> - <div class="dropdown-menu"> - <div class="note-align btn-group"> - <t t-call="website.editor.tplButton"> - <t t-set="options" t-value="leftButton"/> - </t> - <t t-call="website.editor.tplButton"> - <t t-set="options" t-value="centerButton"/> - </t> - <t t-call="website.editor.tplButton"> - <t t-set="options" t-value="rightButton"/> - </t> - <t t-call="website.editor.tplButton"> - <t t-set="options" t-value="justifyButton"/> - </t> - </div> - <div class="note-list btn-group"> - <t t-call="website.editor.tplButton"> - <t t-set="options" t-value="indentButton"/> - </t> - <t t-call="website.editor.tplButton"> - <t t-set="options" t-value="outdentButton"/> - </t> - </div> - </div> - </t> </templates> diff --git a/addons/website/static/src/xml/website.gallery.xml b/addons/website/static/src/xml/website.gallery.xml index bf25990a713..21fe07dca4a 100644 --- a/addons/website/static/src/xml/website.gallery.xml +++ b/addons/website/static/src/xml/website.gallery.xml @@ -18,7 +18,7 @@ <h4 class="modal-title">Add images</h4> </div> <div class="modal-body"> - <form action="/website/attach" method="post" enctype="multipart/form-data" > + <form action="/web_editor/attachment/add" method="post" enctype="multipart/form-data" > <div class="form-group"> <input name="upload" type="file" value="Choose images" multiple="multiple" accept="image/*"/> </div> diff --git a/addons/website/static/src/xml/website.translator.xml b/addons/website/static/src/xml/website.translator.xml index 0019f8d9d3a..8e9c9564f08 100644 --- a/addons/website/static/src/xml/website.translator.xml +++ b/addons/website/static/src/xml/website.translator.xml @@ -1,8 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <templates id="template" xml:space="preserve"> + <t t-name="website.TranslatorAdditionalButtons"> <li><a class="btn btn-link" data-action="edit_master" href="#"> or Edit Master</a></li> </t> + <t t-name="website.TranslatorDialog"> <div class="modal fade oe_website_translator" tabindex="-1" role="dialog"> <div class="modal-dialog"> @@ -19,10 +21,10 @@ <p> Here are the visuals used to help you translate efficiently: <ul class="oe_translate_examples"> - <li class="oe_translatable_todo"> + <li data-oe-translation-state="to_translate"> Content to translate </li> - <li class="oe_translatable_text"> + <li data-oe-translation-state="translated"> Translated content </li> </ul> @@ -37,7 +39,7 @@ </section> </div> <div class="modal-footer"> - <div class="pull-right"> + <div class="pull-left"> <input type="checkbox" name="do_not_show"/> Do not show this dialog later. </div> diff --git a/addons/website/tests/test_converter.py b/addons/website/tests/test_converter.py index 0ab71829f9e..ba60e2fac95 100644 --- a/addons/website/tests/test_converter.py +++ b/addons/website/tests/test_converter.py @@ -1,15 +1,8 @@ # -*- coding: utf-8 -*- -import textwrap import unittest2 - -from lxml import etree, html -from lxml.builder import E - -from openerp.tests import common -from openerp.addons.base.ir import ir_qweb -from openerp.addons.website.models.ir_qweb import html_to_text from openerp.addons.website.models.website import slugify, unslug + class TestUnslug(unittest2.TestCase): def test_unslug(self): tests = { @@ -31,237 +24,6 @@ class TestUnslug(unittest2.TestCase): self.assertEqual(unslug(slug), expected) -class TestHTMLToText(unittest2.TestCase): - def test_rawstring(self): - self.assertEqual( - "foobar", - html_to_text(E.div("foobar"))) - - def test_br(self): - self.assertEqual( - "foo\nbar", - html_to_text(E.div("foo", E.br(), "bar"))) - - self.assertEqual( - "foo\n\nbar\nbaz", - html_to_text(E.div( - "foo", E.br(), E.br(), - "bar", E.br(), - "baz"))) - - def test_p(self): - self.assertEqual( - "foo\n\nbar\n\nbaz", - html_to_text(E.div( - "foo", - E.p("bar"), - "baz"))) - - self.assertEqual( - "foo", - html_to_text(E.div(E.p("foo")))) - - self.assertEqual( - "foo\n\nbar", - html_to_text(E.div("foo", E.p("bar")))) - self.assertEqual( - "foo\n\nbar", - html_to_text(E.div(E.p("foo"), "bar"))) - - self.assertEqual( - "foo\n\nbar\n\nbaz", - html_to_text(E.div( - E.p("foo"), - E.p("bar"), - E.p("baz"), - ))) - - def test_div(self): - self.assertEqual( - "foo\nbar\nbaz", - html_to_text(E.div( - "foo", - E.div("bar"), - "baz" - ))) - - self.assertEqual( - "foo", - html_to_text(E.div(E.div("foo")))) - - self.assertEqual( - "foo\nbar", - html_to_text(E.div("foo", E.div("bar")))) - self.assertEqual( - "foo\nbar", - html_to_text(E.div(E.div("foo"), "bar"))) - - self.assertEqual( - "foo\nbar\nbaz", - html_to_text(E.div( - "foo", - E.div("bar"), - E.div("baz") - ))) - - def test_other_block(self): - self.assertEqual( - "foo\nbar\nbaz", - html_to_text(E.div( - "foo", - E.section("bar"), - "baz" - ))) - - def test_inline(self): - self.assertEqual( - "foobarbaz", - html_to_text(E.div("foo", E.span("bar"), "baz"))) - - def test_whitespace(self): - self.assertEqual( - "foo bar\nbaz", - html_to_text(E.div( - "foo\nbar", - E.br(), - "baz") - )) - - self.assertEqual( - "foo bar\nbaz", - html_to_text(E.div( - E.div(E.span("foo"), " bar"), - "baz"))) - -class TestConvertBack(common.TransactionCase): - def setUp(self): - super(TestConvertBack, self).setUp() - - def field_rountrip_result(self, field, value, expected): - model = 'website.converter.test' - Model = self.registry(model) - id = Model.create( - self.cr, self.uid, { - field: value - }) - [record] = Model.browse(self.cr, self.uid, [id]) - - e = etree.Element('span') - field_value = 'record.%s' % field - e.set('t-field', field_value) - - rendered = self.registry('website.qweb').render_tag_field( - e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, { - 'record': record, - }, context={'inherit_branding': True})) - element = html.fromstring( - rendered, parser=html.HTMLParser(encoding='utf-8')) - - converter = self.registry('website.qweb').get_converter_for( - element.get('data-oe-type')) - - value_back = converter.from_html( - self.cr, self.uid, model, Model._fields[field], element) - - if isinstance(expected, str): - expected = expected.decode('utf-8') - self.assertEqual(value_back, expected) - - def field_roundtrip(self, field, value): - self.field_rountrip_result(field, value, value) - - def test_integer(self): - self.field_roundtrip('integer', 42) - - def test_float(self): - self.field_roundtrip('float', 42.567890) - self.field_roundtrip('float', 324542.567890) - - def test_numeric(self): - self.field_roundtrip('numeric', 42.77) - - def test_char(self): - self.field_roundtrip('char', "foo bar") - - self.field_roundtrip('char', "ⒸⓄⓇⒼⒺ") - - def test_selection(self): - self.field_roundtrip('selection', 3) - - def test_selection_str(self): - self.field_roundtrip('selection_str', 'B') - - def test_text(self): - self.field_roundtrip('text', textwrap.dedent("""\ - You must obey the dance commander - Givin' out the order for fun - You must obey the dance commander - You know that he's the only one - Who gives the orders here, - Alright - Who gives the orders here, - Alright - - It would be awesome - If we could dance-a - It would be awesome, yeah - Let's take the chance-a - It would be awesome, yeah - Let's start the show - Because you never know - You never know - You never know until you go""")) - - def test_m2o(self): - """ the M2O field conversion (from html) is markedly different from - others as it directly writes into the m2o and returns nothing at all. - """ - model = 'website.converter.test' - field = 'many2one' - - Sub = self.registry('website.converter.test.sub') - sub_id = Sub.create(self.cr, self.uid, {'name': "Foo"}) - sub2_id = Sub.create(self.cr, self.uid, {'name': "Bar"}) - - Model = self.registry(model) - id = Model.create(self.cr, self.uid, {field: sub_id}) - [record] = Model.browse(self.cr, self.uid, [id]) - - e = etree.Element('span') - field_value = 'record.%s' % field - e.set('t-field', field_value) - - rendered = self.registry('website.qweb').render_tag_field( - e, {'field': field_value}, '', ir_qweb.QWebContext(self.cr, self.uid, { - 'record': record, - }, context={'inherit_branding': True})) - - element = html.fromstring(rendered, parser=html.HTMLParser(encoding='utf-8')) - # emulate edition - element.set('data-oe-many2one-id', str(sub2_id)) - element.text = "New content" - - converter = self.registry('website.qweb').get_converter_for( - element.get('data-oe-type')) - - value_back = converter.from_html( - self.cr, self.uid, model, Model._fields[field], element) - - self.assertIsNone( - value_back, "the m2o converter should return None to avoid spurious" - " or useless writes on the parent record") - - self.assertEqual( - Sub.browse(self.cr, self.uid, sub_id).name, - "Foo", - "element edition can't change directly the m2o record" - ) - self.assertEqual( - Model.browse(self.cr, self.uid, id).many2one.name, - "Bar", - "element edition should have been change the m2o id" - ) - class TestTitleToSlug(unittest2.TestCase): """ Those tests should pass with or without python-slugify diff --git a/addons/website/tests/test_ui.py b/addons/website/tests/test_ui.py index a944a9ce3d6..c1e45e743bd 100644 --- a/addons/website/tests/test_ui.py +++ b/addons/website/tests/test_ui.py @@ -3,13 +3,10 @@ import openerp.tests class TestUi(openerp.tests.HttpCase): def test_01_public_homepage(self): - self.phantom_js("/", "console.log('ok')", "odoo.__DEBUG__.services['website.snippets.animation']") + self.phantom_js("/", "console.log('ok')", "'website.snippets.animation' in odoo.__DEBUG__.services") def test_02_admin_homepage(self): - self.phantom_js("/", "console.log('ok')", "odoo.__DEBUG__.services['website.snippets.editor']", login='admin') + self.phantom_js("/", "console.log('ok')", "'website.snippets.editor' in odoo.__DEBUG__.services", login='admin') - def test_03_admin_rte(self): - self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('website_rte', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.website_rte", login='admin') - - def test_04_admin_tour_banner(self): + def test_03_admin_tour_banner(self): self.phantom_js("/", "odoo.__DEBUG__.services['web.Tour'].run('banner', 'test')", "odoo.__DEBUG__.services['web.Tour'].tours.banner", login='admin') diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml index af66c71d373..606e95c8503 100644 --- a/addons/website/views/snippets.xml +++ b/addons/website/views/snippets.xml @@ -888,13 +888,13 @@ </div> <div id="snippet_options" class="hidden"> - <t t-call="website.snippet_options"/> - </div> </template> <template id="snippet_options"> + + <t t-call="web_editor.snippet_options"/> <div data-js='blog-style' data-selector="section:not(.carousel):not(.parallax):not(.o_gallery)"> @@ -1018,11 +1018,6 @@ <li data-remove="remove"><a>Remove from gallery</a></li> </div> - <div data-js='media' - data-selector="img, .media_iframe_video, span.fa, i.fa, .glyphicon"> - <li data-edition="edition"><a>Change...</a></li> - </div> - <div data-js='carousel' data-selector=":not(.o_gallery > .container) > .carousel"> <li class="dropdown-submenu"> @@ -1123,14 +1118,6 @@ data-selector='.panel-group > .panel:has(> .panel-collapse)' data-drop-in='.panel-group:has(> .panel > .panel-collapse)'> </div> - - <!-- t-field --> - - <div data-js='many2one' - data-selector="[data-oe-many2one-model]" - data-no-check="true"> - </div> - </template> diff --git a/addons/website/views/website.backend.xml b/addons/website/views/website.backend.xml index 2590051499c..1e10a49dfa7 100644 --- a/addons/website/views/website.backend.xml +++ b/addons/website/views/website.backend.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <openerp> - <data> <template id="assets_backend" name="website assets for backend" inherit_id="web.assets_backend"> <xpath expr="." position="inside"> @@ -9,68 +8,4 @@ </xpath> </template> -<template id="assets_backend_editor" name="assets for backend editor"> - <t t-call="web.assets_common"/> - <script type="text/javascript" src="/web/static/src/js/watch.js"></script> - <script type="text/javascript" src="/website/static/src/js/website.js"></script> - <script type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script> - <script type="text/javascript" src="/web/static/lib/bootstrap/js/bootstrap.js"></script> - <link rel="stylesheet" href='/website/static/src/css/website.css'/> - <link id="bootstrap_css" rel='stylesheet' href='/web/static/lib/bootstrap/css/bootstrap.css'/> - <t t-call="website.summernote"/> - <t t-call="website.editor"/> - <script type="text/javascript" src="/website/static/src/js/website.backend.iframe.js"></script> - <style>.note-popover .popover { left: 5px !important; }</style> -</template> - -<template id="editor_layout" name="Editor layout"><!DOCTYPE html> - <html t-att-lang="lang and lang.replace('_', '-')" - t-att-data-website-id="website.id if editable and website else None" - t-att-data-editable="1" - t-att-data-translatable="'1' if translatable else None" - t-att-data-main-object="repr(main_object) if editable else None" - t-att-data-oe-company-name="res_company.name"> - <head> - <meta charset="utf-8" /> - <title>Odoo Editor layout</title> - <meta name="viewport" content="initial-scale=1"/> - <t t-set="languages" t-value="website.get_languages() if website else None"/> - <t t-if="request and request.website_multilang and website"> - <t t-foreach="website.get_alternate_languages(request.httprequest)" t-as="lang"> - <link rel="alternate" t-att-hreflang="lang['hreflang']" t-att-href="lang['href']"/> - </t> - </t> - <script> - var snippets_url = '<t t-esc="snippets or ''"/>'; - var callback = window.location.href.match(/callback=([^&=]+)/)[1]; - window.top.odoo[callback+"_set_value"] = function (value, fields_values) { - var editable = document.getElementsByClassName("o_editable")[0]; - if (value !== editable.innerHTML) { - editable.innerHTML = value; - } - }; - </script> - <t t-if="not dont_load_assets"> - <t t-call-assets="website.assets_backend_editor"/> - </t> - <t t-raw="head or ''"/> - </head> - <body onload="window.top.odoo[callback+'_content']();"> - <div id="wrapwrap"> - <main> - <t t-raw="0"/> - </main> - </div> - <t t-raw="add_html or ''"/> - </body> - </html> -</template> - -<template id="FieldTextHtml" name="Editor layout"> - <t t-call="website.editor_layout"> - <div class="o_editable" t-att-data-oe-model="model" t-att-data-oe-field="field" t-att-data-oe-id="res_id" data-oe-type="html"><t t-raw="content"/></div> - </t> -</template> - - </data> </openerp> diff --git a/addons/website/views/website_backend_navbar.xml b/addons/website/views/website_backend_navbar.xml index cd023bd3790..46fd18a57dd 100644 --- a/addons/website/views/website_backend_navbar.xml +++ b/addons/website/views/website_backend_navbar.xml @@ -35,8 +35,10 @@ </li> </ul> - <ul id="oe_editzone" class="nav navbar-nav hidden-xs" style="padding-left: 20px; display: none;" groups="base.group_website_publisher"> - <li><button type="button" data-action="edit" class="btn btn-primary btn-xs">Edit</button></li> + <ul id="oe_editzone" class="nav navbar-nav hidden-xs" style="padding-left: 20px;" groups="base.group_website_publisher"> + <li t-if="not translatable"><button type="button" data-action="edit" class="btn btn-primary btn-xs">Edit</button></li> + <li t-if="translatable"><button type="button" data-action="translate" class="btn btn-primary btn-xs">Translate</button></li> + <li t-if="translatable"><a class="btn btn-link" data-action="edit_master" href="#"> or Edit Master</a></li> </ul> <ul id="oe_systray" class="nav navbar-nav navbar-right pull-right hidden-xs"> diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 309c7009fad..98fda216872 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -4,72 +4,6 @@ <!-- Assets --> -<template id="summernote" name="Summernote"> - <script type="text/javascript" src="/website/static/src/js/openerp.define.js"></script> - <script>odoo.enable_website_define();</script> - <script>odoo.website_next_define = 'summernote/core/async';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/async.js"></script> - <script>odoo.website_next_define = 'summernote/core/func';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/func.js"></script> - <script>odoo.website_next_define = 'summernote/core/agent';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/agent.js"></script> - <script>odoo.website_next_define = 'summernote/core/list';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/list.js"></script> - <script>odoo.website_next_define = 'summernote/core/dom';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/dom.js"></script> - <script>odoo.website_next_define = 'summernote/core/key';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/key.js"></script> - <script>odoo.website_next_define = 'summernote/core/range';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/core/range.js"></script> - - <script>odoo.website_next_define = 'summernote/editing/Bullet';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Bullet.js"></script> - <script>odoo.website_next_define = 'summernote/editing/History';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/History.js"></script> - <script>odoo.website_next_define = 'summernote/editing/Style';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Style.js"></script> - <script>odoo.website_next_define = 'summernote/editing/Table';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Table.js"></script> - <script>odoo.website_next_define = 'summernote/editing/Typing';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Typing.js"></script> - <script>odoo.website_next_define = 'summernote/editing/Editor';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/editing/Editor.js"></script> - - <script>odoo.website_next_define = 'summernote/module/Button';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Button.js"></script> - <script>odoo.website_next_define = 'summernote/module/Dialog';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Dialog.js"></script> - <script>odoo.website_next_define = 'summernote/module/Handle';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Handle.js"></script> - <script>odoo.website_next_define = 'summernote/module/Popover';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Popover.js"></script> - <script>odoo.website_next_define = 'summernote/module/Toolbar';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/module/Toolbar.js"></script> - - <script>odoo.website_next_define = 'summernote/Renderer';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/Renderer.js"></script> - <script>odoo.website_next_define = 'summernote/EventHandler';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/EventHandler.js"></script> - - <script>odoo.website_next_define = 'summernote/settings';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/settings.js"></script> - - <script>odoo.website_next_define = 'summernote/summernote';</script> - <script type="text/javascript" src="/website/static/lib/summernote/src/js/summernote.js"></script> - <script>odoo.website_next_define = 'summernote/openerp';</script> - <script type="text/javascript" src="/website/static/src/js/summernote.js"></script> - <script>odoo.disable_website_define();</script> - - <!--link rel="stylesheet" href="/website/static/lib/summernote/src/less/summernote.less"/--> - <link rel="stylesheet" href="/website/static/lib/summernote/summernote.css"/> -</template> - -<template id="assets_common" inherit_id="web.assets_common" name="Website common assets"> - <xpath expr='.' position="inside"> - <t t-call="website.summernote"/> - </xpath> -</template> - <template id="assets_frontend" name="Website assets"> <link id="bootstrap_css" rel="stylesheet" href='/website/static/src/less/import_bootstrap.less'/> @@ -82,7 +16,6 @@ <script type="text/javascript" src="/website/static/src/js/website.snippets.animation.js"></script> <t t-call="web.bootstrap"/> - </template> <template id="assets_backend" name="website assets for backend" inherit_id="web.assets_backend"> @@ -92,41 +25,27 @@ </xpath> </template> -<template id="editor" name="Editor"> - <link rel='stylesheet' href='/website/static/src/css/snippets.css'/> - <link rel='stylesheet' href='/website/static/src/css/editor.css'/> - <link rel='stylesheet' href="/web/static/lib/jquery.ui/jquery-ui.css"/> - <script type="text/javascript" src="/website/static/lib/jQuery.transfo.js"></script> - <t t-call="web.jqueryui_conflict"> - <script type="text/javascript" src="/web/static/lib/jquery.ui/jquery-ui.js"></script> - </t> - <script type="text/javascript" src='/website/static/lib/nearest/jquery.nearest.js'></script> - - <!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) --> - <script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script> - <script type="text/javascript" src="/website/static/lib/MutationObservers/MutationObserver.js"></script> - <script type="text/javascript" src="/website/static/src/js/website.editor.js"></script> - <script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script> - <script type="text/javascript" src="/website/static/src/js/website.snippets.gallery.js" /> -</template> - <template id="assets_editor" name="Editor assets"> - <t t-call="website.editor"/> + <script type="text/javascript" src="/website/static/src/js/website.editor.js"></script> + <script type="text/javascript" src="/website/static/lib/jQuery.transfo.js"></script> <script type="text/javascript" src="/website/static/lib/ace/ace.js"></script> <script type="text/javascript" src="/website/static/lib/ace/theme-monokai.js"></script> <script type="text/javascript" src="/website/static/lib/ace/mode-xml.js"></script> <script type="text/javascript" src="/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script> - <script type="text/javascript" src="/website/static/lib//jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script> - <script type="text/javascript" src="/website/static/src/js/website.contentMenu.js"></script> <!-- groups="base.group_website_designer" --> + <script type="text/javascript" src="/website/static/lib/jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script> <script type="text/javascript" src="/website/static/src/js/website.mobile.js"></script> <script type="text/javascript" src="/website/static/src/js/website.seo.js"></script> <script type="text/javascript" src="/website/static/src/js/website.tour.js"></script> - <script type="text/javascript" src="/website/static/src/js/website.tour.banner.js"></script> <!-- groups="base.group_website_designer" --> - <script type="text/javascript" src="/website/static/src/js/website.tour.rte.js"></script> + <script type="text/javascript" src="/website/static/src/js/website.tour.banner.js"></script> <script type="text/javascript" src="/website/static/src/js/website.ace.js"></script> <script type="text/javascript" src="/website/static/src/js/website.translator.js"></script> <script type="text/javascript" src="/website/static/src/js/website.theme.js"></script> <script type="text/javascript" src="/website/static/src/js/web_planner_website.js"></script> + <script type="text/javascript" src="/website/static/src/js/website.contentMenu.js"></script> + <link rel='stylesheet' href='/web_editor/static/src/css/web_editor.css'/> + <link rel="stylesheet" href="/website/static/src/css/website.editor.css"/> + <script type="text/javascript" src="/website/static/src/js/website.snippets.editor.js"></script> + <script type="text/javascript" src="/website/static/src/js/website.snippets.gallery.js"></script> </template> <!-- Layout --> @@ -160,7 +79,8 @@ t-att-data-website-id="website.id if editable and website else None" t-att-data-editable="'1' if editable else None" t-att-data-translatable="'1' if translatable else None" - t-att-data-view-xmlid="xmlid if editable else None" + t-att-data-edit_translations="'1' if edit_translations else None" + t-att-data-view-xmlid="xmlid if editable or translatable else None" t-att-data-main-object="repr(main_object) if editable else None" t-att-data-oe-company-name="res_company.name"> <head> @@ -236,7 +156,7 @@ </b> </a> <ul class="dropdown-menu js_usermenu" role="menu"> - <li><a t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a></li> + <li id="o_logout"><a t-attf-href="/web/session/logout?redirect=/" role="menuitem">Logout</a></li> </ul> </li> </ul> @@ -266,6 +186,10 @@ </template> <template id="layout_editor" inherit_id="website.layout" name="Editor" groups="base.group_website_publisher,base.group_website_designer"> + <xpath expr='//t[@t-call-assets="website.assets_frontend"][@t-css="false"]' position="before"> + <t groups="base.group_website_designer" t-call-assets="web_editor.summernote"/> + <t groups="base.group_website_designer" t-call-assets="web_editor.editor"/> + </xpath> <xpath expr='//t[@t-call-assets="website.assets_frontend"][@t-css="false"]' position="after"> <t t-call-assets="website.assets_editor" async="True"/> </xpath> @@ -293,8 +217,6 @@ </xpath> </template> - - <!-- Optional Bundle for PDFJS lib Since PDFJS is quite huge (40000 lines), please only load it when it is necessary. @@ -368,7 +290,7 @@ <script type="text/javascript" src="/website/static/lib/pdfjs/pdf.js"></script> </template> - +<!-- Features template --> <template id="login_layout" inherit_id="web.login_layout" name="Website Login Layout"> <xpath expr="t" position="replace"> @@ -421,11 +343,11 @@ </template> <template id="language_selector"> - <ul class="list-inline js_language_selector mt16" t-if="(request and request.website_multilang and len(languages) > 1) or (website and editable)"> + <ul class="list-inline js_language_selector mt16" t-if="(request and request.website_multilang and len(languages) > 1) or (website and (editable or translatable))"> <li t-foreach="languages" t-as="lg"> <a t-att-href="url_for(request.httprequest.path + '?' + keep_query(), lang=lg[0])" - t-att-data-default-lang="editable and 'true' if website and lg[0] == website.default_lang_code else None" + t-att-data-default-lang="(editable or translatable) and 'true' if website and lg[0] == website.default_lang_code else None" t-att-data-lang="lg[0]" class="js_change_lang" > @@ -576,21 +498,6 @@ </div> </template> -<!-- color-picker --> -<!-- HTML class to hide option for one mode : only-text, only-bg --> - -<template id="website.colorpicker" name="Color-Picker"> - <table class="colorpicker"> - <tr> - <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> - </table> -</template> - <template id="kanban"> <t t-set="step"><t t-esc="step or 0"/></t> <t t-set="scope"><t t-esc="scope or 0"/></t> diff --git a/addons/website_gengo/static/src/xml/website.gengo.xml b/addons/website_gengo/static/src/xml/website.gengo.xml index a867f6eb478..b4d8c720744 100644 --- a/addons/website_gengo/static/src/xml/website.gengo.xml +++ b/addons/website_gengo/static/src/xml/website.gengo.xml @@ -23,10 +23,10 @@ <li style="background:#ffffb6;"> Content to translate or you can post them to <b><a href="http://gengo.com/" >Gengo</a></b> for translation. </li> - <li class="oe_translatable_inprogress"> + <li data-oe-translation-state="inprogress"> Translation in process (Gengo) </li> - <li class="oe_translatable_text"> + <li data-oe-translation-state="translated"> Already translated content </li> </ul> diff --git a/addons/website_version/models/ir_ui_view.py b/addons/website_version/models/ir_ui_view.py index 4a3ded70f43..549953254ba 100644 --- a/addons/website_version/models/ir_ui_view.py +++ b/addons/website_version/models/ir_ui_view.py @@ -63,7 +63,7 @@ class view(models.Model): return xml_id @tools.ormcache_context('uid', 'view_id', - keys=('lang', 'inherit_branding', 'editable', 'translatable', 'website_id', 'version_id')) + keys=('lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id', 'version_id')) def _read_template(self, cr, uid, view_id, context=None): arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch'] arch_tree = etree.fromstring(arch) diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py index 7fe0d8cc75d..f7489a5379d 100644 --- a/openerp/addons/base/ir/ir_qweb.py +++ b/openerp/addons/base/ir/ir_qweb.py @@ -592,7 +592,7 @@ class FieldConverter(osv.AbstractModel): ('data-oe-type', field_type), ('data-oe-expression', t_att['field']), ] - if record._fields[field_name].readonly: + if field.readonly: data.append(('data-oe-readonly', 1)) return data @@ -645,7 +645,8 @@ class FieldConverter(osv.AbstractModel): _build_attribute(name, value) for name, value in self.attributes( cr, uid, field_name, record, options, - source_element, g_att, t_att, qweb_context) + source_element, g_att, t_att, qweb_context, + context=context) ) return self.render_element(cr, uid, source_element, t_att, g_att, diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 58f3b12a412..ce2603809ab 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -901,7 +901,7 @@ class view(osv.osv): # apply ormcache_context decorator unless in dev mode... @tools.conditional(not config['dev_mode'], tools.ormcache_context('uid', 'view_id', - keys=('lang', 'inherit_branding', 'editable', 'translatable'))) + keys=('lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations'))) def _read_template(self, cr, uid, view_id, context=None): arch = self.read_combined(cr, uid, view_id, fields=['arch'], context=context)['arch'] arch_tree = etree.fromstring(arch) diff --git a/openerp/addons/base/tests/test_mail_examples.py b/openerp/addons/base/tests/test_mail_examples.py index 78f5cf55269..27ec3ee3e10 100644 --- a/openerp/addons/base/tests/test_mail_examples.py +++ b/openerp/addons/base/tests/test_mail_examples.py @@ -100,7 +100,7 @@ OERP_WEBSITE_HTML_1_OUT = [ ] OERP_WEBSITE_HTML_2 = """ -<div class="mt16 cke_widget_editable cke_widget_element oe_editable oe_dirty" data-oe-model="blog.post" data-oe-id="6" data-oe-field="content" data-oe-type="html" data-oe-translate="0" data-oe-expression="blog_post.content" data-cke-widget-data="{}" data-cke-widget-keep-attr="0" data-widget="oeref" contenteditable="true" data-cke-widget-editable="text"> +<div class="mt16 cke_widget_editable cke_widget_element o_editable o_dirty" data-oe-model="blog.post" data-oe-id="6" data-oe-field="content" data-oe-type="html" data-oe-expression="blog_post.content" data-cke-widget-data="{}" data-cke-widget-keep-attr="0" data-widget="oeref" contenteditable="true" data-cke-widget-editable="text"> <section class="mt16 mb16"> <div class="container"> <div class="row"> diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 67885926e61..e38ae437319 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -270,7 +270,7 @@ def xml_translate(callback, value): trans.process(root) return trans.get_done() except etree.ParseError: - wrapped = "<div>%s</div>" % value + wrapped = "<div>%s</div>" % encode(value) root = etree.fromstring(wrapped, etree.HTMLParser()) # html > body > div trans.process(root[0][0]) |
