diff options
| author | Christophe Matthieu <chm@odoo.com> | 2015-07-07 16:02:53 +0200 |
|---|---|---|
| committer | Christophe Matthieu <chm@odoo.com> | 2015-07-10 17:00:11 +0200 |
| commit | 8c559044a5e1be111dcb577ad7e5ab462c1002a0 (patch) | |
| tree | ba1602e15b2bca38babd6cca63a41da1d05b4321 /addons/web_editor/static/src/js/rte.js | |
| parent | a616c57c609da0e5e56835dcb45e1c05590d25b8 (diff) | |
[IMP] web_editor: split website into web_editor (to use editor in backend for html field) and website
Diffstat (limited to 'addons/web_editor/static/src/js/rte.js')
| -rw-r--r-- | addons/web_editor/static/src/js/rte.js | 597 |
1 files changed, 597 insertions, 0 deletions
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; + +}); |
