flectra/addons/web_editor/static/src/js/editor/translator.js

376 lines
12 KiB
JavaScript

flectra.define('web_editor.translate', function (require) {
'use strict';
var core = require('web.core');
var Dialog = require('web.Dialog');
var localStorage = require('web.local_storage');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var rte = require('web_editor.rte');
var weWidgets = require('web_editor.widget');
var Dialog = require('web.Dialog');
var _t = core._t;
var localStorageNoDialogKey = 'website_translator_nodialog';
var RTETranslatorWidget = rte.Class.extend({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* If the element holds a translation, saves it. Otherwise, fallback to the
* standard saving but with the lang kept.
*
* @override
*/
_saveElement: function ($el, context, withLang) {
var self = this;
if ($el.data('oe-translation-id')) {
return this._rpc({
model: 'ir.translation',
method: 'save_html',
args: [
[+$el.data('oe-translation-id')],
this._getEscapedElement($el).html(),
context || weContext.get()
],
}).fail(function (error) {
Dialog.alert(null, error.data.message);
});
}
return this._super($el, context, withLang === undefined ? true : withLang);
},
});
var AttributeTranslateDialog = weWidgets.Dialog.extend({
/**
* @constructor
*/
init: function (parent, options, node) {
this._super(parent, _.extend({
title: _t("Translate Attribute"),
buttons: [
{text: _t("Close"), classes: 'btn-primary o_save_button', click: this.save}
],
}, options || {}));
this.$target = $(node);
this.translation = $(node).data('translation');
},
/**
* @override
*/
start: function () {
var self = this;
var $group = $('<div/>', {class: 'form-group'}).appendTo(this.$el);
_.each(this.translation, function (node, attr) {
var $node = $(node);
var $label = $('<label class="control-label"></label>').text(attr);
var $input = $('<input class="form-control"/>').val($node.html());
$input.on('change keyup', function () {
var value = $input.val();
$node.html(value).trigger('change', node);
$node.data('$node').attr($node.data('attribute'), value).trigger('translate');
self.trigger_up('rte_change', {target: node});
});
$group.append($label).append($input);
});
return this._super.apply(this, arguments);
}
});
var TranslatorInfoDialog = Dialog.extend({
template: 'web_editor.TranslatorInfoDialog',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/translator.xml']
),
/**
* @constructor
*/
init: function (parent, options) {
this._super(parent, _.extend({
title: _t("Translation Info"),
buttons: [
{text: _t("Ok, never show me this again"), classes: 'btn-primary', close: true, click: this._onStrongOk.bind(this)},
{text: _t("Ok"), close: true}
],
}, options || {}));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the "strong" ok is clicked -> adapt localstorage to make sure
* the dialog is never displayed again.
*
* @private
*/
_onStrongOk: function () {
localStorage.setItem(localStorageNoDialogKey, true);
},
});
var TranslatorMenuBar = Widget.extend({
template: 'web_editor.editorbar',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events: {
'click [data-action="save"]': '_onSaveClick',
'click [data-action="cancel"]': '_onCancelClick',
},
custom_events: {
'rte_change': '_onRTEChange',
},
/**
* @constructor
*/
init: function (parent, $target, lang) {
this._super.apply(this, arguments);
var $edit = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
$edit.filter(':has([data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field])').attr('data-oe-readonly', true);
this.$target = $edit.not('[data-oe-readonly]');
var attrs = ['placeholder', 'title', 'alt'];
_.each(attrs, function (attr) {
$target.find('['+attr+'*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
var $node = $(this);
var translation = $node.data('translation') || {};
var trans = $node.attr(attr);
var match = trans.match(/<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/);
var $trans = $(trans).addClass('hidden o_editable o_editable_translatable_attribute').appendTo('body');
$trans.data('$node', $node).data('attribute', attr);
translation[attr] = $trans[0];
$node.attr(attr, match[2]);
var select2 = $node.data('select2');
if (select2) {
select2.blur();
$node.on('translate', function () {
select2.blur();
});
$node = select2.container.find('input');
}
$node.addClass('o_translatable_attribute').data('translation', translation);
});
});
this.$target_attr = $target.find('.o_translatable_attribute');
this.$target_attribute = $('.o_editable_translatable_attribute');
this.lang = lang || weContext.get().lang;
this.rte = new RTETranslatorWidget(this, this._getRTEConfig);
},
/**
* @override
*/
start: function () {
this.$('#web_editor-toolbars').remove();
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.$el.show(); // TODO seems useless
this.trigger('edit');
if (!localStorage.getItem(localStorageNoDialogKey)) {
new TranslatorInfoDialog(this).open();
}
return this._super.apply(this, arguments);
},
/**
* @override
*/
destroy: function () {
this._cancel();
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Leaves translation mode by hiding the translator bar.
*
* @todo should be destroyed?
* @private
*/
_cancel: function () {
var self = this;
this.rte.cancel();
this.$target.each(function () {
$(this).html(self._getTranlationObject(this).value);
});
this._unmarkTranslatableNode();
this.trigger('cancel');
this.$el.hide();
window.onbeforeunload = null;
},
/**
* Returns the RTE summernote configuration for translation mode.
*
* @private
* @param {jQuery} $editable
*/
_getRTEConfig: function ($editable) {
return {
airMode : true,
focus: false,
airPopover: $editable.data('oe-model') ? [
['history', ['undo', 'redo']],
] : [
['font', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['color']],
['history', ['undo', 'redo']],
],
styleWithSpan: false,
inlinemedia : ['p'],
lang: 'flectra',
onChange: function (html, $editable) {
$editable.trigger('content_changed');
},
};
},
/**
* @private
*/
_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;
},
/**
* @private
*/
_markTranslatableNodes: function () {
var self = this;
this.$target.add(this.$target_attribute).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().prependEvent('click.translator', function (ev) {
if (ev.ctrlKey || !$(ev.target).is(':o_editable')) {
return;
}
ev.preventDefault();
ev.stopPropagation();
});
// attributes
this.$target_attr.each(function () {
var $node = $(this);
var translation = $node.data('translation');
_.each(translation, function (node, attr) {
var trans = self._getTranlationObject(node);
trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
$node.attr('data-oe-translation-state', (trans.state || 'to_translate'));
});
});
this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
if (ev.ctrlKey) {
return;
}
ev.preventDefault();
ev.stopPropagation();
if (ev.type !== 'mousedown') {
return;
}
new AttributeTranslateDialog(self, {}, ev.target).open();
});
},
/**
* Saves the translation and reloads the page to leave edit mode.
*
* @private
* @returns {Deferred} (never resolved as the page is reloading anyway)
*/
_save: function () {
return this.rte.save(weContext.get({lang: this.lang})).then(function () {
window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
return $.Deferred();
});
},
/**
* @private
*/
_unmarkTranslatableNode: function () {
this.$target.removeClass('o_editable').removeAttr('contentEditable');
this.$target.parent().off('.translator');
this.$target_attr.off('.translator');
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the "cancel" button is clicked -> undo changes and leaves
* edition.
*
* @private
*/
_onCancelClick: function () {
this._cancel();
},
/**
* Called when text is edited -> make sure text is not messed up and mark
* the element as dirty.
*
* @private
* @param {FlectraEvent} ev
*/
_onRTEChange: function (ev) {
var $node = $(ev.data.target);
$node.find('p').each(function () { // remove <p/> element which might have been inserted because of copy-paste
var $p = $(this);
$p.after($p.html()).remove();
});
var trans = this._getTranlationObject($node[0]);
$node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
},
/**
* Called when the "save" button is clicked -> saves the translations.
*
* @private
*/
_onSaveClick: function () {
this._save();
},
});
return {
Class: TranslatorMenuBar,
};
});