summaryrefslogtreecommitdiff
path: root/addons/website/static/src/js
diff options
context:
space:
mode:
authorAntony Lesuisse <al@openerp.com>2014-01-31 01:51:57 +0100
committerAntony Lesuisse <al@openerp.com>2014-01-31 01:51:57 +0100
commit4b30f1fcbd8701f82dbbe4e5c8386ee432b063ac (patch)
tree93b17a03b45927c20fbb045cda8c8aa09d853409 /addons/website/static/src/js
parentba2d4c90307661a79d78e6ce3b274b76b4c62717 (diff)
parent1c283ddf242f0681c1d060f3b8658efa3fcaa0a8 (diff)
[MERGE] trunk-website-al
bzr revid: al@openerp.com-20140131005157-jkf8hz6fh2buml2c
Diffstat (limited to 'addons/website/static/src/js')
-rw-r--r--addons/website/static/src/js/website.ace.js302
-rw-r--r--addons/website/static/src/js/website.backend.js32
-rw-r--r--addons/website/static/src/js/website.editor.js1692
-rw-r--r--addons/website/static/src/js/website.editor.newpage.js29
-rw-r--r--addons/website/static/src/js/website.js294
-rw-r--r--addons/website/static/src/js/website.menu.js199
-rw-r--r--addons/website/static/src/js/website.mobile.js28
-rw-r--r--addons/website/static/src/js/website.seo.js527
-rw-r--r--addons/website/static/src/js/website.snippets.animation.js122
-rw-r--r--addons/website/static/src/js/website.snippets.editor.js1443
-rw-r--r--addons/website/static/src/js/website.tests.js40
-rw-r--r--addons/website/static/src/js/website.tour.banner.js118
-rw-r--r--addons/website/static/src/js/website.tour.js439
-rw-r--r--addons/website/static/src/js/website.translator.js209
14 files changed, 5474 insertions, 0 deletions
diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js
new file mode 100644
index 00000000000..514969fc4c2
--- /dev/null
+++ b/addons/website/static/src/js/website.ace.js
@@ -0,0 +1,302 @@
+(function () {
+ 'use strict';
+
+ var hash = "#advanced-view-editor";
+
+ var website = openerp.website;
+ website.add_template_file('/website/static/src/xml/website.ace.xml');
+
+ website.EditorBar.include({
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click a[data-action=ace]': 'launchAce',
+ }),
+ start: function () {
+ var self = this;
+ this.globalEditor = null;
+ return this._super.apply(this, arguments).then(function () {
+ if (window.location.hash.indexOf(hash) >= 0) {
+ self.launchAce();
+ }
+ });
+ },
+ launchAce: function (e) {
+ if (e) {
+ e.preventDefault();
+ }
+ if (this.globalEditor) {
+ this.globalEditor.open();
+ } else {
+ this.globalEditor = new website.ace.ViewEditor(this);
+ this.globalEditor.appendTo($(document.body));
+ }
+ },
+ });
+
+ website.ace = {};
+
+ website.ace.XmlDocument = openerp.Class.extend({
+ init: function (text) {
+ this.xml = text;
+ },
+ isWellFormed: function () {
+ if (document.implementation.createDocument) {
+ var dom = new DOMParser().parseFromString(this.xml, "text/xml");
+ return dom.getElementsByTagName("parsererror").length === 0;
+ } else if (window.ActiveXObject) {
+ // TODO test in IE
+ var msDom = new ActiveXObject("Microsoft.XMLDOM");
+ msDom.async = false;
+ return !msDom.loadXML(this.xml);
+ }
+ return true;
+ },
+ format: function () {
+ return vkbeautify.xml(this.xml, 4);
+ },
+ });
+
+ website.ace.ViewOption = openerp.Widget.extend({
+ template: 'website.ace_view_option',
+ init: function (parent, options) {
+ var indent = "- ";
+ this.view_id = options.id;
+ this.view_name = options.name;
+ for (var i = 0; i<options.level; i++) {
+ this.view_name = indent + this.view_name;
+ }
+ this._super(parent);
+ },
+ });
+
+ website.ace.ViewEditor = openerp.Widget.extend({
+ resizing: false,
+ refX: 0,
+ minWidth: 40,
+ template: 'website.ace_view_editor',
+ events: {
+ 'change #ace-view-list': 'displaySelectedView',
+ 'click button[data-action=save]': 'saveViews',
+ 'click button[data-action=format]': 'formatXml',
+ 'click button[data-action=close]': 'close',
+ },
+ init: function (parent) {
+ this.buffers = {};
+ this._super(parent);
+ },
+ start: function () {
+ var self = this;
+ self.aceEditor = ace.edit(self.$('#ace-view-editor')[0]);
+ self.aceEditor.setTheme("ace/theme/monokai");
+ var viewId = $(document.documentElement).data('view-xmlid');
+ openerp.jsonRpc('/website/customize_template_get', 'call', {
+ 'xml_id': viewId,
+ 'optional': false,
+ }).then(function (views) {
+ self.loadViews.call(self, views);
+ self.open.call(self);
+ var curentHash = window.location.hash;
+ var indexOfView = curentHash.indexOf("?view=");
+ if (indexOfView >= 0) {
+ var viewId = parseInt(curentHash.substring(indexOfView + 6, curentHash.length), 10);
+ self.$('#ace-view-list').val(viewId).change();
+ } else {
+ if (views.length >= 2) {
+ var mainTemplate = views[1];
+ self.$('#ace-view-list').val(mainTemplate.id).trigger('change');
+ }
+ window.location.hash = hash;
+ }
+ });
+
+ var $editor = self.$('.ace_editor');
+ function resizeEditor (target) {
+ var width = Math.min(document.body.clientWidth, Math.max(parseInt(target, 10), self.minWidth));
+ $editor.width(width);
+ self.aceEditor.resize();
+ self.$el.width(width);
+ }
+ function resizeEditorHeight(height) {
+ self.$el.css('top', height);
+ self.$('.ace_editor').css('bottom', height);
+ }
+ function storeEditorWidth() {
+ window.localStorage.setItem('ace_editor_width', self.$el.width());
+ }
+ function readEditorWidth() {
+ var width = window.localStorage.getItem('ace_editor_width');
+ return parseInt(width || 720, 10);
+ }
+ function startResizing (e) {
+ self.refX = e.pageX;
+ self.resizing = true;
+ }
+ function stopResizing () {
+ self.resizing = false;
+ }
+ function updateWidth (e) {
+ if (self.resizing) {
+ var offset = e.pageX - self.refX;
+ var width = self.$el.width() - offset;
+ self.refX = e.pageX;
+ resizeEditor(width);
+ storeEditorWidth();
+ }
+ }
+ document.body.addEventListener('mouseup', stopResizing, true);
+ self.$('.ace_gutter').mouseup(stopResizing).mousedown(startResizing).click(stopResizing);
+ $(document).mousemove(updateWidth);
+ $('button[data-action=edit]').click(function () {
+ self.close();
+ });
+ this.getParent().on('change:height', this, function (editor) {
+ resizeEditorHeight(editor.get('height'));
+ });
+ resizeEditor(readEditorWidth());
+ resizeEditorHeight(this.getParent().get('height'));
+ },
+ loadViews: function (views) {
+ var self = this;
+ var $viewList = self.$('#ace-view-list');
+ var views = this.buildViewGraph(views);
+ _.each(views, function (view) {
+ if (view.id) {
+ new website.ace.ViewOption(self, view).appendTo($viewList);
+ self.loadView(view.id);
+ }
+ });
+ },
+ buildViewGraph: function (views) {
+ var activeViews = _.uniq(_.filter(views, function (view) {
+ return view.active;
+ }), false, function (view) {
+ return view.id;
+ });
+ var index = {};
+ var roots = [];
+ _.each(activeViews, function (view) {
+ index[view.id] = view;
+ view.children = [];
+ });
+ _.each(index, function (view) {
+ var parentId = view.inherit_id;
+ if (parentId && index[parentId]) {
+ index[parentId].children.push(view);
+ } else {
+ roots.push(view);
+ }
+ });
+ var result = [];
+ function visit (node, level) {
+ node.level = level;
+ result.push(node);
+ _.each(node.children, function (child) {
+ visit(child, level + 1);
+ });
+ }
+ _.each(roots, function (node) {
+ visit(node, 0);
+ });
+ return result;
+ },
+ loadView: function (id) {
+ var viewId = parseInt(id, 10);
+ var self = this;
+ openerp.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.ui.view',
+ method: 'read',
+ args: [[viewId], ['arch'], website.get_context()],
+ }).then(function(result) {
+ var editingSession = self.buffers[viewId] = new ace.EditSession(result[0].arch);
+ editingSession.setMode("ace/mode/xml");
+ editingSession.setUndoManager(new ace.UndoManager());
+ editingSession.on("change", function () {
+ setTimeout(function () {
+ var $option = self.$('#ace-view-list').find('[value='+viewId+']');
+ var bufferName = $option.text();
+ var dirtyMarker = " (unsaved changes)";
+ var isDirty = editingSession.getUndoManager().hasUndo();
+ if (isDirty && bufferName.indexOf(dirtyMarker) < 0) {
+ $option.text(bufferName + dirtyMarker);
+ } else if (!isDirty && bufferName.indexOf(dirtyMarker) > 0) {
+ $option.text(bufferName.substring(0, bufferName.indexOf(dirtyMarker)));
+ }
+ }, 1);
+ });
+ if (viewId === self.selectedViewId()) {
+ self.displayView.call(self, viewId);
+ }
+ });
+ },
+ selectedViewId: function () {
+ return parseInt(this.$('#ace-view-list').val(), 10);
+ },
+ displayView: function (id) {
+ var viewId = parseInt(id, 10);
+ var editingSession = this.buffers[viewId];
+ if (editingSession) {
+ this.aceEditor.setSession(editingSession);
+ }
+ },
+ displaySelectedView: function () {
+ this.displayView(this.selectedViewId());
+ this.updateHash();
+ },
+ formatXml: function () {
+ var xml = new website.ace.XmlDocument(this.aceEditor.getValue());
+ this.aceEditor.setValue(xml.format());
+ },
+ saveViews: function () {
+ var self = this;
+ var toSave = _.filter(_.map(self.buffers, function (editingSession, viewId) {
+ return {
+ id: parseInt(viewId, 10),
+ isDirty: editingSession.getUndoManager().hasUndo(),
+ text: editingSession.getValue(),
+ };
+ }), function (session) {
+ return session.isDirty;
+ });
+ var requests = _.map(toSave, self.saveView);
+ $.when.apply($, requests).then(function () {
+ self.reloadPage.call(self);
+ }).fail(function (source, error) {
+ var message = _.isString(error) ? error
+ : (error && error.data && error.data.arguments && error.data.arguments[0] === "Access Denied") ? "Access denied: please sign in"
+ : (error && error.message) ? error.message
+ : "Unexpected error";
+ self.displayError.call(self, message);
+ });
+ },
+ saveView: function (session) {
+ var xml = new website.ace.XmlDocument(session.text);
+ if (xml.isWellFormed()) {
+ return openerp.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.ui.view',
+ method: 'write',
+ args: [[session.id], { 'arch': xml.xml }, website.get_context()],
+ });
+ } else {
+ return $.Deferred().reject(null, "Malformed XML document");
+ }
+ },
+ updateHash: function () {
+ window.location.hash = hash + "?view=" + this.selectedViewId();
+ },
+ reloadPage: function () {
+ this.updateHash();
+ window.location.reload();
+ },
+ displayError: function (error) {
+ // TODO Improve feedback (e.g. update 'Save' button + tooltip)
+ alert(error);
+ },
+ open: function () {
+ this.$el.removeClass('oe_ace_closed').addClass('oe_ace_open');
+ },
+ close: function () {
+ window.location.hash = "";
+ this.$el.removeClass('oe_ace_open').addClass('oe_ace_closed');
+ },
+ });
+
+})();
diff --git a/addons/website/static/src/js/website.backend.js b/addons/website/static/src/js/website.backend.js
new file mode 100644
index 00000000000..43347b85cb7
--- /dev/null
+++ b/addons/website/static/src/js/website.backend.js
@@ -0,0 +1,32 @@
+openerp.website = function(instance) {
+var _t = instance.web._t;
+
+instance.web.form.WidgetWebsiteButton = instance.web.form.AbstractField.extend({
+ template: 'WidgetWebsiteButton',
+ render_value: function() {
+ this._super();
+ this.$("button:first")
+ .toggleClass("btn-success", this.get_value())
+ .toggleClass("btn-danger", !this.get_value());
+ this.$("a:first").attr("href", this.view.datarecord.website_url || "/" );
+ if (this.node.attrs.class) {
+ this.$el.addClass(this.node.attrs.class);
+ }
+ },
+ start: function() {
+ var self = this;
+ this._super.apply(this, arguments);
+
+ this.$("button:first").on("click", function () {
+ console.log("click", !!$(this).hasClass("btn-danger"));
+ self.set_value(!!$(this).hasClass("btn-danger"));
+ return self.view.recursive_save();
+ });
+ },
+});
+
+instance.web.form.widgets = instance.web.form.widgets.extend({
+ 'website_button': 'instance.web.form.WidgetWebsiteButton',
+});
+
+};
diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js
new file mode 100644
index 00000000000..ab97ee2bcf9
--- /dev/null
+++ b/addons/website/static/src/js/website.editor.js
@@ -0,0 +1,1692 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ var _t = openerp._t;
+
+ website.add_template_file('/website/static/src/xml/website.editor.xml');
+ website.dom_ready.done(function () {
+ var is_smartphone = $(document.body)[0].clientWidth < 767;
+
+ if (!is_smartphone) {
+ website.ready().then(website.init_editor);
+ }
+
+ $(document).on('click', 'a.js_link2post', function (ev) {
+ ev.preventDefault();
+ website.form(this.pathname, 'POST');
+ });
+
+ $(document).on('submit', '.cke_editable form', 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();
+ }
+ });
+ });
+
+ /**
+ * An editing host is an HTML element with @contenteditable=true, or the
+ * child of a document in designMode=on (but that one's not supported)
+ *
+ * https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#editing-host
+ */
+ function is_editing_host(element) {
+ return element.getAttribute('contentEditable') === 'true';
+ }
+ /**
+ * Checks that both the element's content *and the element itself* are
+ * editable: an editing host is considered non-editable because its content
+ * is editable but its attributes should not be considered editable
+ */
+ function is_editable_node(element) {
+ return !(element.data('oe-model') === 'ir.ui.view'
+ || element.data('cke-realelement')
+ || (is_editing_host(element) && element.getAttribute('attributeEditable') !== 'true')
+ || element.isReadOnly());
+ }
+
+ function link_dialog(editor) {
+ return new website.editor.RTELinkDialog(editor).appendTo(document.body);
+ }
+ function image_dialog(editor, image) {
+ return new website.editor.RTEImageDialog(editor, image).appendTo(document.body);
+ }
+
+ // only enable editors manually
+ CKEDITOR.disableAutoInline = true;
+ // EDIT ALL THE THINGS
+ CKEDITOR.dtd.$editable = $.extend(
+ {}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline);
+ // Disable removal of empty elements on CKEDITOR activation. Empty
+ // elements are used for e.g. support of FontAwesome icons
+ CKEDITOR.dtd.$removeEmpty = {};
+
+ website.init_editor = function () {
+ CKEDITOR.plugins.add('customdialogs', {
+// requires: 'link,image',
+ init: function (editor) {
+ editor.on('doubleclick', function (evt) {
+ var element = evt.data.element;
+ if (element.is('img') && is_editable_node(element)) {
+ image_dialog(editor, element);
+ return;
+ }
+
+ element = get_selected_link(editor) || evt.data.element;
+ if (!(element.is('a') && is_editable_node(element))) {
+ return;
+ }
+
+ editor.getSelection().selectElement(element);
+ link_dialog(editor);
+ }, null, null, 500);
+
+ //noinspection JSValidateTypes
+ editor.addCommand('link', {
+ exec: function (editor) {
+ link_dialog(editor);
+ return true;
+ },
+ canUndo: false,
+ editorFocus: true,
+ });
+ //noinspection JSValidateTypes
+ editor.addCommand('image', {
+ exec: function (editor) {
+ image_dialog(editor);
+ return true;
+ },
+ canUndo: false,
+ editorFocus: true,
+ });
+
+ editor.ui.addButton('Link', {
+ label: 'Link',
+ command: 'link',
+ toolbar: 'links,10',
+ });
+ editor.ui.addButton('Image', {
+ label: 'Image',
+ command: 'image',
+ toolbar: 'insert,10',
+ });
+
+ editor.setKeystroke(CKEDITOR.CTRL + 76 /*L*/, 'link');
+ }
+ });
+ CKEDITOR.plugins.add( 'tablebutton', {
+ requires: 'panelbutton,floatpanel',
+ init: function( editor ) {
+ var label = "Table";
+
+ editor.ui.add('TableButton', CKEDITOR.UI_PANELBUTTON, {
+ label: label,
+ title: label,
+ // use existing 'table' icon
+ icon: 'table',
+ modes: { wysiwyg: true },
+ editorFocus: true,
+ // panel opens in iframe, @css is CSS file <link>-ed within
+ // frame document, @attributes are set on iframe itself.
+ panel: {
+ css: '/website/static/src/css/editor.css',
+ attributes: { 'role': 'listbox', 'aria-label': label, },
+ },
+
+ onBlock: function (panel, block) {
+ block.autoSize = true;
+ block.element.setHtml(openerp.qweb.render('website.editor.table.panel', {
+ rows: 5,
+ cols: 5,
+ }));
+
+ var $table = $(block.element.$).on('mouseenter', 'td', function (e) {
+ var $e = $(e.target);
+ var y = $e.index() + 1;
+ var x = $e.closest('tr').index() + 1;
+
+ $table
+ .find('td').removeClass('selected').end()
+ .find('tr:lt(' + String(x) + ')')
+ .children().filter(function () { return $(this).index() < y; })
+ .addClass('selected');
+ }).on('click', 'td', function (e) {
+ var $e = $(e.target);
+
+ //noinspection JSPotentiallyInvalidConstructorUsage
+ var table = new CKEDITOR.dom.element(
+ $(openerp.qweb.render('website.editor.table', {
+ rows: $e.closest('tr').index() + 1,
+ cols: $e.index() + 1,
+ }))[0]);
+
+ editor.insertElement(table);
+ setTimeout(function () {
+ //noinspection JSPotentiallyInvalidConstructorUsage
+ var firstCell = new CKEDITOR.dom.element(table.$.rows[0].cells[0]);
+ var range = editor.createRange();
+ range.moveToPosition(firstCell, CKEDITOR.POSITION_AFTER_START);
+ range.select();
+ }, 0);
+ });
+
+ block.element.getDocument().getBody().setStyle('overflow', 'hidden');
+ CKEDITOR.ui.fire('ready', this);
+ },
+ });
+ }
+ });
+
+ CKEDITOR.plugins.add('linkstyle', {
+ requires: 'panelbutton,floatpanel',
+ init: function (editor) {
+ var label = "Link Style";
+
+ editor.ui.add('LinkStyle', CKEDITOR.UI_PANELBUTTON, {
+ label: label,
+ title: label,
+ icon: '/website/static/src/img/bglink.png',
+ modes: { wysiwyg: true },
+ editorFocus: true,
+ panel: {
+ css: '/website/static/lib/bootstrap/css/bootstrap.css',
+ attributes: { 'role': 'listbox', 'aria-label': label },
+ },
+
+ types: {
+ 'btn-default': _t("Basic"),
+ 'btn-primary': _t("Primary"),
+ 'btn-success': _t("Success"),
+ 'btn-info': _t("Info"),
+ 'btn-warning': _t("Warning"),
+ 'btn-danger': _t("Danger"),
+ },
+ sizes: {
+ 'btn-xs': _t("Extra Small"),
+ 'btn-sm': _t("Small"),
+ '': _t("Default"),
+ 'btn-lg': _t("Large")
+ },
+
+ onRender: function () {
+ var self = this;
+ editor.on('selectionChange', function (e) {
+ var path = e.data.path, el;
+
+ if (!(e = path.contains('a')) || e.isReadOnly()) {
+ self.disable();
+ return;
+ }
+
+ self.enable();
+ });
+ // no hook where button is available, so wait
+ // "some time" after render.
+ setTimeout(function () {
+ self.disable();
+ }, 0)
+ },
+ enable: function () {
+ this.setState(CKEDITOR.TRISTATE_OFF);
+ },
+ disable: function () {
+ this.setState(CKEDITOR.TRISTATE_DISABLED);
+ },
+
+ onOpen: function () {
+ var link = get_selected_link(editor);
+ var id = this._.id;
+ var block = this._.panel._.panel._.blocks[id];
+ var $root = $(block.element.$);
+ $root.find('button').removeClass('active').removeProp('disabled');
+
+ // enable buttons matching link state
+ for (var type in this.types) {
+ if (!this.types.hasOwnProperty(type)) { continue; }
+ if (!link.hasClass(type)) { continue; }
+
+ $root.find('button[data-type=types].' + type)
+ .addClass('active');
+ }
+ var found;
+ for (var size in this.sizes) {
+ if (!this.sizes.hasOwnProperty(size)) { continue; }
+ if (!size || !link.hasClass(size)) { continue; }
+ found = true;
+ $root.find('button[data-type=sizes].' + size)
+ .addClass('active');
+ }
+ if (!found && link.hasClass('btn')) {
+ $root.find('button[data-type="sizes"][data-set-class=""]')
+ .addClass('active');
+ }
+ },
+
+ onBlock: function (panel, block) {
+ var self = this;
+ block.autoSize = true;
+
+ var html = ['<div style="padding: 5px">'];
+ html.push('<div style="white-space: nowrap">');
+ _(this.types).each(function (label, key) {
+ html.push(_.str.sprintf(
+ '<button type="button" class="btn %s" ' +
+ 'data-type="types" data-set-class="%s">%s</button>',
+ key, key, label));
+ });
+ html.push('</div>');
+ html.push('<div style="white-space: nowrap; margin: 5px 0; text-align: center">');
+ _(this.sizes).each(function (label, key) {
+ html.push(_.str.sprintf(
+ '<button type="button" class="btn btn-default %s" ' +
+ 'data-type="sizes" data-set-class="%s">%s</button>',
+ key, key, label));
+ });
+ html.push('</div>');
+ html.push('<button type="button" class="btn btn-link btn-block" ' +
+ 'data-type="reset">Reset</button>');
+ html.push('</div>');
+
+ block.element.setHtml(html.join(' '));
+ var $panel = $(block.element.$);
+ $panel.on('click', 'button', function () {
+ self.clicked(this);
+ });
+ },
+ clicked: function (button) {
+ editor.focus();
+ editor.fire('saveSnapshot');
+
+ var $button = $(button),
+ $link = $(get_selected_link(editor).$);
+ if (!$link.hasClass('btn')) {
+ $link.addClass('btn btn-default');
+ }
+ switch($button.data('type')) {
+ case 'reset':
+ $link.removeClass('btn')
+ .removeClass(_.keys(this.types).join(' '))
+ .removeClass(_.keys(this.sizes).join(' '));
+ break;
+ case 'types':
+ $link.removeClass(_.keys(this.types).join(' '))
+ .addClass($button.data('set-class'));
+ break;
+ case 'sizes':
+ $link.removeClass(_.keys(this.sizes).join(' '))
+ .addClass($button.data('set-class'));
+ }
+ this._.panel.hide();
+
+ editor.fire('saveSnapshot');
+ },
+
+ });
+ }
+ });
+
+ CKEDITOR.plugins.add('oeref', {
+ requires: 'widget',
+
+ init: function (editor) {
+ editor.widgets.add('oeref', {
+ editables: { text: '*' },
+ draggable: false,
+
+ upcast: function (el) {
+ var matches = el.attributes['data-oe-type'] && el.attributes['data-oe-type'] !== 'monetary';
+ if (!matches) { return false; }
+
+ if (el.attributes['data-oe-original']) {
+ while (el.children.length) {
+ el.children[0].remove();
+ }
+ el.add(new CKEDITOR.htmlParser.text(
+ el.attributes['data-oe-original']
+ ));
+ }
+ return true;
+ },
+ });
+ editor.widgets.add('monetary', {
+ editables: { text: 'span.oe_currency_value' },
+ draggable: false,
+
+ upcast: function (el) {
+ return el.attributes['data-oe-type'] === 'monetary';
+ }
+ });
+ editor.widgets.add('icons', {
+ draggable: false,
+
+ init: function () {
+ this.on('edit', function () {
+ new website.editor.FontIconsDialog(editor, this.element.$)
+ .appendTo(document.body);
+ });
+ },
+ upcast: function (el) {
+ return el.attributes['class']
+ && (/\bfa\b/.test(el.attributes['class']));
+ }
+ });
+ }
+ });
+
+ var editor = new website.EditorBar();
+ var $body = $(document.body);
+ editor.prependTo($body).then(function () {
+ if (location.search.indexOf("enable_editor") >= 0) {
+ editor.edit();
+ }
+ });
+ };
+
+ /* ----- TOP EDITOR BAR FOR ADMIN ---- */
+ website.EditorBar = openerp.Widget.extend({
+ template: 'website.editorbar',
+ events: {
+ 'click button[data-action=edit]': 'edit',
+ 'click button[data-action=save]': 'save',
+ 'click a[data-action=cancel]': 'cancel',
+ },
+ container: 'body',
+ customize_setup: function() {
+ var self = this;
+ var view_name = $(document.documentElement).data('view-xmlid');
+ if (!view_name) {
+ this.$('#customize-menu-button').addClass("hidden");
+ }
+ var menu = $('#customize-menu');
+ this.$('#customize-menu-button').click(function(event) {
+ menu.empty();
+ openerp.jsonRpc('/website/customize_template_get', 'call', { 'xml_id': view_name }).then(
+ function(result) {
+ _.each(result, function (item) {
+ if (item.header) {
+ menu.append('<li class="dropdown-header">' + item.name + '</li>');
+ } else {
+ menu.append(_.str.sprintf('<li role="presentation"><a href="#" data-view-id="%s" role="menuitem"><strong class="fa fa%s-square-o"></strong> %s</a></li>',
+ item.id, item.active ? '-check' : '', item.name));
+ }
+ });
+ // Adding Static Menus
+ menu.append('<li class="divider"></li>');
+ menu.append('<li><a data-action="ace" href="#">HTML Editor</a></li>');
+ menu.append('<li class="js_change_theme"><a href="/page/website.themes">Change Theme</a></li>');
+ menu.append('<li><a href="/web#return_label=Website&action=website.action_module_website">Install Apps</a></li>');
+ self.trigger('rte:customize_menu_ready');
+ }
+ );
+ });
+ menu.on('click', 'a[data-action!=ace]', function (event) {
+ var view_id = $(event.currentTarget).data('view-id');
+ openerp.jsonRpc('/website/customize_template_toggle', 'call', {
+ 'view_id': view_id
+ }).then( function() {
+ window.location.reload();
+ });
+ });
+ },
+ start: function() {
+ var self = this;
+ this.saving_mutex = new openerp.Mutex();
+
+ this.$('#website-top-edit').hide();
+ this.$('#website-top-view').show();
+
+ $('.dropdown-toggle').dropdown();
+ this.customize_setup();
+
+ this.$buttons = {
+ edit: this.$('button[data-action=edit]'),
+ save: this.$('button[data-action=save]'),
+ cancel: this.$('button[data-action=cancel]'),
+ };
+
+ this.rte = new website.RTE(this);
+ this.rte.on('change', this, this.proxy('rte_changed'));
+ this.rte.on('rte:ready', this, function () {
+ self.setup_hover_buttons();
+ self.trigger('rte:ready');
+ self.check_height();
+ });
+
+ $(window).on('resize', _.debounce(this.check_height.bind(this), 50));
+ this.check_height();
+
+ if (website.is_editable_button) {
+ this.$("button[data-action=edit]").removeClass("hidden");
+ }
+
+ return $.when(
+ this._super.apply(this, arguments),
+ this.rte.appendTo(this.$('#website-top-edit .nav.pull-right'))
+ ).then(function () {
+ self.check_height();
+ });
+ },
+ check_height: function () {
+ var editor_height = this.$el.outerHeight();
+ if (this.get('height') != editor_height) {
+ $(document.body).css('padding-top', editor_height);
+ this.set('height', editor_height);
+ }
+ },
+ edit: function () {
+ this.$buttons.edit.prop('disabled', true);
+ this.$('#website-top-view').hide();
+ this.$('#website-top-edit').show();
+ $('.css_non_editable_mode_hidden').removeClass("css_non_editable_mode_hidden");
+
+ this.rte.start_edition().then(this.check_height.bind(this));
+ this.trigger('rte:called');
+ },
+ rte_changed: function () {
+ this.$buttons.save.prop('disabled', false);
+ },
+ save: function () {
+ var self = this;
+
+ observer.disconnect();
+ var editor = this.rte.editor;
+ var root = editor.element.$;
+ editor.destroy();
+ // FIXME: select editables then filter by dirty?
+ var defs = this.rte.fetch_editables(root)
+ .filter('.oe_dirty')
+ .removeAttr('contentEditable')
+ .removeClass('oe_dirty oe_editable cke_focus oe_carlos_danger')
+ .map(function () {
+ var $el = $(this);
+ // 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('oe_dirty oe_carlos_danger');
+ $el.addClass(id);
+ return $.Deferred().reject({
+ id: id,
+ error: response.data,
+ });
+ });
+ });
+ }).get();
+ return $.when.apply(null, defs).then(function () {
+ website.reload();
+ }, function (failed) {
+ // If there were errors, re-enable edition
+ self.rte.start_edition(true).then(function () {
+ // 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><br/>' + 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');
+ });
+ });
+ });
+ },
+ /**
+ * Saves an RTE content, which always corresponds to a view section (?).
+ */
+ saveElement: function ($el) {
+ var markup = $el.prop('outerHTML');
+ return openerp.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 = $(openerp.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 () {
+ website.reload();
+ })
+ },
+
+ /**
+ * Creates a "hover" button for image and link edition
+ *
+ * @param {String} label the button's label
+ * @param {Function} editfn edition function, called when clicking the button
+ * @param {String} [classes] additional classes to set on the button
+ * @returns {jQuery}
+ */
+ make_hover_button: function (label, editfn, classes) {
+ return $(openerp.qweb.render('website.editor.hoverbutton', {
+ label: label,
+ classes: classes,
+ })).hide().appendTo(document.body).click(function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ editfn.call(this, e);
+ });
+ },
+ /**
+ * For UI clarity, during RTE edition when the user hovers links and
+ * images a small button should appear to make the capability clear,
+ * as not all users think of double-clicking the image or link.
+ */
+ setup_hover_buttons: function () {
+ var editor = this.rte.editor;
+ var $link_button = this.make_hover_button(_t("Change"), function () {
+ var sel = new CKEDITOR.dom.element(previous);
+ editor.getSelection().selectElement(sel);
+ if (previous.tagName.toUpperCase() === 'A') {
+ link_dialog(editor);
+ } else if(sel.hasClass('fa')) {
+ new website.editor.FontIconsDialog(editor, previous)
+ .appendTo(document.body);
+ }
+ $link_button.hide();
+ previous = null;
+ }, 'btn-xs');
+ var $image_button = this.make_hover_button(_t("Change"), function () {
+ image_dialog(editor, new CKEDITOR.dom.element(previous));
+ $image_button.hide();
+ previous = null;
+ });
+
+ // previous is the state of the button-trigger: it's the
+ // currently-ish hovered element which can trigger a button showing.
+ // -ish, because when moving to the button itself ``previous`` is
+ // still set to the element having triggered showing the button.
+ var previous;
+ $(editor.element.$).on('mouseover', 'a, img, .fa', function () {
+ // Back from edit button -> ignore
+ if (previous && previous === this) { return; }
+
+ var selected = new CKEDITOR.dom.element(this);
+ if (!is_editable_node(selected) && !selected.hasClass('fa')) {
+ return;
+ }
+
+ previous = this;
+ var $selected = $(this);
+ var position = $selected.offset();
+ if ($selected.is('img')) {
+ $link_button.hide();
+ // center button on image
+ $image_button.show().offset({
+ top: $selected.outerHeight() / 2
+ + position.top
+ - $image_button.outerHeight() / 2,
+ left: $selected.outerWidth() / 2
+ + position.left
+ - $image_button.outerWidth() / 2,
+ });
+ } else {
+ $image_button.hide();
+ // put button below link, horizontally centered
+ $link_button.show().offset({
+ top: $selected.outerHeight()
+ + position.top,
+ left: $selected.outerWidth() / 2
+ + position.left
+ - $link_button.outerWidth() / 2
+ })
+ }
+ }).on('mouseleave', 'a, img, .fa', function (e) {
+ var current = document.elementFromPoint(e.clientX, e.clientY);
+ if (current === $link_button[0] || current === $image_button[0]) {
+ return;
+ }
+ $image_button.add($link_button).hide();
+ previous = null;
+ });
+ }
+ });
+
+ var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(',');
+ /* ----- RICH TEXT EDITOR ---- */
+ website.RTE = openerp.Widget.extend({
+ tagName: 'li',
+ id: 'oe_rte_toolbar',
+ className: 'oe_right oe_rte_toolbar',
+ // editor.ui.items -> possible commands &al
+ // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'}));
+
+ init: function (EditorBar) {
+ this.EditorBar = EditorBar;
+ this._super.apply(this, arguments);
+ },
+
+ /**
+ * In Webkit-based browsers, triple-click will select a paragraph up to
+ * the start of the next "paragraph" including any empty space
+ * inbetween. When said paragraph is removed or altered, it nukes
+ * the empty space and brings part of the content of the next
+ * "paragraph" (which may well be e.g. an image) into the current one,
+ * completely fucking up layouts and breaking snippets.
+ *
+ * Try to fuck around with selections on triple-click to attempt to
+ * fix this garbage behavior.
+ *
+ * Note: for consistent behavior we may actually want to take over
+ * triple-clicks, in all browsers in order to ensure consistent cross-
+ * platform behavior instead of being at the mercy of rendering engines
+ * & platform selection quirks?
+ */
+ webkitSelectionFixer: function (root) {
+ root.addEventListener('click', function (e) {
+ // only webkit seems to have a fucked up behavior, ignore others
+ // FIXME: $.browser goes away in jquery 1.9...
+ if (!$.browser.webkit) { return; }
+ // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mouseevents
+ // The detail attribute indicates the number of times a mouse button has been pressed
+ // we just want the triple click
+ if (e.detail !== 3) { return; }
+ e.preventDefault();
+
+ // Get closest block-level element to the triple-clicked
+ // element (using ckeditor's block list because why not)
+ var $closest_block = $(e.target).closest(blocks_selector);
+
+ // manually set selection range to the content of the
+ // triple-clicked block-level element, to avoid crossing over
+ // between block-level elements
+ document.getSelection().selectAllChildren($closest_block[0]);
+ });
+ },
+ tableNavigation: function (root) {
+ var self = this;
+ $(root).on('keydown', function (e) {
+ // ignore non-TAB
+ if (e.which !== 9) { return; }
+
+ if (self.handleTab(e)) {
+ e.preventDefault();
+ }
+ });
+ },
+ /**
+ * Performs whatever operation is necessary on a [TAB] hit, returns
+ * ``true`` if the event's default should be cancelled (if the TAB was
+ * handled by the function)
+ */
+ handleTab: function (event) {
+ var forward = !event.shiftKey;
+
+ var root = window.getSelection().getRangeAt(0).commonAncestorContainer;
+ var $cell = $(root).closest('td,th');
+
+ if (!$cell.length) { return false; }
+
+ var cell = $cell[0];
+
+ // find cell in same row
+ var row = cell.parentNode;
+ var sibling = row.cells[cell.cellIndex + (forward ? 1 : -1)];
+ if (sibling) {
+ document.getSelection().selectAllChildren(sibling);
+ return true;
+ }
+
+ // find cell in previous/next row
+ var table = row.parentNode;
+ var sibling_row = table.rows[row.rowIndex + (forward ? 1 : -1)];
+ if (sibling_row) {
+ var new_cell = sibling_row.cells[forward ? 0 : sibling_row.cells.length - 1];
+ document.getSelection().selectAllChildren(new_cell);
+ return true;
+ }
+
+ // at edge cells, copy word/openoffice behavior: if going backwards
+ // from first cell do nothing, if going forwards from last cell add
+ // a row
+ if (forward) {
+ var row_size = row.cells.length;
+ var new_row = document.createElement('tr');
+ while(row_size--) {
+ var newcell = document.createElement('td');
+ // zero-width space
+ newcell.textContent = '\u200B';
+ new_row.appendChild(newcell);
+ }
+ table.appendChild(new_row);
+ document.getSelection().selectAllChildren(new_row.cells[0]);
+ }
+
+ return true;
+ },
+ /**
+ * 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;
+ // create a single editor for the whole page
+ var root = document.getElementById('wrapwrap');
+ if (!restart) {
+ $(root).on('dragstart', 'img', function (e) {
+ e.preventDefault();
+ });
+ this.webkitSelectionFixer(root);
+ this.tableNavigation(root);
+ }
+ var def = $.Deferred();
+ var editor = this.editor = CKEDITOR.inline(root, self._config());
+ editor.on('instanceReady', function () {
+ editor.setReadOnly(false);
+ // ckeditor set root to editable, disable it (only inner
+ // sections are editable)
+ // FIXME: are there cases where the whole editor is editable?
+ editor.editable().setReadOnly(true);
+
+ self.setup_editables(root);
+
+ try {
+ // disable firefox's broken table resizing thing
+ document.execCommand("enableObjectResizing", false, "false");
+ document.execCommand("enableInlineTableEditing", false, "false");
+ } catch (e) {}
+
+
+ // detect & setup any CKEDITOR widget within a newly dropped
+ // snippet. There does not seem to be a simple way to do it for
+ // HTML not inserted via ckeditor APIs:
+ // https://dev.ckeditor.com/ticket/11472
+ $(document.body)
+ .off('snippet-dropped')
+ .on('snippet-dropped', function (e, el) {
+ // CKEDITOR data processor extended by widgets plugin
+ // to add wrappers around upcasting elements
+ el.innerHTML = editor.dataProcessor.toHtml(el.innerHTML, {
+ fixForBody: false,
+ dontFilter: true,
+ });
+ // then repository.initOnAll() handles the conversion
+ // from wrapper to actual widget instance (or something
+ // like that).
+ setTimeout(function () {
+ editor.widgets.initOnAll();
+ }, 0);
+ });
+
+ self.trigger('rte:ready');
+ def.resolve();
+ });
+ return def;
+ },
+
+ setup_editables: function (root) {
+ // selection of editable sub-items was previously in
+ // EditorBar#edit, but for some unknown reason the elements were
+ // apparently removed and recreated (?) at editor initalization,
+ // and observer setup was lost.
+ var self = this;
+ // setup dirty-marking for each editable element
+ this.fetch_editables(root)
+ .addClass('oe_editable')
+ .each(function () {
+ var node = this;
+ var $node = $(node);
+ // only explicitly set contenteditable on view sections,
+ // cke widgets system will do the widgets themselves
+ if ($node.data('oe-model') === 'ir.ui.view') {
+ node.contentEditable = true;
+ }
+
+ observer.observe(node, OBSERVER_CONFIG);
+ $node.one('content_changed', function () {
+ $node.addClass('oe_dirty');
+ self.trigger('change');
+ });
+ });
+ },
+
+ fetch_editables: function (root) {
+ return $(root).find('[data-oe-model]')
+ .not('link, script')
+ .not('.oe_snippet_editor')
+ .filter(function () {
+ var $this = $(this);
+ // keep view sections and fields which are *not* in
+ // view sections for top-level editables
+ return $this.data('oe-model') === 'ir.ui.view'
+ || !$this.closest('[data-oe-model = "ir.ui.view"]').length;
+ });
+ },
+
+ _current_editor: function () {
+ return CKEDITOR.currentInstance;
+ },
+ _config: function () {
+ // base plugins minus
+ // - magicline (captures mousein/mouseout -> breaks draggable)
+ // - contextmenu & tabletools (disable contextual menu)
+ // - bunch of unused plugins
+ var plugins = [
+ 'a11yhelp', 'basicstyles', 'blockquote',
+ 'clipboard', 'colorbutton', 'colordialog', 'dialogadvtab',
+ 'elementspath', /*'enterkey',*/ 'entities', 'filebrowser',
+ 'find', 'floatingspace','format', 'htmlwriter', 'iframe',
+ 'indentblock', 'indentlist', 'justify',
+ 'list', 'pastefromword', 'pastetext', 'preview',
+ 'removeformat', 'resize', 'save', 'selectall', 'stylescombo',
+ 'table', 'templates', 'toolbar', 'undo', 'wysiwygarea'
+ ];
+ return {
+ // FIXME
+ language: 'en',
+ // Disable auto-generated titles
+ // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
+ title: false,
+ plugins: plugins.join(','),
+ uiColor: '',
+ // FIXME: currently breaks RTE?
+ // Ensure no config file is loaded
+ customConfig: '',
+ // Disable ACF
+ allowedContent: true,
+ // Don't insert paragraphs around content in e.g. <li>
+ autoParagraph: false,
+ // Don't automatically add &nbsp; or <br> in empty block-level
+ // elements when edition starts
+ fillEmptyBlocks: false,
+ filebrowserImageUploadUrl: "/website/attach",
+ // Support for sharedSpaces in 4.x
+ extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref,linkstyle',
+ // Place toolbar in controlled location
+ sharedSpaces: { top: 'oe_rte_toolbar' },
+ toolbar: [{
+ name: 'basicstyles', items: [
+ "Bold", "Italic", "Underline", "Strike", "Subscript",
+ "Superscript", "TextColor", "BGColor", "RemoveFormat"
+ ]},{
+ name: 'span', items: [
+ "Link", "LinkStyle", "Blockquote", "BulletedList",
+ "NumberedList", "Indent", "Outdent"
+ ]},{
+ name: 'justify', items: [
+ "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
+ ]},{
+ name: 'special', items: [
+ "Image", "TableButton"
+ ]},{
+ name: 'styles', items: [
+ "Styles"
+ ]}
+ ],
+ // styles dropdown in toolbar
+ stylesSet: [
+ {name: "Normal", element: 'p'},
+ {name: "Heading 1", element: 'h1'},
+ {name: "Heading 2", element: 'h2'},
+ {name: "Heading 3", element: 'h3'},
+ {name: "Heading 4", element: 'h4'},
+ {name: "Heading 5", element: 'h5'},
+ {name: "Heading 6", element: 'h6'},
+ {name: "Formatted", element: 'pre'},
+ {name: "Address", element: 'address'}
+ ],
+ };
+ },
+ });
+
+ website.editor = { };
+ website.editor.Dialog = openerp.Widget.extend({
+ events: {
+ 'hidden.bs.modal': 'destroy',
+ 'click button.save': 'save',
+ 'click button[data-dismiss="modal"]': 'cancel',
+ },
+ init: function (editor) {
+ this._super();
+ this.editor = editor;
+ },
+ start: function () {
+ var sup = this._super();
+ this.$el.modal({backdrop: 'static'});
+ return sup;
+ },
+ save: function () {
+ this.close();
+ },
+ cancel: function () {
+ },
+ close: function () {
+ this.$el.modal('hide');
+ },
+ });
+
+ website.editor.LinkDialog = website.editor.Dialog.extend({
+ template: 'website.editor.dialog.link',
+ events: _.extend({}, website.editor.Dialog.prototype.events, {
+ 'change :input.url-source': function (e) { this.changed($(e.target)); },
+ 'mousedown': function (e) {
+ var $target = $(e.target).closest('.list-group-item');
+ if (!$target.length || $target.hasClass('active')) {
+ // clicked outside groups, or clicked in active groups
+ return;
+ }
+
+ this.changed($target.find('.url-source').filter(':input'));
+ },
+ 'click button.remove': 'remove_link',
+ 'change input#link-text': function (e) {
+ this.text = $(e.target).val()
+ },
+ }),
+ init: function (editor) {
+ this._super(editor);
+ this.text = null;
+ // Store last-performed request to be able to cancel/abort it.
+ this.page_exists_req = null;
+ this.search_pages_req = null;
+ },
+ start: function () {
+ var self = this;
+ this.$('#link-page').select2({
+ minimumInputLength: 1,
+ placeholder: _t("New or existing page"),
+ query: function (q) {
+ $.when(
+ self.page_exists(q.term),
+ self.fetch_pages(q.term)
+ ).then(function (exists, results) {
+ var rs = _.map(results, function (r) {
+ return { id: r.url, text: r.name, };
+ });
+ if (!exists) {
+ rs.push({
+ create: true,
+ id: q.term,
+ text: _.str.sprintf(_t("Create page '%s'"), q.term),
+ });
+ }
+ q.callback({
+ more: false,
+ results: rs
+ });
+ }, function () {
+ q.callback({more: false, results: []});
+ });
+ },
+ });
+ return this._super().then(this.proxy('bind_data'));
+ },
+ save: function () {
+ var self = this, _super = this._super.bind(this);
+ var $e = this.$('.list-group-item.active .url-source').filter(':input');
+ var val = $e.val();
+ if (!val || !$e[0].checkValidity()) {
+ // FIXME: error message
+ $e.closest('.form-group').addClass('has-error');
+ $e.focus();
+ return;
+ }
+
+ var done = $.when();
+ if ($e.hasClass('email-address')) {
+ this.make_link('mailto:' + val, false, val);
+ } else if ($e.hasClass('page')) {
+ var data = $e.select2('data');
+ if (!data.create) {
+ self.make_link(data.id, false, data.text);
+ } else {
+ // Create the page, get the URL back
+ done = $.get(_.str.sprintf(
+ '/pagenew/%s?noredirect=1', encodeURI(data.id)))
+ .then(function (response) {
+ self.make_link(response, false, data.id);
+ });
+ }
+ } else {
+ this.make_link(val, this.$('input.window-new').prop('checked'));
+ }
+ done.then(_super);
+ },
+ make_link: function (url, new_window, label) {
+ },
+ bind_data: function (text, href, new_window) {
+ href = href || this.element && (this.element.data( 'cke-saved-href')
+ || this.element.getAttribute('href'));
+
+ if (new_window === undefined) {
+ new_window = this.element
+ ? this.element.getAttribute('target') === '_blank'
+ : false;
+ }
+ if (text === undefined) {
+ text = this.element ? this.element.getText() : '';
+ }
+
+ this.$('input#link-text').val(text);
+ this.$('input.window-new').prop('checked', new_window);
+
+ if (!href) { return; }
+ var match, $control;
+ if ((match = /mailto:(.+)/.exec(href))) {
+ $control = this.$('input.email-address').val(match[1]);
+ }
+ if (!$control) {
+ $control = this.$('input.url').val(href);
+ }
+
+ this.changed($control);
+ },
+ changed: function ($e) {
+ this.$('.url-source').filter(':input').not($e).val('')
+ .filter(function () { return !!$(this).data('select2'); })
+ .select2('data', null);
+ $e.closest('.list-group-item')
+ .addClass('active')
+ .siblings().removeClass('active')
+ .addBack().removeClass('has-error');
+ },
+ call: function (method, args, kwargs) {
+ var self = this;
+ var req = method + '_req';
+
+ if (this[req]) { this[req].abort(); }
+
+ return this[req] = openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'website',
+ method: method,
+ args: args,
+ kwargs: kwargs,
+ }).always(function () {
+ self[req] = null;
+ });
+ },
+ page_exists: function (term) {
+ return this.call('page_exists', [null, term], {
+ context: website.get_context(),
+ });
+ },
+ fetch_pages: function (term) {
+ return this.call('search_pages', [null, term], {
+ limit: 9,
+ context: website.get_context(),
+ });
+ },
+ });
+ website.editor.RTELinkDialog = website.editor.LinkDialog.extend({
+ start: function () {
+ var element;
+ if ((element = this.get_selected_link()) && element.hasAttribute('href')) {
+ this.editor.getSelection().selectElement(element);
+ }
+ this.element = element;
+ if (element) {
+ this.add_removal_button();
+ }
+
+ return this._super();
+ },
+ add_removal_button: function () {
+ this.$('.modal-footer').prepend(
+ openerp.qweb.render(
+ 'website.editor.dialog.link.footer-button'));
+ },
+ remove_link: function () {
+ var editor = this.editor;
+ // same issue as in make_link
+ setTimeout(function () {
+ editor.removeStyle(new CKEDITOR.style({
+ element: 'a',
+ type: CKEDITOR.STYLE_INLINE,
+ alwaysRemoveElement: true,
+ }));
+ }, 0);
+ this.close();
+ },
+ /**
+ * Greatly simplified version of CKEDITOR's
+ * plugins.link.dialogs.link.onOk.
+ *
+ * @param {String} url
+ * @param {Boolean} [new_window=false]
+ * @param {String} [label=null]
+ */
+ make_link: function (url, new_window, label) {
+ var attributes = {href: url, 'data-cke-saved-href': url};
+ var to_remove = [];
+ if (new_window) {
+ attributes['target'] = '_blank';
+ } else {
+ to_remove.push('target');
+ }
+
+ if (this.element) {
+ this.element.setAttributes(attributes);
+ this.element.removeAttributes(to_remove);
+ if (this.text) { this.element.setText(this.text); }
+ } else {
+ var selection = this.editor.getSelection();
+ var range = selection.getRanges(true)[0];
+
+ if (range.collapsed) {
+ //noinspection JSPotentiallyInvalidConstructorUsage
+ var text = new CKEDITOR.dom.text(
+ this.text || label || url);
+ range.insertNode(text);
+ range.selectNodeContents(text);
+ }
+
+ //noinspection JSPotentiallyInvalidConstructorUsage
+ new CKEDITOR.style({
+ type: CKEDITOR.STYLE_INLINE,
+ element: 'a',
+ attributes: attributes,
+ }).applyToRange(range);
+
+ // focus dance between RTE & dialog blow up the stack in Safari
+ // and Chrome, so defer select() until dialog has been closed
+ setTimeout(function () {
+ range.select();
+ }, 0);
+ }
+ },
+ /**
+ * CKEDITOR.plugins.link.getSelectedLink ignores the editor's root,
+ * if the editor is set directly on a link it will thus not work.
+ */
+ get_selected_link: function () {
+ return get_selected_link(this.editor);
+ },
+ });
+
+ /**
+ * ImageDialog widget. Lets users change an image, including uploading a
+ * new image in OpenERP or selecting the image style (if supported by
+ * the caller).
+ *
+ * Initialized as usual, but the caller can hook into two events:
+ *
+ * @event start({url, style}) called during dialog initialization and
+ * opening, the handler can *set* the ``url``
+ * and ``style`` properties on its parameter
+ * to provide these as default values to the
+ * dialog
+ * @event save({url, style}) called during dialog finalization, the handler
+ * is provided with the image url and style
+ * selected by the users (or possibly the ones
+ * originally passed in)
+ */
+ website.editor.ImageDialog = website.editor.Dialog.extend({
+ template: 'website.editor.dialog.image',
+ events: _.extend({}, website.editor.Dialog.prototype.events, {
+ 'change .url-source': function (e) { this.changed($(e.target)); },
+ 'click button.filepicker': function () {
+ this.$('input[type=file]').click();
+ },
+ 'change input[type=file]': 'file_selection',
+ 'change input.url': 'preview_image',
+ 'click a[href=#existing]': 'browse_existing',
+ 'change select.image-style': 'preview_image',
+ }),
+
+ start: function () {
+ this.$('.modal-footer [disabled]').text("Uploading…");
+ var $options = this.$('.image-style').children();
+ this.image_styles = $options.map(function () { return this.value; }).get();
+
+ var o = { url: null, style: null, };
+ // avoid typos, prevent addition of new properties to the object
+ Object.preventExtensions(o);
+ this.trigger('start', o);
+
+ if (o.url) {
+ if (o.style) {
+ this.$('.image-style').val(o.style);
+ }
+ this.set_image(o.url);
+ }
+
+ return this._super();
+ },
+ save: function () {
+ this.trigger('save', {
+ url: this.$('input.url').val(),
+ style: this.$('.image-style').val(),
+ });
+ return this._super();
+ },
+ cancel: function () {
+ this.trigger('cancel');
+ },
+
+ /**
+ * Sets the provided image url as the dialog's value-to-save and
+ * refreshes the preview element to use it.
+ */
+ set_image: function (url) {
+ this.$('input.url').val(url);
+ this.preview_image();
+ },
+
+ file_selection: function () {
+ this.$el.addClass('nosave');
+ this.$('button.filepicker').removeClass('btn-danger btn-success');
+
+ var self = this;
+ var callback = _.uniqueId('func_');
+ this.$('input[name=func]').val(callback);
+
+ window[callback] = function (url, error) {
+ delete window[callback];
+ self.file_selected(url, error);
+ };
+ this.$('form').submit();
+ },
+ file_selected: function(url, error) {
+ var $button = this.$('button.filepicker');
+ if (error) {
+ $button.addClass('btn-danger');
+ return;
+ }
+ $button.addClass('btn-success');
+ this.set_image(url);
+ },
+ preview_image: function () {
+ this.$el.removeClass('nosave');
+ var image = this.$('input.url').val();
+ if (!image) { return; }
+
+ this.$('img.image-preview')
+ .attr('src', image)
+ .removeClass(this.image_styles.join(' '))
+ .addClass(this.$('select.image-style').val());
+ },
+ browse_existing: function (e) {
+ e.preventDefault();
+ new website.editor.ExistingImageDialog(this).appendTo(document.body);
+ },
+ });
+ website.editor.RTEImageDialog = website.editor.ImageDialog.extend({
+ init: function (editor, image) {
+ this._super(editor);
+
+ this.element = image;
+
+ this.on('start', this, this.proxy('started'));
+ this.on('save', this, this.proxy('saved'));
+ },
+ started: function (holder) {
+ if (!this.element) {
+ var selection = this.editor.getSelection();
+ this.element = selection && selection.getSelectedElement();
+ }
+
+ var el = this.element;
+ if (!el || !el.is('img')) {
+ return;
+ }
+ _(this.image_styles).each(function (style) {
+ if (el.hasClass(style)) {
+ holder.style = style;
+ }
+ });
+ holder.url = el.getAttribute('src');
+ },
+ saved: function (data) {
+ var element, editor = this.editor;
+ if (!(element = this.element)) {
+ element = editor.document.createElement('img');
+ element.addClass('img');
+ element.addClass('img-responsive');
+ // focus event handler interactions between bootstrap (modal)
+ // and ckeditor (RTE) lead to blowing the stack in Safari and
+ // Chrome (but not FF) when this is done synchronously =>
+ // defer insertion so modal has been hidden & destroyed before
+ // it happens
+ setTimeout(function () {
+ editor.insertElement(element);
+ }, 0);
+ }
+
+ var style = data.style;
+ element.setAttribute('src', data.url);
+ element.removeAttribute('data-cke-saved-src');
+ $(element.$).removeClass(this.image_styles.join(' '));
+ if (style) { element.addClass(style); }
+ },
+ });
+
+ var IMAGES_PER_ROW = 6;
+ var IMAGES_ROWS = 4;
+ website.editor.ExistingImageDialog = website.editor.Dialog.extend({
+ template: 'website.editor.dialog.image.existing',
+ events: _.extend({}, website.editor.Dialog.prototype.events, {
+ 'click .existing-attachments img': 'select_existing',
+ 'click .pager > li': function (e) {
+ e.preventDefault();
+ var $target = $(e.currentTarget);
+ if ($target.hasClass('disabled')) {
+ return;
+ }
+ this.page += $target.hasClass('previous') ? -1 : 1;
+ this.display_attachments();
+ },
+ }),
+ init: function (parent) {
+ this.image = null;
+ this.page = 0;
+ this.parent = parent;
+ this._super(parent.editor);
+ },
+
+ start: function () {
+ return $.when(
+ this._super(),
+ this.fetch_existing().then(this.proxy('fetched_existing')));
+ },
+
+ fetch_existing: function () {
+ return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'ir.attachment',
+ method: 'search_read',
+ args: [],
+ kwargs: {
+ fields: ['name', 'website_url'],
+ domain: [['res_model', '=', 'ir.ui.view']],
+ order: 'name',
+ context: website.get_context(),
+ }
+ });
+ },
+ fetched_existing: function (records) {
+ this.records = records;
+ this.display_attachments();
+ },
+ display_attachments: function () {
+ 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(
+ openerp.qweb.render(
+ 'website.editor.dialog.image.existing.content', {rows: rows}));
+ this.$('.pager')
+ .find('li.previous').toggleClass('disabled', (from === 0)).end()
+ .find('li.next').toggleClass('disabled', (from + per_screen >= records.length));
+
+ },
+ select_existing: function (e) {
+ var link = $(e.currentTarget).attr('src');
+ if (link) {
+ this.parent.set_image(link);
+ }
+ this.close()
+ },
+ });
+
+ function get_selected_link(editor) {
+ var sel = editor.getSelection(),
+ el = sel.getSelectedElement();
+ if (el && el.is('a')) { return el; }
+
+ var range = sel.getRanges(true)[0];
+ if (!range) { return null; }
+
+ range.shrink(CKEDITOR.SHRINK_TEXT);
+ var commonAncestor = range.getCommonAncestor();
+ var viewRoot = editor.elementPath(commonAncestor).contains(function (element) {
+ return element.data('oe-model') === 'ir.ui.view'
+ });
+ if (!viewRoot) { return null; }
+ // if viewRoot is the first link, don't edit it.
+ return new CKEDITOR.dom.elementPath(commonAncestor, viewRoot)
+ .contains('a', true);
+ }
+
+ website.editor.FontIconsDialog = website.editor.Dialog.extend({
+ template: 'website.editor.dialog.font-icons',
+ events : _.extend({}, website.editor.Dialog.prototype.events, {
+ change: 'update_preview',
+ 'click .font-icons-icon': function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$('#fa-icon').val(e.target.getAttribute('data-id'));
+ this.update_preview();
+ },
+ 'click #fa-preview span': function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$('#fa-size').val(e.target.getAttribute('data-size'));
+ this.update_preview();
+ },
+ 'input input#icon-search': function () {
+ var needle = this.$('#icon-search').val();
+ var icons = this.icons;
+ if (needle) {
+ icons = _(icons).filter(function (icon) {
+ return icon.id.substring(3).indexOf(needle) !== -1;
+ });
+ }
+
+ this.$('div.font-icons-icons').html(
+ openerp.qweb.render(
+ 'website.editor.dialog.font-icons.icons',
+ {icons: icons}));
+ },
+ }),
+
+ // List of FontAwesome icons in 4.0.3, extracted from the cheatsheet.
+ // Each icon provides the unicode codepoint as ``text`` and the class
+ // name as ``id`` so the whole thing can be fed directly to select2
+ // without post-processing and do the right thing (except for the part
+ // where we still need to implement ``initSelection``)
+ // TODO: add id/name to the text in order to allow FAYT selection of icons?
+ icons: [{"text": "\uf000", "id": "fa-glass"}, {"text": "\uf001", "id": "fa-music"}, {"text": "\uf002", "id": "fa-search"}, {"text": "\uf003", "id": "fa-envelope-o"}, {"text": "\uf004", "id": "fa-heart"}, {"text": "\uf005", "id": "fa-star"}, {"text": "\uf006", "id": "fa-star-o"}, {"text": "\uf007", "id": "fa-user"}, {"text": "\uf008", "id": "fa-film"}, {"text": "\uf009", "id": "fa-th-large"}, {"text": "\uf00a", "id": "fa-th"}, {"text": "\uf00b", "id": "fa-th-list"}, {"text": "\uf00c", "id": "fa-check"}, {"text": "\uf00d", "id": "fa-times"}, {"text": "\uf00e", "id": "fa-search-plus"}, {"text": "\uf010", "id": "fa-search-minus"}, {"text": "\uf011", "id": "fa-power-off"}, {"text": "\uf012", "id": "fa-signal"}, {"text": "\uf013", "id": "fa-cog"}, {"text": "\uf014", "id": "fa-trash-o"}, {"text": "\uf015", "id": "fa-home"}, {"text": "\uf016", "id": "fa-file-o"}, {"text": "\uf017", "id": "fa-clock-o"}, {"text": "\uf018", "id": "fa-road"}, {"text": "\uf019", "id": "fa-download"}, {"text": "\uf01a", "id": "fa-arrow-circle-o-down"}, {"text": "\uf01b", "id": "fa-arrow-circle-o-up"}, {"text": "\uf01c", "id": "fa-inbox"}, {"text": "\uf01d", "id": "fa-play-circle-o"}, {"text": "\uf01e", "id": "fa-repeat"}, {"text": "\uf021", "id": "fa-refresh"}, {"text": "\uf022", "id": "fa-list-alt"}, {"text": "\uf023", "id": "fa-lock"}, {"text": "\uf024", "id": "fa-flag"}, {"text": "\uf025", "id": "fa-headphones"}, {"text": "\uf026", "id": "fa-volume-off"}, {"text": "\uf027", "id": "fa-volume-down"}, {"text": "\uf028", "id": "fa-volume-up"}, {"text": "\uf029", "id": "fa-qrcode"}, {"text": "\uf02a", "id": "fa-barcode"}, {"text": "\uf02b", "id": "fa-tag"}, {"text": "\uf02c", "id": "fa-tags"}, {"text": "\uf02d", "id": "fa-book"}, {"text": "\uf02e", "id": "fa-bookmark"}, {"text": "\uf02f", "id": "fa-print"}, {"text": "\uf030", "id": "fa-camera"}, {"text": "\uf031", "id": "fa-font"}, {"text": "\uf032", "id": "fa-bold"}, {"text": "\uf033", "id": "fa-italic"}, {"text": "\uf034", "id": "fa-text-height"}, {"text": "\uf035", "id": "fa-text-width"}, {"text": "\uf036", "id": "fa-align-left"}, {"text": "\uf037", "id": "fa-align-center"}, {"text": "\uf038", "id": "fa-align-right"}, {"text": "\uf039", "id": "fa-align-justify"}, {"text": "\uf03a", "id": "fa-list"}, {"text": "\uf03b", "id": "fa-outdent"}, {"text": "\uf03c", "id": "fa-indent"}, {"text": "\uf03d", "id": "fa-video-camera"}, {"text": "\uf03e", "id": "fa-picture-o"}, {"text": "\uf040", "id": "fa-pencil"}, {"text": "\uf041", "id": "fa-map-marker"}, {"text": "\uf042", "id": "fa-adjust"}, {"text": "\uf043", "id": "fa-tint"}, {"text": "\uf044", "id": "fa-pencil-square-o"}, {"text": "\uf045", "id": "fa-share-square-o"}, {"text": "\uf046", "id": "fa-check-square-o"}, {"text": "\uf047", "id": "fa-arrows"}, {"text": "\uf048", "id": "fa-step-backward"}, {"text": "\uf049", "id": "fa-fast-backward"}, {"text": "\uf04a", "id": "fa-backward"}, {"text": "\uf04b", "id": "fa-play"}, {"text": "\uf04c", "id": "fa-pause"}, {"text": "\uf04d", "id": "fa-stop"}, {"text": "\uf04e", "id": "fa-forward"}, {"text": "\uf050", "id": "fa-fast-forward"}, {"text": "\uf051", "id": "fa-step-forward"}, {"text": "\uf052", "id": "fa-eject"}, {"text": "\uf053", "id": "fa-chevron-left"}, {"text": "\uf054", "id": "fa-chevron-right"}, {"text": "\uf055", "id": "fa-plus-circle"}, {"text": "\uf056", "id": "fa-minus-circle"}, {"text": "\uf057", "id": "fa-times-circle"}, {"text": "\uf058", "id": "fa-check-circle"}, {"text": "\uf059", "id": "fa-question-circle"}, {"text": "\uf05a", "id": "fa-info-circle"}, {"text": "\uf05b", "id": "fa-crosshairs"}, {"text": "\uf05c", "id": "fa-times-circle-o"}, {"text": "\uf05d", "id": "fa-check-circle-o"}, {"text": "\uf05e", "id": "fa-ban"}, {"text": "\uf060", "id": "fa-arrow-left"}, {"text": "\uf061", "id": "fa-arrow-right"}, {"text": "\uf062", "id": "fa-arrow-up"}, {"text": "\uf063", "id": "fa-arrow-down"}, {"text": "\uf064", "id": "fa-share"}, {"text": "\uf065", "id": "fa-expand"}, {"text": "\uf066", "id": "fa-compress"}, {"text": "\uf067", "id": "fa-plus"}, {"text": "\uf068", "id": "fa-minus"}, {"text": "\uf069", "id": "fa-asterisk"}, {"text": "\uf06a", "id": "fa-exclamation-circle"}, {"text": "\uf06b", "id": "fa-gift"}, {"text": "\uf06c", "id": "fa-leaf"}, {"text": "\uf06d", "id": "fa-fire"}, {"text": "\uf06e", "id": "fa-eye"}, {"text": "\uf070", "id": "fa-eye-slash"}, {"text": "\uf071", "id": "fa-exclamation-triangle"}, {"text": "\uf072", "id": "fa-plane"}, {"text": "\uf073", "id": "fa-calendar"}, {"text": "\uf074", "id": "fa-random"}, {"text": "\uf075", "id": "fa-comment"}, {"text": "\uf076", "id": "fa-magnet"}, {"text": "\uf077", "id": "fa-chevron-up"}, {"text": "\uf078", "id": "fa-chevron-down"}, {"text": "\uf079", "id": "fa-retweet"}, {"text": "\uf07a", "id": "fa-shopping-cart"}, {"text": "\uf07b", "id": "fa-folder"}, {"text": "\uf07c", "id": "fa-folder-open"}, {"text": "\uf07d", "id": "fa-arrows-v"}, {"text": "\uf07e", "id": "fa-arrows-h"}, {"text": "\uf080", "id": "fa-bar-chart-o"}, {"text": "\uf081", "id": "fa-twitter-square"}, {"text": "\uf082", "id": "fa-facebook-square"}, {"text": "\uf083", "id": "fa-camera-retro"}, {"text": "\uf084", "id": "fa-key"}, {"text": "\uf085", "id": "fa-cogs"}, {"text": "\uf086", "id": "fa-comments"}, {"text": "\uf087", "id": "fa-thumbs-o-up"}, {"text": "\uf088", "id": "fa-thumbs-o-down"}, {"text": "\uf089", "id": "fa-star-half"}, {"text": "\uf08a", "id": "fa-heart-o"}, {"text": "\uf08b", "id": "fa-sign-out"}, {"text": "\uf08c", "id": "fa-linkedin-square"}, {"text": "\uf08d", "id": "fa-thumb-tack"}, {"text": "\uf08e", "id": "fa-external-link"}, {"text": "\uf090", "id": "fa-sign-in"}, {"text": "\uf091", "id": "fa-trophy"}, {"text": "\uf092", "id": "fa-github-square"}, {"text": "\uf093", "id": "fa-upload"}, {"text": "\uf094", "id": "fa-lemon-o"}, {"text": "\uf095", "id": "fa-phone"}, {"text": "\uf096", "id": "fa-square-o"}, {"text": "\uf097", "id": "fa-bookmark-o"}, {"text": "\uf098", "id": "fa-phone-square"}, {"text": "\uf099", "id": "fa-twitter"}, {"text": "\uf09a", "id": "fa-facebook"}, {"text": "\uf09b", "id": "fa-github"}, {"text": "\uf09c", "id": "fa-unlock"}, {"text": "\uf09d", "id": "fa-credit-card"}, {"text": "\uf09e", "id": "fa-rss"}, {"text": "\uf0a0", "id": "fa-hdd-o"}, {"text": "\uf0a1", "id": "fa-bullhorn"}, {"text": "\uf0f3", "id": "fa-bell"}, {"text": "\uf0a3", "id": "fa-certificate"}, {"text": "\uf0a4", "id": "fa-hand-o-right"}, {"text": "\uf0a5", "id": "fa-hand-o-left"}, {"text": "\uf0a6", "id": "fa-hand-o-up"}, {"text": "\uf0a7", "id": "fa-hand-o-down"}, {"text": "\uf0a8", "id": "fa-arrow-circle-left"}, {"text": "\uf0a9", "id": "fa-arrow-circle-right"}, {"text": "\uf0aa", "id": "fa-arrow-circle-up"}, {"text": "\uf0ab", "id": "fa-arrow-circle-down"}, {"text": "\uf0ac", "id": "fa-globe"}, {"text": "\uf0ad", "id": "fa-wrench"}, {"text": "\uf0ae", "id": "fa-tasks"}, {"text": "\uf0b0", "id": "fa-filter"}, {"text": "\uf0b1", "id": "fa-briefcase"}, {"text": "\uf0b2", "id": "fa-arrows-alt"}, {"text": "\uf0c0", "id": "fa-users"}, {"text": "\uf0c1", "id": "fa-link"}, {"text": "\uf0c2", "id": "fa-cloud"}, {"text": "\uf0c3", "id": "fa-flask"}, {"text": "\uf0c4", "id": "fa-scissors"}, {"text": "\uf0c5", "id": "fa-files-o"}, {"text": "\uf0c6", "id": "fa-paperclip"}, {"text": "\uf0c7", "id": "fa-floppy-o"}, {"text": "\uf0c8", "id": "fa-square"}, {"text": "\uf0c9", "id": "fa-bars"}, {"text": "\uf0ca", "id": "fa-list-ul"}, {"text": "\uf0cb", "id": "fa-list-ol"}, {"text": "\uf0cc", "id": "fa-strikethrough"}, {"text": "\uf0cd", "id": "fa-underline"}, {"text": "\uf0ce", "id": "fa-table"}, {"text": "\uf0d0", "id": "fa-magic"}, {"text": "\uf0d1", "id": "fa-truck"}, {"text": "\uf0d2", "id": "fa-pinterest"}, {"text": "\uf0d3", "id": "fa-pinterest-square"}, {"text": "\uf0d4", "id": "fa-google-plus-square"}, {"text": "\uf0d5", "id": "fa-google-plus"}, {"text": "\uf0d6", "id": "fa-money"}, {"text": "\uf0d7", "id": "fa-caret-down"}, {"text": "\uf0d8", "id": "fa-caret-up"}, {"text": "\uf0d9", "id": "fa-caret-left"}, {"text": "\uf0da", "id": "fa-caret-right"}, {"text": "\uf0db", "id": "fa-columns"}, {"text": "\uf0dc", "id": "fa-sort"}, {"text": "\uf0dd", "id": "fa-sort-asc"}, {"text": "\uf0de", "id": "fa-sort-desc"}, {"text": "\uf0e0", "id": "fa-envelope"}, {"text": "\uf0e1", "id": "fa-linkedin"}, {"text": "\uf0e2", "id": "fa-undo"}, {"text": "\uf0e3", "id": "fa-gavel"}, {"text": "\uf0e4", "id": "fa-tachometer"}, {"text": "\uf0e5", "id": "fa-comment-o"}, {"text": "\uf0e6", "id": "fa-comments-o"}, {"text": "\uf0e7", "id": "fa-bolt"}, {"text": "\uf0e8", "id": "fa-sitemap"}, {"text": "\uf0e9", "id": "fa-umbrella"}, {"text": "\uf0ea", "id": "fa-clipboard"}, {"text": "\uf0eb", "id": "fa-lightbulb-o"}, {"text": "\uf0ec", "id": "fa-exchange"}, {"text": "\uf0ed", "id": "fa-cloud-download"}, {"text": "\uf0ee", "id": "fa-cloud-upload"}, {"text": "\uf0f0", "id": "fa-user-md"}, {"text": "\uf0f1", "id": "fa-stethoscope"}, {"text": "\uf0f2", "id": "fa-suitcase"}, {"text": "\uf0a2", "id": "fa-bell-o"}, {"text": "\uf0f4", "id": "fa-coffee"}, {"text": "\uf0f5", "id": "fa-cutlery"}, {"text": "\uf0f6", "id": "fa-file-text-o"}, {"text": "\uf0f7", "id": "fa-building-o"}, {"text": "\uf0f8", "id": "fa-hospital-o"}, {"text": "\uf0f9", "id": "fa-ambulance"}, {"text": "\uf0fa", "id": "fa-medkit"}, {"text": "\uf0fb", "id": "fa-fighter-jet"}, {"text": "\uf0fc", "id": "fa-beer"}, {"text": "\uf0fd", "id": "fa-h-square"}, {"text": "\uf0fe", "id": "fa-plus-square"}, {"text": "\uf100", "id": "fa-angle-double-left"}, {"text": "\uf101", "id": "fa-angle-double-right"}, {"text": "\uf102", "id": "fa-angle-double-up"}, {"text": "\uf103", "id": "fa-angle-double-down"}, {"text": "\uf104", "id": "fa-angle-left"}, {"text": "\uf105", "id": "fa-angle-right"}, {"text": "\uf106", "id": "fa-angle-up"}, {"text": "\uf107", "id": "fa-angle-down"}, {"text": "\uf108", "id": "fa-desktop"}, {"text": "\uf109", "id": "fa-laptop"}, {"text": "\uf10a", "id": "fa-tablet"}, {"text": "\uf10b", "id": "fa-mobile"}, {"text": "\uf10c", "id": "fa-circle-o"}, {"text": "\uf10d", "id": "fa-quote-left"}, {"text": "\uf10e", "id": "fa-quote-right"}, {"text": "\uf110", "id": "fa-spinner"}, {"text": "\uf111", "id": "fa-circle"}, {"text": "\uf112", "id": "fa-reply"}, {"text": "\uf113", "id": "fa-github-alt"}, {"text": "\uf114", "id": "fa-folder-o"}, {"text": "\uf115", "id": "fa-folder-open-o"}, {"text": "\uf118", "id": "fa-smile-o"}, {"text": "\uf119", "id": "fa-frown-o"}, {"text": "\uf11a", "id": "fa-meh-o"}, {"text": "\uf11b", "id": "fa-gamepad"}, {"text": "\uf11c", "id": "fa-keyboard-o"}, {"text": "\uf11d", "id": "fa-flag-o"}, {"text": "\uf11e", "id": "fa-flag-checkered"}, {"text": "\uf120", "id": "fa-terminal"}, {"text": "\uf121", "id": "fa-code"}, {"text": "\uf122", "id": "fa-reply-all"}, {"text": "\uf122", "id": "fa-mail-reply-all"}, {"text": "\uf123", "id": "fa-star-half-o"}, {"text": "\uf124", "id": "fa-location-arrow"}, {"text": "\uf125", "id": "fa-crop"}, {"text": "\uf126", "id": "fa-code-fork"}, {"text": "\uf127", "id": "fa-chain-broken"}, {"text": "\uf128", "id": "fa-question"}, {"text": "\uf129", "id": "fa-info"}, {"text": "\uf12a", "id": "fa-exclamation"}, {"text": "\uf12b", "id": "fa-superscript"}, {"text": "\uf12c", "id": "fa-subscript"}, {"text": "\uf12d", "id": "fa-eraser"}, {"text": "\uf12e", "id": "fa-puzzle-piece"}, {"text": "\uf130", "id": "fa-microphone"}, {"text": "\uf131", "id": "fa-microphone-slash"}, {"text": "\uf132", "id": "fa-shield"}, {"text": "\uf133", "id": "fa-calendar-o"}, {"text": "\uf134", "id": "fa-fire-extinguisher"}, {"text": "\uf135", "id": "fa-rocket"}, {"text": "\uf136", "id": "fa-maxcdn"}, {"text": "\uf137", "id": "fa-chevron-circle-left"}, {"text": "\uf138", "id": "fa-chevron-circle-right"}, {"text": "\uf139", "id": "fa-chevron-circle-up"}, {"text": "\uf13a", "id": "fa-chevron-circle-down"}, {"text": "\uf13b", "id": "fa-html5"}, {"text": "\uf13c", "id": "fa-css3"}, {"text": "\uf13d", "id": "fa-anchor"}, {"text": "\uf13e", "id": "fa-unlock-alt"}, {"text": "\uf140", "id": "fa-bullseye"}, {"text": "\uf141", "id": "fa-ellipsis-h"}, {"text": "\uf142", "id": "fa-ellipsis-v"}, {"text": "\uf143", "id": "fa-rss-square"}, {"text": "\uf144", "id": "fa-play-circle"}, {"text": "\uf145", "id": "fa-ticket"}, {"text": "\uf146", "id": "fa-minus-square"}, {"text": "\uf147", "id": "fa-minus-square-o"}, {"text": "\uf148", "id": "fa-level-up"}, {"text": "\uf149", "id": "fa-level-down"}, {"text": "\uf14a", "id": "fa-check-square"}, {"text": "\uf14b", "id": "fa-pencil-square"}, {"text": "\uf14c", "id": "fa-external-link-square"}, {"text": "\uf14d", "id": "fa-share-square"}, {"text": "\uf14e", "id": "fa-compass"}, {"text": "\uf150", "id": "fa-caret-square-o-down"}, {"text": "\uf151", "id": "fa-caret-square-o-up"}, {"text": "\uf152", "id": "fa-caret-square-o-right"}, {"text": "\uf153", "id": "fa-eur"}, {"text": "\uf154", "id": "fa-gbp"}, {"text": "\uf155", "id": "fa-usd"}, {"text": "\uf156", "id": "fa-inr"}, {"text": "\uf157", "id": "fa-jpy"}, {"text": "\uf158", "id": "fa-rub"}, {"text": "\uf159", "id": "fa-krw"}, {"text": "\uf15a", "id": "fa-btc"}, {"text": "\uf15b", "id": "fa-file"}, {"text": "\uf15c", "id": "fa-file-text"}, {"text": "\uf15d", "id": "fa-sort-alpha-asc"}, {"text": "\uf15e", "id": "fa-sort-alpha-desc"}, {"text": "\uf160", "id": "fa-sort-amount-asc"}, {"text": "\uf161", "id": "fa-sort-amount-desc"}, {"text": "\uf162", "id": "fa-sort-numeric-asc"}, {"text": "\uf163", "id": "fa-sort-numeric-desc"}, {"text": "\uf164", "id": "fa-thumbs-up"}, {"text": "\uf165", "id": "fa-thumbs-down"}, {"text": "\uf166", "id": "fa-youtube-square"}, {"text": "\uf167", "id": "fa-youtube"}, {"text": "\uf168", "id": "fa-xing"}, {"text": "\uf169", "id": "fa-xing-square"}, {"text": "\uf16a", "id": "fa-youtube-play"}, {"text": "\uf16b", "id": "fa-dropbox"}, {"text": "\uf16c", "id": "fa-stack-overflow"}, {"text": "\uf16d", "id": "fa-instagram"}, {"text": "\uf16e", "id": "fa-flickr"}, {"text": "\uf170", "id": "fa-adn"}, {"text": "\uf171", "id": "fa-bitbucket"}, {"text": "\uf172", "id": "fa-bitbucket-square"}, {"text": "\uf173", "id": "fa-tumblr"}, {"text": "\uf174", "id": "fa-tumblr-square"}, {"text": "\uf175", "id": "fa-long-arrow-down"}, {"text": "\uf176", "id": "fa-long-arrow-up"}, {"text": "\uf177", "id": "fa-long-arrow-left"}, {"text": "\uf178", "id": "fa-long-arrow-right"}, {"text": "\uf179", "id": "fa-apple"}, {"text": "\uf17a", "id": "fa-windows"}, {"text": "\uf17b", "id": "fa-android"}, {"text": "\uf17c", "id": "fa-linux"}, {"text": "\uf17d", "id": "fa-dribbble"}, {"text": "\uf17e", "id": "fa-skype"}, {"text": "\uf180", "id": "fa-foursquare"}, {"text": "\uf181", "id": "fa-trello"}, {"text": "\uf182", "id": "fa-female"}, {"text": "\uf183", "id": "fa-male"}, {"text": "\uf184", "id": "fa-gittip"}, {"text": "\uf185", "id": "fa-sun-o"}, {"text": "\uf186", "id": "fa-moon-o"}, {"text": "\uf187", "id": "fa-archive"}, {"text": "\uf188", "id": "fa-bug"}, {"text": "\uf189", "id": "fa-vk"}, {"text": "\uf18a", "id": "fa-weibo"}, {"text": "\uf18b", "id": "fa-renren"}, {"text": "\uf18c", "id": "fa-pagelines"}, {"text": "\uf18d", "id": "fa-stack-exchange"}, {"text": "\uf18e", "id": "fa-arrow-circle-o-right"}, {"text": "\uf190", "id": "fa-arrow-circle-o-left"}, {"text": "\uf191", "id": "fa-caret-square-o-left"}, {"text": "\uf192", "id": "fa-dot-circle-o"}, {"text": "\uf193", "id": "fa-wheelchair"}, {"text": "\uf194", "id": "fa-vimeo-square"}, {"text": "\uf195", "id": "fa-try"}, {"text": "\uf196", "id": "fa-plus-square-o"}],
+ init: function (editor, element) {
+ this._super(editor);
+ this.element = element;
+ },
+ /**
+ * Initializes select2: in Chrome and Safari, <select> font apparently
+ * isn't customizable (?) and the fontawesome glyphs fail to appear.
+ */
+ start: function () {
+ return this._super().then(this.proxy('load_data'));
+ },
+ /**
+ * Removes existing FontAwesome classes on the bound element, and sets
+ * all the new ones if necessary.
+ */
+ save: function () {
+ var classes = this.element.className.split(/\s+/);
+ var non_fa_classes = _.reject(classes, function (cls) {
+ return cls === 'fa' || /^fa-/.test(cls);
+ });
+ var final_classes = non_fa_classes.concat(this.get_fa_classes());
+ this.element.className = final_classes.join(' ');
+ this._super();
+ },
+ /**
+ * 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.element.className.split(/\s+/);
+ for (var i = 0; i < classes.length; i++) {
+ var cls = classes[i];
+ switch(cls) {
+ 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;
+ default:
+ if (!/^fa-/.test(cls)) { continue; }
+ this.$('#fa-icon').val(cls);
+ }
+ }
+ this.update_preview();
+ },
+ /**
+ * Serializes the dialog to an array of FontAwesome classes. Includes
+ * the base ``fa``.
+ */
+ get_fa_classes: function () {
+ return [
+ 'fa',
+ this.$('#fa-icon').val(),
+ this.$('#fa-size').val(),
+ this.$('#fa-rotation').val(),
+ this.$('#fa-border').prop('checked') ? 'fa-border' : ''
+ ];
+ },
+ update_preview: function () {
+ var $preview = this.$('#fa-preview').empty();
+ var sizes = ['', 'fa-2x', 'fa-3x', 'fa-4x', 'fa-5x'];
+ var classes = this.get_fa_classes();
+ var no_sizes = _.difference(classes, sizes).join(' ');
+ var selected = false;
+ for (var i = sizes.length - 1; i >= 0; i--) {
+ var size = sizes[i];
+
+ var $p = $('<span>')
+ .attr('data-size', size)
+ .addClass(size)
+ .addClass(no_sizes);
+ if ((size && _.contains(classes, size)) || (!size && !selected)) {
+ $p.addClass('font-icons-selected');
+ selected = true;
+ }
+ $preview.prepend($p);
+ }
+ }
+ });
+
+ website.Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver;
+ var OBSERVER_CONFIG = {
+ childList: true,
+ attributes: true,
+ characterData: true,
+ subtree: true,
+ attributeOldValue: true,
+ };
+ var observer = new website.Observer(function (mutations) {
+ // NOTE: Webkit does not fire DOMAttrModified => webkit browsers
+ // relying on JsMutationObserver shim (Chrome < 18, Safari < 6)
+ // will not mark dirty on attribute changes (@class, img/@src,
+ // a/@href, ...)
+ _(mutations).chain()
+ .filter(function (m) {
+ // ignore any change related to mundane image-edit-button
+ if (m.target && m.target.className
+ && m.target.className.indexOf('image-edit-button') !== -1) {
+ return false;
+ }
+ switch(m.type) {
+ case 'attributes': // ignore .cke_focus being added or removed
+ // if attribute is not a class, can't be .cke_focus change
+ if (m.attributeName !== 'class') { return true; }
+
+ // find out what classes were added or removed
+ var oldClasses = (m.oldValue || '').split(/\s+/);
+ var newClasses = m.target.className.split(/\s+/);
+ var change = _.union(_.difference(oldClasses, newClasses),
+ _.difference(newClasses, oldClasses));
+ // ignore mutation if the *only* change is .cke_focus
+ return change.length !== 1 || change[0] === 'cke_focus';
+ case 'childList':
+ // Remove ignorable nodes from addedNodes or removedNodes,
+ // if either set remains non-empty it's considered to be an
+ // impactful change. Otherwise it's ignored.
+ return !!remove_mundane_nodes(m.addedNodes).length ||
+ !!remove_mundane_nodes(m.removedNodes).length;
+ default:
+ return true;
+ }
+ })
+ .map(function (m) {
+ var node = m.target;
+ while (node && !$(node).hasClass('oe_editable')) {
+ node = node.parentNode;
+ }
+ return node;
+ })
+ .compact()
+ .uniq()
+ .each(function (node) { $(node).trigger('content_changed'); })
+ });
+ function remove_mundane_nodes(nodes) {
+ if (!nodes || !nodes.length) { return []; }
+
+ var output = [];
+ for(var i=0; i<nodes.length; ++i) {
+ var node = nodes[i];
+ if (node.nodeType === document.ELEMENT_NODE) {
+ if (node.nodeName === 'BR' && node.getAttribute('type') === '_moz') {
+ // <br type="_moz"> appears when focusing RTE in FF, ignore
+ continue;
+ }
+ }
+
+ output.push(node);
+ }
+ return output;
+ }
+})();
diff --git a/addons/website/static/src/js/website.editor.newpage.js b/addons/website/static/src/js/website.editor.newpage.js
new file mode 100644
index 00000000000..944a24b89d8
--- /dev/null
+++ b/addons/website/static/src/js/website.editor.newpage.js
@@ -0,0 +1,29 @@
+(function() {
+ "use strict";
+
+ var website = openerp.website;
+ website.is_editable = true;
+ website.is_editable_button = true;
+
+ website.EditorBar.include({
+ start: function() {
+ var res = this._super();
+ this.$("a[data-action=new_page]").parents("li").removeClass("hidden");
+ this.$(".oe_content_menu li.divider").removeClass("hidden");
+ return res;
+ },
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click a[data-action=new_page]': function (ev) {
+ ev.preventDefault();
+ website.prompt({
+ window_title: "New Page",
+ input: "Page Title",
+ }).then(function (val) {
+ if (val) {
+ document.location = '/website/add/' + encodeURI(val);
+ }
+ });
+ }
+ }),
+ });
+})(); \ No newline at end of file
diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js
new file mode 100644
index 00000000000..6fde9495d36
--- /dev/null
+++ b/addons/website/static/src/js/website.js
@@ -0,0 +1,294 @@
+(function() {
+ "use strict";
+
+ var website = {};
+ // The following line can be removed in 2017
+ openerp.website = website;
+
+ website.get_context = function (dict) {
+ var html = document.documentElement;
+ return _.extend({
+ lang: html.getAttribute('lang').replace('-', '_'),
+ website_id: html.getAttribute('data-website-id')|0
+ }, dict);
+ };
+
+ website.parseQS = function (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;
+ website.parseSearch = function () {
+ if (!parsedSearch) {
+ parsedSearch = website.parseQS(window.location.search.substring(1));
+ }
+ return parsedSearch;
+ };
+ website.parseHash = function () {
+ return website.parseQS(window.location.hash.substring(1));
+ };
+
+ /* ----- TEMPLATE LOADING ---- */
+ var templates_def = $.Deferred().resolve();
+ website.add_template_file = function(template) {
+ templates_def = templates_def.then(function() {
+ var def = $.Deferred();
+ openerp.qweb.add_template(template, function(err) {
+ if (err) {
+ def.reject(err);
+ } else {
+ def.resolve();
+ }
+ });
+ return def;
+ });
+ };
+ website.add_template_file('/website/static/src/xml/website.xml');
+ website.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 all_ready = null;
+ var dom_ready = website.dom_ready = $.Deferred();
+ $(document).ready(function () {
+ website.is_editable = website.is_editable || $('html').data('editable');
+ website.is_editable_button= website.is_editable_button || $('html').data('editable');
+ dom_ready.resolve();
+ });
+
+ website.init_kanban = function ($kanban) {
+ $('.js_kanban_col', $kanban).each(function () {
+ var $col = $(this);
+ var $pagination = $('.pagination', $col);
+ if(!$pagination.size()) {
+ return;
+ }
+
+ var page_count = $col.data('page_count');
+ var scope = $pagination.last().find("li").size()-2;
+ var kanban_url_col = $pagination.find("li a:first").attr("href").replace(/[0-9]+$/, '');
+
+ var data = {
+ 'domain': $col.data('domain'),
+ 'model': $col.data('model'),
+ 'template': $col.data('template'),
+ 'step': $col.data('step'),
+ 'orderby': $col.data('orderby')
+ };
+
+ $pagination.on('click', 'a', function (ev) {
+ ev.preventDefault();
+ var $a = $(ev.target);
+ if($a.parent().hasClass('active')) {
+ return;
+ }
+
+ var page = +$a.attr("href").split(",").pop().split('-')[1];
+ data['page'] = page;
+
+ $.post('/website/kanban/', data, function (col) {
+ $col.find("> .thumbnail").remove();
+ $pagination.last().before(col);
+ });
+
+ var page_start = page - parseInt(Math.floor((scope-1)/2), 10);
+ if (page_start < 1 ) page_start = 1;
+ var page_end = page_start + (scope-1);
+ if (page_end > page_count ) page_end = page_count;
+
+ if (page_end - page_start < scope) {
+ page_start = page_end - scope > 0 ? page_end - scope : 1;
+ }
+
+ $pagination.find('li.prev a').attr("href", kanban_url_col+(page-1 > 0 ? page-1 : 1));
+ $pagination.find('li.next a').attr("href", kanban_url_col+(page < page_end ? page+1 : page_end));
+ for(var i=0; i < scope; i++) {
+ $pagination.find('li:not(.prev):not(.next):eq('+i+') a').attr("href", kanban_url_col+(page_start+i)).html(page_start+i);
+ }
+ $pagination.find('li.active').removeClass('active');
+ $pagination.find('li:has(a[href="'+kanban_url_col+page+'"])').addClass('active');
+
+ });
+
+ });
+ };
+
+ /**
+ * Returns a deferred resolved when the templates are loaded
+ * and the Widgets can be instanciated.
+ */
+ website.ready = function() {
+ if (!all_ready) {
+ all_ready = dom_ready.then(function () {
+ return templates_def;
+ }).then(function () {
+ if (website.is_editable) {
+ website.id = $('html').data('website-id');
+ website.session = new openerp.Session();
+ var modules = ['website'];
+ return openerp._t.database.load_translations(website.session, modules, website.get_context().lang);
+ }
+ }).promise();
+ }
+ return all_ready;
+ };
+
+ website.error = function(data, url) {
+ var $error = $(openerp.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');
+ };
+
+ website.prompt = function (options) {
+ /**
+ * A bootstrapped version of prompt() albeit asynchronous
+ * This was built to quickly prompt the user with a single field.
+ * For anything more complex, please use editor.Dialog class
+ *
+ * Usage Ex:
+ *
+ * website.prompt("What... is your quest ?").then(function (answer) {
+ * arthur.reply(answer || "To seek the Holy Grail.");
+ * });
+ *
+ * website.prompt({
+ * select: "Please choose your destiny",
+ * init: function() {
+ * return [ [0, "Sub-Zero"], [1, "Robo-Ky"] ];
+ * }
+ * }).then(function (answer) {
+ * mame_station.loadCharacter(answer);
+ * });
+ *
+ * @param {Object|String} options A set of options used to configure the prompt or the text field name if string
+ * @param {String} [options.window_title=''] title of the prompt modal
+ * @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title
+ * @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title
+ * @param {String} [options.select] tell the modal to use a select box, the given value will be the field title
+ * @param {Object} [options.default=''] default value of the field
+ * @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a deferred]
+ */
+ if (typeof options === 'string') {
+ options = {
+ text: options
+ };
+ }
+ options = _.extend({
+ window_title: '',
+ field_name: '',
+ default: '',
+ init: function() {}
+ }, options || {});
+
+ var type = _.intersection(Object.keys(options), ['input', 'textarea', 'select']);
+ type = type.length ? type[0] : 'text';
+ options.field_type = type;
+ options.field_name = options.field_name || options[type];
+
+ var def = $.Deferred();
+ var dialog = $(openerp.qweb.render('website.prompt', options)).appendTo("body");
+ var field = dialog.find(options.field_type).first();
+ field.val(options.default);
+ field.fillWith = function (data) {
+ if (field.is('select')) {
+ var select = field[0];
+ data.forEach(function (item) {
+ select.options[select.options.length] = new Option(item[1], item[0]);
+ });
+ } else {
+ field.val(data);
+ }
+ };
+ var init = options.init(field, dialog);
+ $.when(init).then(function (fill) {
+ if (fill) {
+ field.fillWith(fill);
+ }
+ dialog.modal('show');
+ field.focus();
+ dialog.on('click', '.btn-primary', function () {
+ def.resolve(field.val(), field);
+ dialog.remove();
+ });
+ });
+ dialog.on('hidden.bs.modal', function () {
+ def.reject();
+ dialog.remove();
+ });
+ if (field.is('input[type="text"], select')) {
+ field.keypress(function (e) {
+ if (e.which == 13) {
+ e.preventDefault();
+ dialog.find('.btn-primary').trigger('click');
+ }
+ });
+ }
+ return def;
+ };
+
+ website.form = function (url, method, params) {
+ var form = document.createElement('form');
+ form.action = url;
+ form.method = method;
+ _.each(params, function (v, k) {
+ var param = document.createElement('input');
+ param.type = 'hidden';
+ param.name = k;
+ param.value = v;
+ form.appendChild(param);
+ });
+ document.body.appendChild(form);
+ form.submit();
+ };
+
+ dom_ready.then(function () {
+
+ /* ----- BOOTSTRAP STUFF ---- */
+ // $('.js_tooltip').bstooltip();
+
+ /* ----- PUBLISHING STUFF ---- */
+ $(document).on('click', '.js_publish_management .js_publish_btn', function () {
+ var $data = $(this).parents(".js_publish_management:first");
+ var self=this;
+ openerp.jsonRpc($data.data('controller') || '/website/publish', 'call', {'id': +$data.data('id'), 'object': $data.data('object')})
+ .then(function (result) {
+ $data.toggleClass("css_unpublished css_published");
+ $data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off');
+ }).fail(function (err, data) {
+ website.error(data, '/web#return_label=Website&model='+$data.data('object')+'&id='+$data.data('id'));
+ });
+ });
+
+ /* ----- KANBAN WEBSITE ---- */
+ $('.js_kanban').each(function () {
+ website.init_kanban(this);
+ });
+
+ setTimeout(function () {
+ if (window.location.hash.indexOf("scrollTop=") > -1) {
+ window.document.body.scrollTop = +location.hash.match(/scrollTop=([0-9]+)/)[1];
+ }
+ },0);
+ });
+
+ return website;
+})();
diff --git a/addons/website/static/src/js/website.menu.js b/addons/website/static/src/js/website.menu.js
new file mode 100644
index 00000000000..63ee508bb72
--- /dev/null
+++ b/addons/website/static/src/js/website.menu.js
@@ -0,0 +1,199 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ website.menu = {};
+ website.add_template_file('/website/static/src/xml/website.menu.xml');
+
+ website.EditorBar.include({
+ start: function() {
+ var res = this._super();
+ this.$(".oe_content_menu a[data-action=edit-structure]").parents("li").removeClass("hidden");
+ return res;
+ },
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click a[data-action="edit-structure"]': 'editStructure',
+ }),
+ editStructure: function () {
+ var self = this;
+ var context = website.get_context();
+ openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'website.menu',
+ method: 'get_tree',
+ args: [context.website_id],
+ kwargs: {
+ context: context
+ },
+ }).then(function (menu) {
+ var result = new website.menu.EditMenuDialog(menu).appendTo(document.body);
+ return result;
+ });
+ },
+ });
+
+ website.menu.EditMenuDialog = website.editor.Dialog.extend({
+ template: 'website.menu.dialog.edit',
+ events: _.extend({}, website.editor.Dialog.prototype.events, {
+ 'click a.js_add_menu': 'add_menu',
+ 'click button.js_edit_menu': 'edit_menu',
+ 'click button.js_delete_menu': 'delete_menu',
+ }),
+ init: function (menu) {
+ this.menu = menu;
+ this.root_menu_id = menu.id;
+ this.flat = this.flatenize(menu);
+ this.to_delete = [];
+ this._super();
+ },
+ start: function () {
+ var r = this._super.apply(this, arguments);
+ this.$('.oe_menu_editor').nestedSortable({
+ listType: 'ul',
+ handle: 'div',
+ items: 'li',
+ maxLevels: 2,
+ toleranceElement: '> div',
+ forcePlaceholderSize: true,
+ opacity: 0.6,
+ placeholder: 'oe_menu_placeholder',
+ tolerance: 'pointer',
+ attribute: 'data-menu-id',
+ expression: '()(.+)', // nestedSortable takes the second match of an expression (*sigh*)
+ });
+ return r;
+ },
+ flatenize: function (node, dict) {
+ dict = dict || {};
+ var self = this;
+ dict[node.id] = node;
+ node.children.forEach(function (child) {
+ self.flatenize(child, dict);
+ });
+ return dict;
+ },
+ add_menu: function () {
+ var self = this;
+ var dialog = new website.menu.MenuEntryDialog();
+ dialog.on('add-menu', this, function (link) {
+ var new_menu = {
+ id: _.uniqueId('new-'),
+ name: link[2] || link[0],
+ url: link[0],
+ new_window: link[1],
+ parent_id: false,
+ sequence: 0,
+ children: [],
+ };
+ self.flat[new_menu.id] = new_menu;
+ self.$('.oe_menu_editor').append(
+ openerp.qweb.render('website.menu.dialog.submenu', { submenu: new_menu }));
+ });
+ dialog.appendTo(document.body);
+ },
+ edit_menu: function (ev) {
+ var self = this;
+ var menu_id = $(ev.currentTarget).closest('[data-menu-id]').data('menu-id');
+ var menu = self.flat[menu_id];
+ if (menu) {
+ var dialog = new website.menu.MenuEntryDialog(undefined, menu);
+ dialog.on('update-menu', this, function (link) {
+ var id = link.shift();
+ var menu_obj = self.flat[id];
+ _.extend(menu_obj, {
+ name: link[2],
+ url: link[0],
+ new_window: link[1],
+ });
+ var $menu = self.$('[data-menu-id="' + id + '"]');
+ $menu.find('.js_menu_label').first().text(menu_obj.name);
+ });
+ dialog.appendTo(document.body);
+ } else {
+ alert("Could not find menu entry");
+ }
+ },
+ delete_menu: function (ev) {
+ var self = this;
+ var $menu = $(ev.currentTarget).closest('[data-menu-id]');
+ var mid = $menu.data('menu-id')|0;
+ if (mid) {
+ this.to_delete.push(mid);
+ }
+ $menu.remove();
+ },
+ save: function () {
+ var self = this;
+ var new_menu = this.$('.oe_menu_editor').nestedSortable('toArray', {startDepthCount: 0});
+ var levels = [];
+ var data = [];
+ var context = website.get_context();
+ // Resequence, re-tree and remove useless data
+ new_menu.forEach(function (menu) {
+ if (menu.item_id) {
+ levels[menu.depth] = (levels[menu.depth] || 0) + 1;
+ var mobj = self.flat[menu.item_id];
+ mobj.sequence = levels[menu.depth];
+ mobj.parent_id = (menu.parent_id|0) || menu.parent_id || self.root_menu_id;
+ delete(mobj.children);
+ data.push(mobj);
+ }
+ });
+ openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'website.menu',
+ method: 'save',
+ args: [[context.website_id], { data: data, to_delete: self.to_delete }],
+ kwargs: {
+ context: context
+ },
+ }).then(function (menu) {
+ self.close();
+ website.reload();
+ });
+ },
+ });
+
+ website.menu.MenuEntryDialog = website.editor.LinkDialog.extend({
+ template: 'website.menu.dialog.add',
+ init: function (editor, data) {
+ this.data = data;
+ return this._super.apply(this, arguments);
+ },
+ start: function () {
+ var self = this;
+ var result = $.when(this._super.apply(this, arguments)).then(function () {
+ if (self.data) {
+ self.bind_data(self.data.name, self.data.url, self.data.new_window);
+ }
+ var $link_text = self.$('#link-text').focus();
+ self.$('#link-page').change(function (e) {
+ if ($link_text.val()) { return; }
+ var data = $(this).select2('data');
+ $link_text.val(data.create ? data.id : data.text);
+ $link_text.focus();
+ });
+ });
+ return result;
+ },
+ save: function () {
+ var $e = this.$('#link-text');
+ if (!$e.val() || !$e[0].checkValidity()) {
+ $e.closest('.form-group').addClass('has-error');
+ $e.focus();
+ return;
+ }
+ return this._super.apply(this, arguments);
+ },
+ make_link: function (url, new_window, label) {
+ var menu_label = this.$('input#link-text').val() || label;
+ if (this.data) {
+ this.trigger('update-menu', [this.data.id, url, new_window, menu_label]);
+ } else {
+ this.trigger('add-menu', [url, new_window, menu_label]);
+ }
+ },
+ destroy: function () {
+ this._super.apply(this, arguments);
+ },
+ });
+
+})();
diff --git a/addons/website/static/src/js/website.mobile.js b/addons/website/static/src/js/website.mobile.js
new file mode 100644
index 00000000000..4acf2d78db8
--- /dev/null
+++ b/addons/website/static/src/js/website.mobile.js
@@ -0,0 +1,28 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ website.EditorBar.include({
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click a[data-action=show-mobile-preview]': 'mobilePreview',
+ }),
+ mobilePreview: function () {
+ (new website.MobilePreview()).appendTo($(document.body));
+ },
+ });
+
+ website.MobilePreview = openerp.Widget.extend({
+ template: 'website.mobile_preview',
+ events: {
+ 'hidden.bs.modal': 'destroy'
+ },
+ start: function () {
+ document.getElementById("mobile-viewport").src = window.location.origin + window.location.pathname + "#mobile-preview";
+ this.$el.modal();
+ },
+ destroy: function () {
+ $('.modal-backdrop').remove();
+ this._super();
+ },
+ });
+})();
diff --git a/addons/website/static/src/js/website.seo.js b/addons/website/static/src/js/website.seo.js
new file mode 100644
index 00000000000..8588b7a3765
--- /dev/null
+++ b/addons/website/static/src/js/website.seo.js
@@ -0,0 +1,527 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ website.add_template_file('/website/static/src/xml/website.seo.xml');
+
+ website.EditorBar.include({
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click a[data-action=promote-current-page]': 'launchSeo',
+ }),
+ launchSeo: function () {
+ (new website.seo.Configurator(this)).appendTo($(document.body));
+ },
+ });
+
+ website.seo = {};
+
+ function analyzeKeyword(htmlPage, keyword) {
+ return htmlPage.isInTitle(keyword) ? {
+ title: 'label label-primary',
+ description: "This keyword is used in the page title",
+ } : htmlPage.isInDescription(keyword) ? {
+ title: 'label label-info',
+ description: "This keyword is used in the page description",
+ } : htmlPage.isInBody(keyword) ? {
+ title: 'label label-info',
+ description: "This keyword is used in the page content."
+ } : {
+ title: 'label label-default',
+ description: "This keyword is not used anywhere on the page."
+ };
+ }
+
+ website.seo.Suggestion = openerp.Widget.extend({
+ template: 'website.seo_suggestion',
+ events: {
+ 'click .js_seo_suggestion': 'select',
+ },
+ init: function (parent, options) {
+ this.root = options.root;
+ this.keyword = options.keyword;
+ this.htmlPage = options.page;
+ this._super(parent);
+ },
+ start: function () {
+ this.htmlPage.on('title-changed', this, this.renderElement);
+ this.htmlPage.on('description-changed', this, this.renderElement);
+ },
+ analyze: function () {
+ return analyzeKeyword(this.htmlPage, this.keyword);
+ },
+ highlight: function () {
+ return this.analyze().title;
+ },
+ tooltip: function () {
+ return this.analyze().description;
+ },
+ select: function () {
+ this.trigger('selected', this.keyword);
+ },
+ });
+
+ website.seo.SuggestionList = openerp.Widget.extend({
+ template: 'website.seo_suggestion_list',
+ init: function (parent, options) {
+ this.root = options.root;
+ this.htmlPage = options.page;
+ this._super(parent);
+ },
+ start: function () {
+ this.refresh();
+ },
+ refresh: function () {
+ var self = this;
+ self.$el.append("Loading...");
+ function addSuggestions (list) {
+ self.$el.empty();
+ // TODO Improve algorithm + Ajust based on custom user keywords
+ var regex = new RegExp(self.root, "gi");
+ var cleanList = _.map(list, function (word) {
+ return word.replace(regex, "").trim();
+ });
+ // TODO Order properly ?
+ _.each(_.uniq(cleanList), function (keyword) {
+ if (keyword) {
+ var suggestion = new website.seo.Suggestion(self, {
+ root: self.root,
+ keyword: keyword,
+ page: self.htmlPage,
+ });
+ suggestion.on('selected', self, function (word) {
+ self.trigger('selected', word);
+ });
+ suggestion.appendTo(self.$el);
+ }
+ });
+ }
+ $.getJSON("http://suggest.hp.af.cm/suggest/"+encodeURIComponent(this.root + " "), addSuggestions);
+ },
+ });
+
+ website.seo.Keyword = openerp.Widget.extend({
+ template: 'website.seo_keyword',
+ events: {
+ 'click a[data-action=remove-keyword]': 'destroy',
+ },
+ maxWordsPerKeyword: 4, // TODO Check
+ init: function (parent, options) {
+ this.keyword = options.word;
+ this.htmlPage = options.page;
+ this._super(parent);
+ },
+ start: function () {
+ this.htmlPage.on('title-changed', this, this.updateLabel);
+ this.htmlPage.on('description-changed', this, this.updateLabel);
+ this.suggestionList = new website.seo.SuggestionList(this, {
+ root: this.keyword,
+ page: this.htmlPage,
+ });
+ this.suggestionList.on('selected', this, function (word) {
+ this.trigger('selected', word);
+ });
+ this.suggestionList.appendTo(this.$('.js_seo_keyword_suggestion'));
+ },
+ analyze: function () {
+ return analyzeKeyword(this.htmlPage, this.keyword);
+ },
+ highlight: function () {
+ return this.analyze().title;
+ },
+ tooltip: function () {
+ return this.analyze().description;
+ },
+ updateLabel: function () {
+ var cssClass = "oe_seo_keyword js_seo_keyword " + this.highlight();
+ this.$(".js_seo_keyword").attr('class', cssClass);
+ this.$(".js_seo_keyword").attr('title', this.tooltip());
+ },
+ destroy: function () {
+ this.trigger('removed');
+ this._super();
+ },
+ });
+
+ website.seo.KeywordList = openerp.Widget.extend({
+ template: 'website.seo_list',
+ maxKeywords: 10,
+ init: function (parent, options) {
+ this.htmlPage = options.page;
+ this._super(parent);
+ },
+ start: function () {
+ var self = this;
+ var existingKeywords = self.htmlPage.keywords();
+ if (existingKeywords.length > 0) {
+ _.each(existingKeywords, function (word) {
+ self.add.call(self, word);
+ });
+ } else {
+ var companyName = self.htmlPage.company().toLowerCase();
+ if (companyName != 'yourcompany') {
+ self.add(companyName);
+ }
+ }
+ },
+ keywords: function () {
+ var result = [];
+ this.$('.js_seo_keyword').each(function () {
+ result.push($(this).data('keyword'));
+ });
+ return result;
+ },
+ isFull: function () {
+ return this.keywords().length >= this.maxKeywords;
+ },
+ exists: function (word) {
+ return _.contains(this.keywords(), word);
+ },
+ add: function (candidate) {
+ var self = this;
+ // TODO Refine
+ var word = candidate ? candidate.replace(/[,;.:<>]+/g, " ").replace(/ +/g, " ").trim().toLowerCase() : "";
+ if (word && !self.isFull() && !self.exists(word)) {
+ var keyword = new website.seo.Keyword(self, {
+ word: word,
+ page: this.htmlPage,
+ });
+ keyword.on('removed', self, function () {
+ self.trigger('list-not-full');
+ self.trigger('removed', word);
+ });
+ keyword.on('selected', self, function (word) {
+ self.trigger('selected', word);
+ });
+ keyword.appendTo(self.$el);
+ }
+ if (self.isFull()) {
+ self.trigger('list-full');
+ }
+ },
+ });
+
+ website.seo.Image = openerp.Widget.extend({
+ template: 'website.seo_image',
+ init: function (parent, options) {
+ this.src = options.src;
+ this.alt = options.alt;
+ this._super(parent);
+ },
+ });
+
+
+ website.seo.ImageList = openerp.Widget.extend({
+ init: function (parent, options) {
+ this.htmlPage = options.page;
+ this._super(parent);
+ },
+ start: function () {
+ var self = this;
+ this.htmlPage.images().each(function (index, image) {
+ new website.seo.Image(self, image).appendTo(self.$el);
+ });
+ },
+ images: function () {
+ var result = [];
+ this.$('input').each(function () {
+ var $input = $(this);
+ result.push({
+ src: $input.attr('src'),
+ alt: $input.val(),
+ });
+ });
+ return result;
+ },
+ add: function (image) {
+ new website.seo.Image(this, image).appendTo(this.$el);
+ },
+ });
+
+ website.seo.Preview = openerp.Widget.extend({
+ template: 'website.seo_preview',
+ init: function (parent, options) {
+ this.title = options.title;
+ this.url = options.url;
+ this.description = options.description || "[ The description will be generated by google unless you specify one ]";
+ this._super(parent);
+ },
+ });
+
+ website.seo.HtmlPage = openerp.Class.extend(openerp.PropertiesMixin, {
+ url: function () {
+ var url = window.location.href;
+ var hashIndex = url.indexOf('#');
+ return hashIndex >= 0 ? url.substring(0, hashIndex) : url;
+ },
+ title: function () {
+ var $title = $('title');
+ return ($title.length > 0) && $title.text() && $title.text().trim();
+ },
+ changeTitle: function (title) {
+ // TODO create tag if missing
+ $('title').text(title);
+ this.trigger('title-changed', title);
+ },
+ description: function () {
+ var $description = $('meta[name=description]');
+ return ($description.length > 0) && ($description.attr('value') && $description.attr('value').trim());
+ },
+ changeDescription: function (description) {
+ // TODO create tag if missing
+ $('meta[name=description]').attr('value', description);
+ this.trigger('description-changed', description);
+ },
+ keywords: function () {
+ var $keywords = $('meta[name=keywords]');
+ var parsed = ($keywords.length > 0) && $keywords.attr('value') && $keywords.attr('value').split(",");
+ return (parsed && parsed[0]) ? parsed: [];
+ },
+ changeKeywords: function (keywords) {
+ // TODO create tag if missing
+ $('meta[name=keywords]').attr('value', keywords.join(","));
+ this.trigger('keywords-changed', keywords);
+ },
+ headers: function (tag) {
+ return $('#wrap '+tag).map(function () {
+ return $(this).text();
+ });
+ },
+ images: function () {
+ return $('#wrap img').map(function () {
+ var $img = $(this);
+ return {
+ src: $img.attr('src'),
+ alt: $img.attr('alt'),
+ };
+ });
+ },
+ company: function () {
+ return $('meta[name="openerp.company"]').attr('value');
+ },
+ bodyText: function () {
+ return $('body').children().not('.js_seo_configuration').text();
+ },
+ isInBody: function (text) {
+ return new RegExp("\\b"+text+"\\b", "gi").test(this.bodyText());
+ },
+ isInTitle: function (text) {
+ return new RegExp("\\b"+text+"\\b", "gi").test(this.title());
+ },
+ isInDescription: function (text) {
+ return new RegExp("\\b"+text+"\\b", "gi").test(this.description());
+ },
+ });
+
+ website.seo.Tip = openerp.Widget.extend({
+ template: 'website.seo_tip',
+ events: {
+ 'closed.bs.alert': 'destroy',
+ },
+ init: function (parent, options) {
+ this.message = options.message;
+ // cf. http://getbootstrap.com/components/#alerts
+ // success, info, warning or danger
+ this.type = options.type || 'info';
+ this._super(parent);
+ },
+ });
+
+ website.seo.Configurator = openerp.Widget.extend({
+ template: 'website.seo_configuration',
+ events: {
+ 'keyup input[name=seo_page_keywords]': 'confirmKeyword',
+ 'keyup input[name=seo_page_title]': 'titleChanged',
+ 'keyup textarea[name=seo_page_description]': 'descriptionChanged',
+ 'click button[data-action=add]': 'addKeyword',
+ 'click button[data-action=update]': 'update',
+ 'hidden.bs.modal': 'destroy',
+ },
+ canEditTitle: false,
+ canEditDescription: false,
+ canEditKeywords: false,
+ maxTitleSize: 65,
+ maxDescriptionSize: 150,
+ start: function () {
+ var self = this;
+ var $modal = self.$el;
+ var htmlPage = this.htmlPage = new website.seo.HtmlPage();
+ $modal.find('.js_seo_page_url').text(htmlPage.url());
+ $modal.find('input[name=seo_page_title]').val(htmlPage.title());
+ $modal.find('textarea[name=seo_page_description]').val(htmlPage.description());
+ // self.suggestImprovements();
+ // self.imageList = new website.seo.ImageList(self, { page: htmlPage });
+ // if (htmlPage.images().length === 0) {
+ // $modal.find('.js_image_section').remove();
+ // } else {
+ // self.imageList.appendTo($modal.find('.js_seo_image_list'));
+ // }
+ self.keywordList = new website.seo.KeywordList(self, { page: htmlPage });
+ self.keywordList.on('list-full', self, function () {
+ $modal.find('input[name=seo_page_keywords]')
+ .attr('readonly', "readonly")
+ .attr('placeholder', "Remove a keyword first");
+ $modal.find('button[data-action=add]')
+ .prop('disabled', true).addClass('disabled');
+ });
+ self.keywordList.on('list-not-full', self, function () {
+ $modal.find('input[name=seo_page_keywords]')
+ .removeAttr('readonly').attr('placeholder', "");
+ $modal.find('button[data-action=add]')
+ .prop('disabled', false).removeClass('disabled');
+ });
+ self.keywordList.on('selected', self, function (word) {
+ self.keywordList.add(word);
+ });
+ self.keywordList.appendTo($modal.find('.js_seo_keywords_list'));
+ self.disableUnsavableFields();
+ self.renderPreview();
+ $modal.modal();
+ },
+ disableUnsavableFields: function () {
+ var self = this;
+ var $modal = self.$el;
+ self.loadMetaData().then(function (data) {
+ self.canEditTitle = data && ('website_meta_title' in data);
+ self.canEditDescription = data && ('website_meta_description' in data);
+ self.canEditKeywords = data && ('website_meta_keywords' in data);
+ if (!self.canEditTitle) {
+ $modal.find('input[name=seo_page_title]').attr('disabled', true);
+ }
+ if (!self.canEditDescription) {
+ $modal.find('textarea[name=seo_page_description]').attr('disabled', true);
+ }
+ if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) {
+ $modal.find('button[data-action=update]').attr('disabled', true);
+ }
+ });
+ },
+ suggestImprovements: function () {
+ var tips = [];
+ var self = this;
+ function displayTip(message, type) {
+ new website.seo.Tip(self, {
+ message: message,
+ type: type,
+ }).appendTo(self.$('.js_seo_tips'));
+ }
+ var htmlPage = this.htmlPage;
+
+ // Add message suggestions at the top of the dialog
+ // if necessary....
+ // if (htmlPage.headers('h1').length === 0) {
+ // tips.push({
+ // type: 'warning',
+ // message: "This page seems to be missing a title.",
+ // });
+ // }
+
+ if (tips.length > 0) {
+ _.each(tips, function (tip) {
+ displayTip(tip.message, tip.type);
+ });
+ }
+ },
+ confirmKeyword: function (e) {
+ if (e.keyCode == 13) {
+ this.addKeyword();
+ }
+ },
+ addKeyword: function (word) {
+ var $input = this.$('input[name=seo_page_keywords]');
+ var keyword = _.isString(word) ? word : $input.val();
+ this.keywordList.add(keyword);
+ $input.val("");
+ },
+ update: function () {
+ var self = this;
+ var data = {};
+ if (self.canEditTitle) {
+ data.website_meta_title = self.htmlPage.title();
+ }
+ if (self.canEditDescription) {
+ data.website_meta_description = self.htmlPage.description();
+ }
+ if (self.canEditKeywords) {
+ data.website_meta_keywords = self.keywordList.keywords().join(", ");
+ }
+ self.saveMetaData(data).then(function () {
+ self.$el.modal('hide');
+ });
+ },
+ getMainObject: function () {
+ var repr = $('html').data('main-object');
+ var m = repr.match(/.+\((.+), (\d+)\)/);
+ if (!m) {
+ return null;
+ } else {
+ return {
+ model: m[1],
+ id: m[2]|0
+ };
+ }
+ },
+ loadMetaData: function () {
+ var self = this;
+ var obj = this.getMainObject();
+ var def = $.Deferred();
+ if (!obj) {
+ // return $.Deferred().reject(new Error("No main_object was found."));
+ def.resolve(null);
+ } else {
+ var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords'];
+ var model = website.session.model(obj.model);
+ model.call('read', [[obj.id], fields, website.get_context()]).then(function (data) {
+ if (data.length) {
+ var meta = data[0];
+ meta.model = obj.model;
+ def.resolve(meta);
+ } else {
+ def.resolve(null);
+ }
+ }).fail(function () {
+ def.reject();
+ });
+ }
+ return def;
+ },
+ saveMetaData: function (data) {
+ var obj = this.getMainObject();
+ if (!obj) {
+ return $.Deferred().reject();
+ } else {
+ var model = website.session.model(obj.model);
+ return model.call('write', [[obj.id], data, website.get_context()]);
+ }
+ },
+ titleChanged: function () {
+ var self = this;
+ setTimeout(function () {
+ var title = self.$('input[name=seo_page_title]').val();
+ self.htmlPage.changeTitle(title);
+ self.renderPreview();
+ }, 0);
+ },
+ descriptionChanged: function () {
+ var self = this;
+ setTimeout(function () {
+ var description = self.$('textarea[name=seo_page_description]').attr('value');
+ self.htmlPage.changeDescription(description);
+ self.renderPreview();
+ }, 0);
+ },
+ renderPreview: function () {
+ var preview = new website.seo.Preview(this, {
+ title: this.htmlPage.title(),
+ description: this.htmlPage.description(),
+ url: this.htmlPage.url(),
+ });
+ var $preview = this.$('.js_seo_preview');
+ $preview.empty();
+ preview.appendTo($preview);
+ },
+ destroy: function () {
+ this.htmlPage.changeKeywords(this.keywordList.keywords());
+ this._super();
+ },
+ });
+})();
diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js
new file mode 100644
index 00000000000..60c5aea49e2
--- /dev/null
+++ b/addons/website/static/src/js/website.snippets.animation.js
@@ -0,0 +1,122 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ website.snippet = {};
+ website.snippet.readyAnimation = [];
+
+ website.snippet.start_animation = function () {
+ for (var k in website.snippet.animationRegistry) {
+ var Animation = website.snippet.animationRegistry[k];
+ var selector = "[data-snippet-id='"+k+"']";
+ if (Animation.prototype.selector) {
+ selector += ", " + Animation.prototype.selector;
+ }
+ $(selector).each(function() {
+ var $snipped_id = $(this);
+ if ( !$snipped_id.parents("#oe_snippets").length &&
+ !$snipped_id.parent("body").length &&
+ !$snipped_id.data("snippet-view")) {
+ website.snippet.readyAnimation.push($snipped_id);
+ $snipped_id.data("snippet-view", new Animation($snipped_id));
+ }
+ });
+ }
+ };
+ website.snippet.stop_animation = function () {
+ $(website.snippet.readyAnimation).each(function() {
+ var $snipped_id = $(this);
+ if ($snipped_id.data("snippet-view")) {
+ $snipped_id.data("snippet-view").stop();
+ $snipped_id.data("snippet-view", false);
+ }
+ });
+ };
+ $(document).ready(website.snippet.start_animation);
+
+
+ website.snippet.animationRegistry = {};
+ website.snippet.Animation = openerp.Class.extend({
+ selector: false,
+ $: function () {
+ return this.$el.find.apply(this.$el, arguments);
+ },
+ init: function (dom) {
+ this.$el = this.$target = $(dom);
+ this.start();
+ },
+ /*
+ * 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 () {
+ },
+ });
+
+ website.snippet.animationRegistry.slider = website.snippet.Animation.extend({
+ selector: ".carousel",
+ start: function () {
+ this.$target.carousel({interval: 10000});
+ },
+ });
+
+ website.snippet.animationRegistry.parallax = website.snippet.Animation.extend({
+ parallax: ".parallax",
+ start: function () {
+ var self = this;
+ setTimeout(function () {self.set_values();});
+ this.on_scroll = function () {
+ var speed = parseFloat(self.$target.attr("data-scroll-background-ratio") || 0);
+ if (speed == 1) return;
+ var offset = parseFloat(self.$target.attr("data-scroll-background-offset") || 0);
+ var top = offset + window.scrollY * speed;
+ self.$target.css("background-position", "0px " + top + "px");
+ };
+ this.on_resize = function () {
+ self.set_values();
+ };
+ $(window).on("scroll", this.on_scroll);
+ $(window).on("resize", this.on_resize);
+ },
+ stop: function () {
+ $(window).off("scroll", this.on_scroll)
+ .off("resize", this.on_resize);
+ },
+ set_values: function () {
+ var self = this;
+ var speed = parseFloat(self.$target.attr("data-scroll-background-ratio") || 0);
+
+ if (speed === 1 || this.$target.css("background-image") === "none") {
+ this.$target.css("background-attachment", "fixed").css("background-position", "0px 0px");
+ return;
+ } else {
+ this.$target.css("background-attachment", "scroll");
+ }
+
+ this.$target.attr("data-scroll-background-offset", 0);
+ var img = new Image();
+ img.onload = function () {
+ var offset = 0;
+ var padding = parseInt($(document.body).css("padding-top"));
+ if (speed < 1) {
+ var inner_offset = self.$target.outerHeight() - this.height / this.width * document.body.clientWidth;
+ var outer_offset = self.$target.offset().top - (document.body.clientHeight - self.$target.outerHeight()) - padding;
+ offset = - outer_offset * speed + inner_offset;
+ } else {
+ offset = - self.$target.offset().top * speed;
+ }
+ self.$target.attr("data-scroll-background-offset", offset > 0 ? 0 : offset);
+ $(window).scroll();
+ };
+ img.src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)/g, "");
+ $(window).scroll();
+ }
+ });
+
+})();
diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js
new file mode 100644
index 00000000000..8a3e7a4421d
--- /dev/null
+++ b/addons/website/static/src/js/website.snippets.editor.js
@@ -0,0 +1,1443 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ website.add_template_file('/website/static/src/xml/website.snippets.xml');
+
+ website.EditorBar.include({
+ start: function () {
+ var self = this;
+ $("[data-oe-model]").on('click', function (event) {
+ var $this = $(event.srcElement);
+ var tag = $this[0].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;
+ $("body").off('click');
+ window.snippets = this.snippets = new website.snippet.BuildingBlock(this);
+ this.snippets.appendTo(this.$el);
+
+ this.on('rte:ready', this, function () {
+ self.snippets.$button.removeClass("hidden");
+ website.snippet.stop_animation();
+ website.snippet.start_animation();
+ });
+
+ return this._super.apply(this, arguments);
+ },
+ save: function () {
+ this.snippets.make_active(false);
+
+ // FIXME: call clean_for_save on all snippets of the page, not only modified ones
+ // important for banner of parallax that changes data automatically.
+ this.snippets.clean_for_save();
+ remove_added_snippet_id();
+ this._super();
+ },
+ });
+
+ /* ----- SNIPPET SELECTOR ---- */
+
+ var observer = new website.Observer(function (mutations) {
+ if (!_(mutations).find(function (m) {
+ return m.type === 'childList' && m.addedNodes.length > 0;
+ })) {
+ return;
+ }
+ hack_to_add_snippet_id()
+ });
+
+ // puts $el at the same absolute position as $target
+ function hack_to_add_snippet_id () {
+ _.each(website.snippet.selector, function (val) {
+ $(val[0]).each(function() {
+ if (!$(this).is("[data-snippet-id]") && $(this).parents("[data-oe-model]").length) {
+ $(this).attr("data-snippet-id", val[1]);
+ }
+ });
+ });
+ }
+ function remove_added_snippet_id () {
+ _.each(website.snippet.selector, function (val) {
+ $(val[0]).each(function() {
+ if ($(this).data("snippet-id") === val[1]) {
+ $(this).removeAttr("data-snippet-id");
+ }
+ });
+ });
+ }
+
+ $(document).ready(function() {
+ hack_to_add_snippet_id();
+ });
+
+ // 'snippet-dropped' is triggered on '#oe_snippets' whith $target as attribute when a snippet is dropped
+ // 'snippet-activated' is triggered on '#oe_snippets' (and on snippet) when a snippet is activated
+
+ website.snippet.styles = {};
+ website.snippet.selector = [];
+ website.snippet.BuildingBlock = openerp.Widget.extend({
+ template: 'website.snippets',
+ activeSnippets: [],
+ init: function (parent) {
+ this.parent = parent;
+ this._super.apply(this, arguments);
+ if(!$('#oe_manipulators').length){
+ $("<div id='oe_manipulators'></div>").appendTo('body');
+ }
+ this.$active_snipped_id = false;
+ hack_to_add_snippet_id();
+ this.snippets = [];
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+ },
+ dom_filter: function (dom, sibling) {
+ if (typeof dom === "string") {
+ var include = "[data-oe-model]";
+ var sdom = dom.split(',');
+ dom = "";
+ _.each(sdom, function (val) {
+ val = val.replace(/^\s+|\s+$/g, '');
+ dom += include + " " + val + ", ";
+ if (!sibling) {
+ val = val.split(" ");
+ dom += val.shift() + include + val.join(" ") + ", ";
+ }
+ });
+ dom = dom.replace(/,\s*$/g, '');
+ return $(dom);
+ } else {
+ return (!sibling && $(dom).is("[data-oe-model]")) || $(dom).parents("[data-oe-model]").length ? $(dom) : $("");
+ }
+ },
+ start: function() {
+ var self = this;
+
+ this.$button = $(openerp.qweb.render('website.snippets_button'))
+ .prependTo(this.parent.$("#website-top-edit ul"))
+ .find("button");
+
+ this.$button.click(function () {
+ self.make_active(false);
+ self.$el.toggleClass("hidden");
+ });
+ $("#wrapwrap").click(function () {
+ self.$el.addClass("hidden");
+ });
+
+ this.fetch_snippet_templates();
+
+ this.bind_snippet_click_editor();
+
+ this.$el.addClass("hidden");
+
+ this.$modal = $(openerp.qweb.render('website.snippets_modal'));
+ this.$modal.appendTo("body");
+
+ $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) {
+ e.preventDefault();
+ });
+
+ this.getParent().on('change:height', this, function (editor) {
+ self.$el.css('top', editor.get('height'));
+ });
+ self.$el.css('top', this.parent.get('height'));
+ },
+ _get_snippet_url: function () {
+ return '/website/snippets';
+ },
+ fetch_snippet_templates: function () {
+ var self = this;
+
+ openerp.jsonRpc(this._get_snippet_url(), 'call', {})
+ .then(function (html) {
+ var $html = $(html);
+
+ var $styles = $html.find("[data-snippet-style-id]");
+ $styles.each(function () {
+ var $style = $(this);
+ var style_id = $style.data('snippet-style-id');
+ website.snippet.styles[style_id] = {
+ 'snippet-style-id' : style_id,
+ 'selector': $style.data('selector'),
+ '$el': $style,
+ };
+ });
+ $styles.addClass("hidden");
+
+ self.$snippets = $html.find(".tab-content > div > div").addClass("oe_snippet");
+ self.$el.append($html);
+
+
+ var snippets = 0;
+ self.$snippets.each(function () {
+ if (self.snippet_have_dropzone($(this)))
+ snippets++;
+ });
+ if (!snippets) self.$button.css("display", "none");
+
+ self.make_snippet_draggable(self.$snippets);
+ });
+ },
+ cover_target: function ($el, $target){
+ var pos = $target.offset();
+ var mt = parseInt($target.css("margin-top") || 0);
+ var mb = parseInt($target.css("margin-bottom") || 0);
+ $el.css({
+ 'position': 'absolute',
+ 'width': $target.outerWidth(),
+ 'height': $target.outerHeight() + mt + mb+1,
+ 'top': pos.top - mt,
+ 'left': pos.left
+ });
+ $el.find(".oe_handle.size").css("bottom", (mb-7)+'px');
+ },
+ show: function () {
+ this.$el.removeClass("hidden");
+ },
+ hide: function () {
+ this.$el.addClass("hidden");
+ },
+
+ snippet_have_dropzone: function ($snippet) {
+ return (($snippet.data('selector-siblings') && this.dom_filter($snippet.data('selector-siblings')).size() > 0) ||
+ ($snippet.data('selector-children') && this.dom_filter($snippet.data('selector-children')).size() > 0) ||
+ ($snippet.data('selector-vertical-children') && this.dom_filter($snippet.data('selector-vertical-children')).size() > 0));
+ },
+
+ bind_snippet_click_editor: function () {
+ var self = this;
+ var snipped_event_flag = false;
+ $("#wrapwrap").on('click', function (event) {
+ if (snipped_event_flag) {
+ return;
+ }
+ snipped_event_flag = true;
+ setTimeout(function () {snipped_event_flag = false;}, 0);
+ var $target = $(event.srcElement);
+ if (!$target.attr("data-snippet-id")) {
+ $target = $target.parents("[data-snippet-id]:first");
+ }
+ if (!$target.attr("data-oe-model") && !$target.parents("[data-oe-model]:first").length) {
+ $target = false;
+ }
+ 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").onBlur();
+ }
+ }
+ },
+ snippet_focus: function ($snippet) {
+ if ($snippet) {
+ if ($snippet.data("snippet-editor")) {
+ $snippet.data("snippet-editor").onFocus();
+ }
+ }
+ },
+ clean_for_save: function () {
+ for (var k in this.snippets) {
+ if (!this.snippets.hasOwnProperty(k)) { continue; }
+ var editor = $(this.snippets[k]).data("snippet-editor");
+ if (editor) {
+ editor.clean_for_save();
+ }
+ }
+ },
+ 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);
+ }
+ $("#oe_snippets").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;
+ var editor = website.snippet.editorRegistry[$snippet.data("snippet-id")] || website.snippet.editorRegistry.resize;
+ $snippet.data("snippet-editor", new editor(this, $snippet));
+ }
+ this.cover_target($snippet.data('overlay'), $snippet);
+ },
+
+ path_eval: function(path){
+ var obj = window;
+ path = path.split('.');
+ do{
+ obj = obj[path.shift()];
+ }while(path.length && obj);
+ return obj;
+ },
+
+ // 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: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 = $(this);
+ snipped_id = $snippet.data('snippet-id');
+ action = $snippet.find('.oe_snippet_body').size() ? 'insert' : 'mutate';
+ if( action === 'insert'){
+ if (!$snippet.data('selector-siblings') && !$snippet.data('selector-children') && !$snippet.data('selector-vertical-children')) {
+ console.debug($snippet.data("snippet-id") + " have oe_snippet_body class and have not for insert action"+
+ "data-selector-siblings, data-selector-children or data-selector-vertical-children tag for mutate action");
+ return;
+ }
+ self.activate_insertion_zones({
+ siblings: $snippet.data('selector-siblings'),
+ children: $snippet.data('selector-children'),
+ vertical_children: $snippet.data('selector-vertical-children')
+ });
+
+ $toInsert = $snippet.find('.oe_snippet_body').clone();
+ $toInsert.removeClass('oe_snippet_body');
+ $toInsert.data('src-snippet-id', snipped_id);
+ if (!$toInsert.data('snippet-id')) {
+ $toInsert.attr('data-snippet-id', snipped_id);
+ } else {
+ snipped_id = $toInsert.data('snippet-id');
+ }
+
+ } else if( action === 'mutate' ){
+ if (!$snippet.data('selector')) {
+ console.debug($snippet.data("snippet-id") + " have not oe_snippet_body class and have not data-selector tag");
+ return;
+ }
+ var $targets = self.activate_overlay_zones($snippet.data('selector'));
+ $targets.each(function(){
+ var $clone = $(this).data('overlay').clone();
+ $clone.addClass("oe_drop_zone").data('target', $(this));
+ $(this).data('overlay').after($clone);
+ });
+
+ }
+
+ $('.oe_drop_zone').droppable({
+ over: function(){
+ if( action === 'insert'){
+ dropped = true;
+ $(this).first().after($toInsert);
+ }
+ },
+ out: function(){
+ var prev = $toInsert.prev();
+ if( action === 'insert' && this === prev[0]){
+ dropped = false;
+ $toInsert.detach();
+ }
+ }
+ });
+ },
+ stop: function(ev, ui){
+ if (action === 'insert' && ! dropped && $('.oe_drop_zone') && ui.position.top > 50) {
+ var el = $('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
+ if (el.length) {
+ el.after($toInsert);
+ dropped = true;
+ }
+ }
+
+ $('.oe_drop_zone').droppable('destroy').remove();
+ if (dropped) {
+ var $target = false;
+ if(action === 'insert'){
+ $target = $toInsert;
+
+ website.snippet.start_animation();
+
+ self.create_overlay($target);
+ if ($snippet.data("snippet-editor")) {
+ $target.data("snippet-editor").drop_and_build_snippet($target);
+ }
+
+ $target.find("[data-snippet-id]").each(function () {
+ var $snippet = $(this);
+ var snippet_id = $snippet.data("data-snippet-id");
+ self.create_overlay($snippet);
+ if ($snippet.data("snippet-editor")) {
+ $snippet.data("snippet-editor").drop_and_build_snippet($snippet);
+ }
+ });
+
+ } else {
+ $target = $(this).data('target');
+
+ self.create_overlay($target);
+ if (website.snippet.editorRegistry[snipped_id]) {
+ var snippet = new website.snippet.editorRegistry[snipped_id](self, $target);
+ snippet.drop_and_build_snippet($target);
+ }
+ }
+ setTimeout(function () {
+ $("#oe_snippets").trigger('snippet-dropped', $target);
+ self.make_active($target);
+ },0);
+ } else {
+ $toInsert.remove();
+ if (self.$modal.find('input:not(:checked)').length) {
+ self.$modal.modal('toggle');
+ }
+ }
+ },
+ });
+ },
+
+ // 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('snippet-id') === id;
+ }).first();
+ },
+
+ // Create element insertion drop zones. two css selectors can be provided
+ // selector.children -> will insert drop zones as direct child of the selected elements
+ // in case the selected elements have children themselves, dropzones will be interleaved
+ // with them.
+ // selector.siblings -> will insert drop zones after and before selected elements
+ activate_insertion_zones: function(selector){
+ var self = this;
+ var child_selector = selector.children;
+ var sibling_selector = selector.siblings;
+ var vertical_child_selector = selector.vertical_children;
+
+ var zone_template = "<div class='oe_drop_zone oe_insert'></div>";
+
+ if(child_selector){
+ self.dom_filter(child_selector).each(function (){
+ var $zone = $(this);
+ $zone.find('> *:not(.oe_drop_zone):visible').after(zone_template);
+ $zone.prepend(zone_template);
+ });
+ }
+
+ if(vertical_child_selector){
+ self.dom_filter(vertical_child_selector).each(function (){
+ var $zone = $(this);
+ var $template = $(zone_template).addClass("oe_vertical");
+ var nb = 0;
+ var $lastinsert = false;
+ var left = 0;
+ var temp_left = 0;
+ $zone.find('> *:not(.oe_drop_zone):visible').each(function () {
+ var $col = $(this);
+ $template.css('height', ($col.outerHeight() + parseInt($col.css("margin-top")) + parseInt($col.css("margin-bottom")))+'px');
+ $lastinsert = $template.clone();
+ $(this).after($lastinsert);
+
+ temp_left = $col.position().left;
+ if (left === temp_left) {
+ $col.prev(".oe_drop_zone.oe_vertical").remove();
+ $col.before($template.clone().css("clear", "left"));
+ }
+ else if (!nb) {
+ $col.before($template.clone());
+ }
+ left = temp_left;
+ nb ++;
+ });
+ if (!nb) {
+ $zone.prepend($template.css('height', $zone.outerHeight()+'px'));
+ }
+ });
+ }
+
+ if(sibling_selector){
+ self.dom_filter(sibling_selector, true).each(function (){
+ var $zone = $(this);
+ if($zone.prev('.oe_drop_zone:visible').length === 0){
+ $zone.before(zone_template);
+ }
+ if($zone.next('.oe_drop_zone:visible').length === 0){
+ $zone.after(zone_template);
+ }
+ });
+ }
+
+ var count;
+ do {
+ count = 0;
+ // var $zones = $('.oe_drop_zone + .oe_drop_zone'); // no two consecutive zones
+ // count += $zones.length;
+ // $zones.remove();
+
+ $zones = $('.oe_drop_zone > .oe_drop_zone:not(.oe_vertical)').remove(); // no recursive zones
+ count += $zones.length;
+ $zones.remove();
+ } while (count > 0);
+
+ // Cleaning up zones placed between floating or inline elements. We do not like these kind of zones.
+ var $zones = $('.oe_drop_zone:not(.oe_vertical)');
+ $zones.each(function (){
+ var zone = $(this);
+ var prev = zone.prev();
+ var next = zone.next();
+ var float_prev = prev.css('float') || 'none';
+ var float_next = next.css('float') || 'none';
+ var disp_prev = prev.css('display') || null;
+ var disp_next = next.css('display') || null;
+ if( (float_prev === 'left' || float_prev === 'right')
+ && (float_next === 'left' || float_next === 'right') ){
+ zone.remove();
+ }else if( !( disp_prev === null
+ || disp_next === null
+ || disp_prev === 'block'
+ || disp_next === 'block' )){
+ zone.remove();
+ }
+ });
+ },
+
+ // generate drop zones covering the elements selected by the selector
+ // we generate overlay drop zones only to get an idea of where the snippet are, the drop
+ activate_overlay_zones: function(selector){
+ var $targets = this.dom_filter(selector || '[data-snippet-id]');
+ var self = this;
+
+ if (typeof selector !== 'string' && !$targets.length) {
+ console.debug( "A good node must have a [data-oe-model] attribute or must have at least one parent with [data-oe-model] attribute.");
+ console.debug( "Wrong node(s): ", selector);
+ }
+
+ function is_visible($el){
+ return $el.css('display') != 'none'
+ && $el.css('opacity') != '0'
+ && $el.css('visibility') != 'hidden';
+ }
+
+ // filter out invisible elements
+ $targets = $targets.filter(function(){ return is_visible($(this)); });
+
+ // filter out elements with invisible parents
+ $targets = $targets.filter(function(){
+ var parents = $(this).parents().filter(function(){ return !is_visible($(this)); });
+ return parents.length === 0;
+ });
+
+ $targets.each(function () {
+ var $target = $(this);
+ if (!$target.data('overlay')) {
+ var $zone = $(openerp.qweb.render('website.snippet_overlay'));
+ $zone.appendTo('#oe_manipulators');
+ $zone.data('target',$target);
+ $target.data('overlay',$zone);
+
+ $target.on("DOMNodeInserted DOMNodeRemoved DOMSubtreeModified", function () {
+ self.cover_target($zone, $target);
+ });
+ $('body').on("resize", function () {
+ self.cover_target($zone, $target);
+ });
+ }
+ self.cover_target($target.data('overlay'), $target);
+ });
+ return $targets;
+ }
+ });
+
+
+ website.snippet.styleRegistry = {};
+ website.snippet.StyleEditor = openerp.Class.extend({
+ // initialisation (don't overwrite)
+ init: function (parent, $target, snippet_id) {
+ this.parent = parent;
+ this.$target = $target;
+ var styles = this.$target.data("snippet-style-ids") || {};
+ styles[snippet_id] = this;
+ this.$target.data("snippet-style-ids", styles);
+ this.$overlay = this.$target.data('overlay');
+ this['snippet-style-id'] = snippet_id;
+ this.$el = website.snippet.styles[snippet_id].$el.find(">li").clone();
+
+ this.required = this.$el.data("required");
+
+ this.set_active();
+ this.$el.find('li[data-class] a').on('mouseover mouseout click', _.bind(this._mouse, this));
+ this.$target.on('snippet-style-reset', _.bind(this.set_active, this));
+
+ this.start();
+ },
+ _mouse: function (event) {
+ var self = this;
+
+ if (event.type === 'mouseout') {
+ if (!this.over) return;
+ this.over = false;
+ } else if (event.type === 'click') {
+ this.over = false;
+ }else {
+ this.over = true;
+ }
+
+ var $prev, $next;
+ if (event.type === 'mouseout') {
+ $prev = $(event.currentTarget).parent();
+ $next = this.$el.find("li[data-class].active");
+ } else {
+ $prev = this.$el.find("li[data-class].active");
+ $next = $(event.currentTarget).parent();
+ }
+ if (!$prev.length) {
+ $prev = false;
+ }
+ if ($prev && $prev[0] === $next[0]) {
+ $next = false;
+ if (this.required) {
+ return;
+ }
+ }
+
+ var np = {'$next': $next, '$prev': $prev};
+
+ if (event.type === 'click') {
+ setTimeout(function () {
+ self.set_active();
+ self.$target.trigger("snippet-style-change", [self, np]);
+ },0);
+ this.select(event, {'$next': $next, '$prev': $prev});
+ } else {
+ setTimeout(function () {
+ self.$target.trigger("snippet-style-preview", [self, np]);
+ },0);
+ this.preview(event, np);
+ }
+ },
+ // start is call just after the init
+ start: function () {
+ },
+ /* select
+ * called when a user select an item
+ * variables: np = {$next, $prev}
+ * $next is false if they are no next item selected
+ * $prev is false if they are no previous item selected
+ */
+ select: function (event, np) {
+ var self = this;
+ // add or remove html class
+ if (np.$prev) {
+ this.$target.removeClass(np.$prev.data('class' || ""));
+ }
+ if (np.$next) {
+ this.$target.addClass(np.$next.data('class') || "");
+ }
+ },
+ /* preview
+ * called when a user is on mouse over or mouse out of an item
+ * variables: np = {$next, $prev}
+ * $next is false if they are no next item selected
+ * $prev is false if they are no previous item selected
+ */
+ preview: function (event, np) {
+ var self = this;
+
+ // add or remove html class
+ if (np.$prev) {
+ this.$target.removeClass(np.$prev.data('class') || "");
+ }
+ if (np.$next) {
+ this.$target.addClass(np.$next.data('class') || "");
+ }
+ },
+ /* set_active
+ * select and set item active or not (add highlight item and his parents)
+ * called before start
+ */
+ set_active: function () {
+ var self = this;
+ this.$el.find('li').removeClass("active");
+ var $active = this.$el.find('li[data-class]')
+ .filter(function () {
+ var $li = $(this);
+ return ($li.data('class') && self.$target.hasClass($li.data('class')));
+ })
+ .first()
+ .addClass("active");
+ this.$el.find('li:has(li[data-class].active)').addClass("active");
+ }
+ });
+
+
+ website.snippet.styleRegistry.background = website.snippet.StyleEditor.extend({
+ _get_bg: function () {
+ return this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
+ },
+ _set_bg: function (src) {
+ this.$target.css("background-image", src && src !== "" ? 'url(' + src + ')' : "");
+ },
+ start: function () {
+ this._super();
+ var src = this._get_bg();
+ this.$el.find("li[data-class].active.oe_custom_bg").data("src", src);
+ },
+ select: function(event, np) {
+ var self = this;
+ this._super(event, np);
+ if (np.$next) {
+ if (np.$next.hasClass("oe_custom_bg")) {
+ var editor = new website.editor.ImageDialog();
+ editor.on('start', self, function (o) {o.url = np.$prev && np.$prev.data("src") || np.$next && np.$next.data("src") || "";});
+ editor.on('save', self, function (o) {
+ self._set_bg(o.url);
+ np.$next.data("src", o.url);
+ self.$target.trigger("snippet-style-change", [self, np]);
+ });
+ editor.on('cancel', self, function () {
+ if (!np.$prev || np.$prev.data("src") === "") {
+ self.$target.removeClass(np.$next.data("class"));
+ self.$target.trigger("snippet-style-change", [self, np]);
+ }
+ });
+ editor.appendTo($('body'));
+ } else {
+ this._set_bg(np.$next.data("src"));
+ }
+ } else {
+ this._set_bg(false);
+ this.$target.removeClass(np.$prev.data("class"));
+ }
+ },
+ preview: function (event, np) {
+ this._super(event, np);
+ if (np.$next) {
+ this._set_bg(np.$next.data("src"));
+ }
+ },
+ set_active: function () {
+ var self = this;
+ var bg = self.$target.css("background-image");
+ this.$el.find('li').removeClass("active");
+ var $active = this.$el.find('li[data-class]')
+ .filter(function () {
+ var $li = $(this);
+ return ($li.data('src') && bg.indexOf($li.data('src')) >= 0) ||
+ (!$li.data('src') && self.$target.hasClass($li.data('class')));
+ })
+ .first();
+ if (!$active.length) {
+ $active = this.$target.css("background-image") !== 'none' ?
+ this.$el.find('li[data-class].oe_custom_bg') :
+ this.$el.find('li[data-class=""]');
+ }
+ $active.addClass("active");
+ this.$el.find('li:has(li[data-class].active)').addClass("active");
+ }
+ });
+
+
+ website.snippet.editorRegistry = {};
+ website.snippet.Editor = openerp.Class.extend({
+ init: function (parent, dom) {
+ this.parent = parent;
+ this.$target = $(dom);
+ this.$overlay = this.$target.data('overlay');
+ this.snippet_id = this.$target.data("snippet-id");
+ this._readXMLData();
+ this.load_style_options();
+ this.get_parent_block();
+ this.start();
+ },
+
+ /*
+ * _readXMLData
+ * Read data XML and set value into:
+ * this.$el :
+ * all xml data
+ * this.$overlay :
+ * Dom hover the $target who content options
+ * this.$editor :
+ * content of .oe_snippet_options
+ * Displayed into the overlay options on focus
+ */
+ _readXMLData: function() {
+ var self = this;
+ this.$el = this.parent.$snippets.filter(function () { return $(this).data("snippet-id") == self.snippet_id; }).clone();
+ this.$editor = this.$el.find(".oe_snippet_options");
+ var $options = this.$overlay.find(".oe_overlay_options");
+ this.$editor.prependTo($options.find(".oe_options ul"));
+ if ($options.find(".oe_options ul li").length) {
+ $options.find(".oe_options").removeClass("hidden");
+ }
+ },
+
+
+ // 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.parent.hide();
+ self.parent.editor_busy = true;
+ self.size = {
+ width: self.$target.width(),
+ height: self.$target.height()
+ };
+ self.$target.after("<div class='oe_drop_clone' style='display: none;'/>");
+ self.$target.detach();
+ self.$overlay.addClass("hidden");
+
+ self.parent.activate_insertion_zones({
+ siblings: self.$el ? self.$el.data('selector-siblings') : false,
+ children: self.$el ? self.$el.data('selector-children') : false,
+ vertical_children: self.$el ? self.$el.data('selector-vertical-children') : false,
+ });
+
+ $("body").addClass('move-important');
+
+ self._drag_and_drop_after_insert_dropzone();
+ self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
+ },
+ _drag_and_drop_stop: function (){
+ var self = this;
+ if (!self.dropped) {
+ $(".oe_drop_clone").after(self.$target);
+ }
+ self.$overlay.removeClass("hidden");
+ $("body").removeClass('move-important');
+ $('.oe_drop_zone').droppable('destroy').remove();
+ $(".oe_drop_clone, .oe_drop_to_remove").remove();
+ self.parent.editor_busy = false;
+ self.get_parent_block();
+ setTimeout(function () {self.parent.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');
+ _.each(website.snippet.styles, function (val) {
+ if (!self.parent.dom_filter(val.selector).is(self.$target)) {
+ return;
+ }
+ var Editor = website.snippet.styleRegistry[val['snippet-style-id']] || website.snippet.StyleEditor;
+ var editor = new Editor(self, self.$target, val['snippet-style-id']);
+ $ul.prepend(editor.$el.addClass("snippet-style-" + val['snippet-style-id']));
+ });
+
+ if ($ul.find("li").length) {
+ $styles.removeClass("hidden");
+ }
+ },
+
+ get_parent_block: function () {
+ var self = this;
+ var $button = this.$overlay.find('.oe_snippet_parent');
+ var $parent = this.$target.parents("[data-snippet-id]:first");
+ if ($parent.length) {
+ $button.removeClass("hidden");
+ $button.off("click").on('click', function (event) {
+ event.preventDefault();
+ setTimeout(function () {
+ self.parent.make_active($parent);
+ }, 0);
+ });
+ } else {
+ $button.addClass("hidden");
+ }
+ },
+
+ /*
+ * start
+ * This method is called after init and _readXMLData
+ */
+ start: function () {
+ var self = this;
+ this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this));
+ this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this));
+ this._drag_and_drop();
+ },
+
+ on_clone: function () {
+ var $clone = this.$target.clone(false);
+ this.$target.after($clone);
+ return false;
+ },
+
+ on_remove: function () {
+ this.onBlur();
+ var index = _.indexOf(this.parent.snippets, this.$target.get(0));
+ delete this.parent.snippets[index];
+ this.$target.remove();
+ this.$overlay.remove();
+ return false;
+ },
+
+ /*
+ * drop_and_build_snippet
+ * This method is called just after that a thumbnail is drag and dropped into a drop zone
+ * (after the insertion of this.$body, if this.$body exists)
+ */
+ drop_and_build_snippet: function ($target) {
+ },
+
+ /* onFocus
+ * This method is called when the user click inside the snippet in the dom
+ */
+ onFocus : function () {
+ this.$overlay.addClass('oe_active');
+ },
+
+ /* onFocus
+ * This method is called when the user click outside the snippet in the dom, after a focus
+ */
+ onBlur : function () {
+ this.$overlay.removeClass('oe_active');
+ },
+
+ /* clean_for_save
+ * function called just before save vue
+ */
+ clean_for_save: function () {
+ this.$target.removeAttr('contentEditable')
+ .find('*').removeAttr('contentEditable');
+ this.$target.removeAttr('attributeEditable')
+ .find('*').removeAttr('attributeEditable');
+ },
+ });
+
+
+ website.snippet.editorRegistry.resize = website.snippet.Editor.extend({
+ start: function () {
+ var self = this;
+ this._super();
+ var $box = $(openerp.qweb.render("website.snippets.resize"));
+
+ var resize_values = this.getSize();
+ if (!resize_values.n) $box.find(".oe_handle.n").remove();
+ if (!resize_values.s) $box.find(".oe_handle.s").remove();
+ if (!resize_values.e) $box.find(".oe_handle.e").remove();
+ if (!resize_values.w) $box.find(".oe_handle.w").remove();
+ if (!resize_values.size) $box.find(".oe_handle.size").remove();
+
+ this.$overlay.append($box.find(".oe_handles").html());
+
+ this.$overlay.find(".oe_handle:not(:has(.oe_handle_button)), .oe_handle .oe_handle_button").on('mousedown', function (event){
+ event.preventDefault();
+
+ var $handle = $(this);
+
+ var resize_values = self.getSize();
+ var compass = false;
+ var XY = false;
+ if ($handle.hasClass('n')) {
+ compass = 'n';
+ XY = 'Y';
+ }
+ else if ($handle.hasClass('s')) {
+ compass = 's';
+ XY = 'Y';
+ }
+ else if ($handle.hasClass('e')) {
+ compass = 'e';
+ XY = 'X';
+ }
+ else if ($handle.hasClass('w')) {
+ compass = 'w';
+ XY = 'X';
+ }
+ else if ($handle.hasClass('size')) {
+ compass = 'size';
+ XY = 'Y';
+ }
+
+ var resize = resize_values[compass];
+ if (!resize) return;
+
+
+ if (compass === 'size') {
+ var offset = self.$target.offset().top;
+ if (self.$target.css("background").match(/rgba\(0, 0, 0, 0\)/)) {
+ self.$target.addClass("resize_editor_busy");
+ }
+ } else {
+ var xy = event['page'+XY];
+ var current = resize[2] || 0;
+ _.each(resize[0], function (val, key) {
+ if (self.$target.hasClass(val)) {
+ current = key;
+ }
+ });
+ var begin = current;
+ var beginClass = self.$target.attr("class");
+ var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
+ }
+
+ self.parent.editor_busy = true;
+
+ var cursor = $handle.css("cursor")+'-important';
+ var $body = $(document.body);
+ $body.addClass(cursor);
+
+ var body_mousemove = function (event){
+ event.preventDefault();
+ if (compass === 'size') {
+ var dy = event.pageY-offset;
+ dy = dy - dy%resize;
+ if (dy <= 0) dy = resize;
+ self.$target.css("height", dy+"px");
+ self.on_resize(compass, null, dy);
+ self.parent.cover_target(self.$overlay, self.$target);
+ return;
+ }
+ var dd = event['page'+XY] - xy + resize[1][begin];
+ var next = current+1 === resize[1].length ? current : (current+1);
+ var prev = current ? (current-1) : 0;
+
+ var change = false;
+ if (dd > (2*resize[1][next] + resize[1][current])/3) {
+ self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
+ self.$target.addClass(resize[0][next]);
+ current = next;
+ change = true;
+ }
+ if (prev != current && dd < (2*resize[1][prev] + resize[1][current])/3) {
+ self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
+ self.$target.addClass(resize[0][prev]);
+ current = prev;
+ change = true;
+ }
+
+ if (change) {
+ self.on_resize(compass, beginClass, current);
+ self.parent.cover_target(self.$overlay, self.$target);
+ }
+ };
+
+ var body_mouseup = function(){
+ $body.unbind('mousemove', body_mousemove);
+ $body.unbind('mouseup', body_mouseup);
+ $body.removeClass(cursor);
+ self.parent.editor_busy = false;
+ self.$target.removeClass("resize_editor_busy");
+ };
+ $body.mousemove(body_mousemove);
+ $body.mouseup(body_mouseup);
+ });
+ },
+ getSize: function () {
+ var grid = [0,4,8,16,32,48,64,92,128];
+ this.grid = {
+ // list of class (Array), grid (Array), default value (INT)
+ n: [_.map(grid, function (v) {return 'mt'+v;}), grid],
+ s: [_.map(grid, function (v) {return 'mb'+v;}), grid],
+ // INT if the user can resize the snippet (resizing per INT px)
+ size: null
+ };
+ return this.grid;
+ },
+
+ /* on_resize
+ * called when the box is resizing and the class change, before the cover_target
+ * @compass: resize direction : 'n', 's', 'e', 'w'
+ * @beginClass: attributes class at the begin
+ * @current: curent increment in this.grid
+ */
+ on_resize: function (compass, beginClass, current) {
+
+ }
+ });
+
+ website.snippet.editorRegistry.colmd = website.snippet.editorRegistry.resize.extend({
+ getSize: function () {
+ this.grid = this._super();
+ var width = this.$target.parents(".row:first").first().outerWidth();
+
+ var grid = [1,2,3,4,5,6,7,8,9,10,11,12];
+ this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})];
+
+ var grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11];
+ this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12];
+
+ return this.grid;
+ },
+ _drag_and_drop_after_insert_dropzone: function(){
+ var self = this;
+ var $zones = $(".row:has(> .oe_drop_zone)").each(function () {
+ var $row = $(this);
+ var width = $row.innerWidth();
+ var pos = 0;
+ while (width > pos + self.size.width) {
+ var $last = $row.find("> .oe_drop_zone:last");
+ $last.each(function () {
+ pos = $(this).position().left;
+ });
+ if (width > pos + self.size.width) {
+ $row.append("<div class='col-md-1 oe_drop_to_remove'/>");
+ var $add_drop = $last.clone();
+ $row.append($add_drop);
+ self._drag_and_drop_active_drop_zone($add_drop);
+ }
+ }
+ });
+ },
+ _drag_and_drop_start: function () {
+ this._super();
+ this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, ''));
+ },
+ _drag_and_drop_stop: function () {
+ this.$target.addClass("col-md-offset-" + this.$target.prevAll(".oe_drop_to_remove").length);
+ this._super();
+ },
+ hide_remove_button: function() {
+ this.$overlay.find('.oe_snippet_remove').toggleClass("hidden",
+ !this.$target.siblings().length && this.$target.parents("[data-snippet-id]:first").find("[data-snippet-id='colmd']").length > 1);
+ },
+ onFocus : function () {
+ this._super();
+ this.hide_remove_button();
+ },
+ on_clone: function () {
+ var $clone = this.$target.clone(false);
+ var _class = $clone.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, '');
+ _class += ' col-md-1';
+ $clone.attr("class", _class);
+ this.$target.after($clone);
+ this.hide_remove_button();
+ return false;
+ },
+ on_remove: function () {
+ if (!this.$target.siblings().length) {
+ var $parent = this.$target.parents("[data-snippet-id]:first");
+ if($parent.find("[data-snippet-id='colmd']").length > 1) {
+ return false;
+ } else {
+ if (!$parent.data("snippet-editor")) {
+ this.parent.create_overlay($parent);
+ }
+ $parent.data("snippet-editor").on_remove();
+ }
+ }
+ this._super();
+ this.hide_remove_button();
+ return false;
+ },
+ on_resize: function (compass, beginClass, current) {
+ if (compass !== 'w')
+ return;
+
+ // don't change the right border position when we change the offset (replace col size)
+ var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0);
+ var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0);
+ var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0);
+ if (offset < 0) {
+ offset = 0;
+ }
+ var colSize = beginCol - (offset - beginOffset);
+ if (colSize <= 0) {
+ colSize = 1;
+ offset = beginOffset + beginCol - 1;
+ }
+ this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, ''));
+
+ this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize));
+ if (offset > 0) {
+ this.$target.addClass('col-md-offset-' + offset);
+ }
+ },
+ });
+
+ website.snippet.editorRegistry.slider = website.snippet.editorRegistry.resize.extend({
+ drop_and_build_snippet: function() {
+ var id = $(".carousel").length;
+ this.id = "myCarousel" + id;
+ this.$target.attr("id", this.id);
+ this.$target.find(".carousel-control").attr("href", "#myCarousel" + id);
+ this.$target.find("[data-target]").attr("data-target", "#myCarousel" + id);
+
+ this.rebind_event();
+ },
+ // rebind event to active carousel on edit mode
+ rebind_event: function () {
+ var self = this;
+ this.$target.find('.carousel-indicators [data-target]').off('click').on('click', function () {
+ self.$target.carousel(+$(this).data('slide-to')); });
+
+ this.$target.attr('contentEditable', 'false');
+ this.$target.find('.oe_structure, .content>.row').attr('contentEditable', 'true');
+
+ this.$target.carousel('pause');
+ },
+ clean_for_save: function () {
+ this._super();
+ this.$target.find(".item").removeClass("next prev left right");
+ if(!this.$target.find(".item.active").length) {
+ this.$target.find(".item:first").addClass("active");
+ }
+ },
+ onFocus: function () {
+ this._super();
+ this.$target.carousel('pause');
+ },
+ onBlur: function () {
+ this._super();
+ this.$target.carousel('cycle');
+ },
+ start : function () {
+ var self = this;
+ this._super();
+ this.id = this.$target.attr("id");
+ this.$inner = this.$target.find('.carousel-inner');
+ this.$indicators = this.$target.find('.carousel-indicators');
+
+ this.$editor.find(".js_add").on('click', function () {self.on_add_slide(); return false;});
+ this.$editor.find(".js_remove").on('click', function () {self.on_remove_slide(); return false;});
+
+ this.rebind_event();
+ },
+ on_add_slide: function () {
+ var self = this;
+ var cycle = this.$inner.find('.item').length;
+ var $active = this.$inner.find('.item.active, .item.prev, .item.next').first();
+ var index = $active.index();
+ this.$target.find('.carousel-control, .carousel-indicators').removeClass("hidden");
+ this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>');
+
+ var $clone = this.$el.find(".item.active").clone();
+
+ // insert
+ $clone.removeClass('active').insertAfter($active);
+ setTimeout(function() {
+ self.$target.carousel().carousel(++index);
+ self.rebind_event();
+ },0);
+ return $clone;
+ },
+ on_remove_slide: function () {
+ if (this.remove_process) {
+ return;
+ }
+ var self = this;
+ var new_index = 0;
+ var cycle = this.$inner.find('.item').length - 1;
+ var index = this.$inner.find('.item.active').index();
+
+ if (cycle > 0) {
+ this.remove_process = true;
+ var $el = this.$inner.find('.item.active');
+ self.$target.on('slid.bs.carousel', function (event) {
+ $el.remove();
+ self.$indicators.find("li:last").remove();
+ self.$target.off('slid.bs.carousel');
+ self.rebind_event();
+ self.remove_process = false;
+ if (cycle == 1) {
+ self.on_remove_slide(event);
+ }
+ });
+ setTimeout(function () {
+ self.$target.carousel( index > 0 ? --index : cycle );
+ }, 500);
+ } else {
+ this.$target.find('.carousel-control, .carousel-indicators').addClass("hidden");
+ }
+ },
+ });
+
+ website.snippet.editorRegistry.carousel = website.snippet.editorRegistry.slider.extend({
+ getSize: function () {
+ this.grid = this._super();
+ this.grid.size = 8;
+ return this.grid;
+ },
+ clean_for_save: function () {
+ this._super();
+ this.$target.css("background-image", "");
+ this.$target.removeClass(this._class);
+ },
+ load_style_options : function () {
+ this._super();
+ $(".snippet-style-size li[data-class='']").remove();
+ },
+ start : function () {
+ var self = this;
+ this._super();
+
+ // set background and prepare to clean for save
+ var add_class = function (c){
+ if (c) self._class = (self._class || "").replace(new RegExp("[ ]+" + c.replace(" ", "|[ ]+")), '') + ' ' + c;
+ return self._class || "";
+ };
+ this.$target.on('snippet-style-change snippet-style-preview', function (event, style, np) {
+ var $active = self.$target.find(".item.active");
+ if (style['snippet-style-id'] === "size") return;
+ if (style['snippet-style-id'] === "background") {
+ $active.css("background-image", self.$target.css("background-image"));
+ }
+ if (np.$prev) {
+ $active.removeClass(np.$prev.data("class"));
+ }
+ if (np.$next) {
+ $active.addClass(np.$next.data("class"));
+ add_class(np.$next.data("class"));
+ }
+ });
+ this.$target.on('slid', function () { // slide.bs.carousel
+ var $active = self.$target.find(".item.active");
+ self.$target
+ .css("background-image", $active.css("background-image"))
+ .removeClass(add_class($active.attr("class")))
+ .addClass($active.attr("class"))
+ .trigger("snippet-style-reset");
+
+ self.$target.carousel();
+ });
+ this.$target.trigger('slid');
+ },
+ on_add_slide: function () {
+ var $clone = this._super();
+
+ // choose an other background
+ var $styles = this.$target.data("snippet-style-ids").background.$el.find("li[data-class]:not(.oe_custom_bg)");
+ var styles_index = $styles.index($styles.filter(".active")[0]);
+ var $select = $($styles[styles_index >= $styles.length-1 ? 0 : styles_index+1]);
+ $clone.css("background-image", $select.data("src") ? "url('"+ $select.data("src") +"')" : "");
+ $clone.addClass($select.data("class") || "");
+
+ return $clone;
+ },
+ // rebind event to active carousel on edit mode
+ rebind_event: function () {
+ var self = this;
+ this.$target.find('.carousel-control').off('click').on('click', function () {
+ self.$target.carousel( $(this).data('slide')); });
+
+ this.$target.find('.carousel-image, .content').attr('contentEditable', 'true');
+ this.$target.find('.carousel-image').attr('attributeEditable', 'true');
+ this._super();
+ },
+ });
+
+ website.snippet.editorRegistry.parallax = website.snippet.editorRegistry.resize.extend({
+ getSize: function () {
+ this.grid = this._super();
+ this.grid.size = 8;
+ return this.grid;
+ },
+ on_resize: function (compass, beginClass, current) {
+ this.$target.data("snippet-view").set_values();
+ },
+ start : function () {
+ var self = this;
+ this._super();
+ this.scroll();
+ this.$target.on('snippet-style-change snippet-style-preview', function () {
+ self.$target.data("snippet-view").set_values();
+ });
+ this.$target.attr('contentEditable', 'false');
+ this.$target.find('> div > .oe_structure').attr('contentEditable', 'true');
+ },
+ scroll: function () {
+ var self = this;
+ var $ul = this.$editor.find('ul[name="parallax-scroll"]');
+ var $li = $ul.find("li");
+ var speed = this.$target.data('scroll-background-ratio') || 0.6 ;
+ $ul.find('[data-value="' + speed + '"]').addClass('active');
+ $li.on('click', function (event) {
+ $li.removeClass("active");
+ $(this).addClass("active");
+ var speed = $(this).data('value');
+ self.$target.attr('data-scroll-background-ratio', speed);
+ self.$target.data("snippet-view").set_values();
+ return false;
+ });
+ this.$target.data("snippet-view").set_values();
+ },
+ clean_for_save: function () {
+ this._super();
+ this.$target.find(".parallax")
+ .css("background-position", '')
+ .removeAttr("data-scroll-background-offset");
+ }
+ });
+
+ /*
+ * data-snippet-id automatically setted
+ * Don't need to add data-snippet-id="..." into the views
+ */
+
+ website.snippet.selector.push([".row > [class*='col-md-']", 'colmd']);
+ website.snippet.selector.push(['hr', 'hr']);
+ website.snippet.selector.push(['blockquote', 'quote']);
+
+})();
diff --git a/addons/website/static/src/js/website.tests.js b/addons/website/static/src/js/website.tests.js
new file mode 100644
index 00000000000..977dc8e1848
--- /dev/null
+++ b/addons/website/static/src/js/website.tests.js
@@ -0,0 +1,40 @@
+var tests = {};
+var droptest = function () {
+ var errors = [];
+ for (var snippet_id in tests) {
+ if (!tests[snippet_id]['activated'] || !tests[snippet_id]['dropped']){
+ console.log("Can't dropped or activated snippet: " + snippet_id);
+ }
+ }
+ if (errors.length) {
+ console.log(tests);
+ throw new Error("Can't dropped or activated at least one snippet");
+ }
+ $("#oe_snippets").off('snippet-activated snippet-dropped');
+};
+var droptesttime = setTimeout(droptest,0);
+
+$("#oe_snippets").off('snippet-activated snippet-dropped')
+ .on('snippet-activated', function (event, dom) {
+ tests[$(dom).data('src-snippet-id')]['activated'] = true;
+ clearTimeout(droptesttime);
+ droptesttime = setTimeout(droptest,0);
+ })
+ .on('snippet-dropped', function (event, dom, src_snipped_id) {
+ tests[$(dom).data('src-snippet-id')]['dropped'] = true;
+ clearTimeout(droptesttime);
+ droptesttime = setTimeout(droptest,0);
+ });
+var $thumbnails = $('#oe_snippets div.oe_snippet[data-snippet-id] .oe_snippet_thumbnail');
+$thumbnails.each(function () {
+ var $thumbnail = $(this);
+ tests[$thumbnail.parent().data('snippet-id')] = {};
+ var position = $thumbnail.position();
+ $thumbnail.trigger( $.Event( "mousedown", { which: 1, pageX: position.left, pageY: position.top } ) );
+ $thumbnail.trigger( $.Event( "mousemove", { which: 1, pageX: position.left+100, pageY: position.top+100 } ) );
+ $first_drop = $(".oe_drop_zone").first();
+ position = $first_drop.position();
+ $first_drop.trigger( $.Event( "mouseup", { which: 1, pageX: position.left+20, pageY: position.top+20 } ) );
+ clearTimeout(droptesttime);
+ droptesttime = setTimeout(droptest,0);
+});
diff --git a/addons/website/static/src/js/website.tour.banner.js b/addons/website/static/src/js/website.tour.banner.js
new file mode 100644
index 00000000000..2c26ae34e98
--- /dev/null
+++ b/addons/website/static/src/js/website.tour.banner.js
@@ -0,0 +1,118 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+
+ website.EditorBar.include({
+ start: function () {
+ this.registerTour(new website.BannerTour(this));
+ return this._super();
+ },
+ });
+
+ website.BannerTour = website.Tour.extend({
+ id: 'banner',
+ name: "Insert a banner",
+ path: '/page/website.homepage',
+ init: function (editor) {
+ var self = this;
+ self.steps = [
+ {
+ title: "Welcome to your website!",
+ content: "This tutorial will guide you to build your home page. We will start by adding a banner.",
+ template: self.popover({ next: "Start Tutorial", end: "Skip It" }),
+ backdrop: true,
+ },
+ {
+ waitNot: '.popover.tour',
+ element: 'button[data-action=edit]',
+ placement: 'bottom',
+ title: "Edit this page",
+ content: "Every page of your website can be modified through the <i>Edit</i> button.",
+ template: self.popover({ fixed: true }),
+ },
+ {
+ element: 'button[data-action=snippet]',
+ placement: 'bottom',
+ title: "Insert building blocks",
+ content: "To add content in a page, you can insert building blocks.",
+ template: self.popover({ fixed: true }),
+ },
+ {
+ snippet: 'carousel',
+ placement: 'bottom',
+ title: "Drag & Drop a Banner",
+ content: "Drag the Banner block and drop it in your page.",
+ template: self.popover({ fixed: true }),
+ },
+ {
+ waitFor: '.oe_overlay_options .oe_options:visible',
+ element: '#wrap [data-snippet-id=carousel]:first .carousel-caption',
+ sampleText: 'My Title',
+ placement: 'top',
+ title: "Customize banner's text",
+ content: "Click in the text and start editing it. Click continue once it's done.",
+ },
+ {
+ waitNot: '#wrap [data-snippet-id=carousel]:first .carousel-caption:contains("Your Banner Title")',
+ element: '.oe_overlay_options .oe_options',
+ placement: 'left',
+ title: "Customize the banner",
+ content: "Customize any block through this menu. Try to change the background of the banner.",
+ template: self.popover({ next: "Continue" }),
+ },
+ {
+ waitNot: '.popover.tour',
+ element: 'button[data-action=snippet]',
+ placement: 'bottom',
+ title: "Add Another Block",
+ content: "Let's add another building block to your page.",
+ template: self.popover({ fixed: true }),
+ },
+ {
+ snippet: 'three-columns',
+ placement: 'bottom',
+ title: "Drag & Drop a Block",
+ content: "Drag the <em>'3 Columns'</em> block and drop it below the banner.",
+ template: self.popover({ fixed: true }),
+ },
+ {
+ waitFor: '.oe_overlay_options .oe_options:visible',
+ element: 'button[data-action=save]',
+ placement: 'right',
+ title: "Save your modifications",
+ content: "Publish your page by clicking on the <em>'Save'</em> button.",
+ template: self.popover({ fixed: true }),
+ },
+ {
+ waitFor: 'button[data-action=edit]:visible',
+ title: "Congratulation!",
+ content: "Your homepage has been updated.",
+ template: self.popover({ next: "Continue" }),
+ },
+ {
+ waitNot: '.popover.tour',
+ element: 'a[data-action=show-mobile-preview]',
+ placement: 'bottom',
+ title: "Test Your Mobile Version",
+ content: "Let's check how your homepage looks like on mobile devices.",
+ template: self.popover({ fixed: true }),
+ },
+ {
+ element: 'button[data-dismiss=modal]',
+ placement: 'right',
+ title: "Close Mobile Preview",
+ content: "Scroll in the mobile preview to test the rendering. Once it's ok, close this dialog.",
+ },
+ {
+ waitNot: '.modal',
+ title: "Congrats",
+ content: "Congratulation. This tour is finished.",
+ template: self.popover({ fixed: true, next: "Close Tutorial" }),
+ },
+ ];
+ return this._super();
+ },
+ });
+
+}());
diff --git a/addons/website/static/src/js/website.tour.js b/addons/website/static/src/js/website.tour.js
new file mode 100644
index 00000000000..d0611419904
--- /dev/null
+++ b/addons/website/static/src/js/website.tour.js
@@ -0,0 +1,439 @@
+(function () {
+ 'use strict';
+
+var website = openerp.website;
+website.add_template_file('/website/static/src/xml/website.tour.xml');
+
+website.EditorBar.include({
+ tours: [],
+ start: function () {
+ // $('.tour-backdrop').click(function (e) {
+ // e.stopImmediatePropagation();
+ // e.preventDefault();
+ // });
+ var self = this;
+ var menu = $('#help-menu');
+ _.each(this.tours, function (tour) {
+ var $menuItem = $($.parseHTML('<li><a href="#">'+tour.name+'</a></li>'));
+ $menuItem.click(function () {
+ tour.reset();
+ tour.trigger();
+ });
+ menu.append($menuItem);
+ });
+
+ this.waitRTEReady = false;
+ this.on('rte:called', this, function () {self.waitRTEReady = true; });
+ this.on('rte:ready', this, function () {self.waitRTEReady = false;});
+
+ var res = this._super();
+ website.Tour.waitReady.call(this, this.testRunning);
+ return res;
+ },
+ registerTour: function (tour) {
+ website.Tour.add(tour);
+ this.tours.push(tour);
+ },
+ testRunning: function () {
+ if (this.waitRTEReady) {
+ this.on('rte:ready', this, function () {
+ website.Tour.each(function () {
+ this.running();
+ });
+ });
+ } else {
+ website.Tour.each(function () {
+ this.running();
+ });
+ }
+ }
+});
+
+
+/////////////////////////////////////////////////
+
+
+/* jQuery selector to match exact text inside an element
+ * :containsExact() - case insensitive
+ * :containsExactCase() - case sensitive
+ * :containsRegex() - set by user ( use: $(el).find(':containsRegex(/(red|blue|yellow)/gi)') )
+ */
+$.extend($.expr[':'],{
+ containsExact: function(a,i,m){
+ return $.trim(a.innerHTML.toLowerCase()) === m[3].toLowerCase();
+ },
+ containsExactCase: function(a,i,m){
+ return $.trim(a.innerHTML) === m[3];
+ },
+ // Note all escaped characters need to be double escaped
+ // inside of the containsRegex, so "\(" needs to be "\\("
+ containsRegex: function(a,i,m){
+ var regreg = /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})$/,
+ reg = regreg.exec(m[3]);
+ return reg ? new RegExp(reg[1], reg[2]).test($.trim(a.innerHTML)) : false;
+ }
+});
+$.ajaxSetup({
+ beforeSend:function(){
+ $.ajaxBusy = ($.ajaxBusy|0) + 1;
+ },
+ complete:function(){
+ $.ajaxBusy--;
+ }
+});
+
+
+website.Tour = openerp.Class.extend({
+ steps: [],
+ defaultDelay: 50, //ms
+ localStorage: window.localStorage,
+ init: function (url) {
+ this.tour = new Tour({
+ name: this.id,
+ storage: this.tourStorage,
+ keyboard: false,
+ template: this.popover(),
+ onHide: function () {
+ window.scrollTo(0, 0);
+ }
+ });
+ this.registerSteps();
+ },
+
+ run: function (automatic, force) {
+ if (force) this.reset();
+
+ for (var k in this.localStorage) {
+ if (!k.indexOf("tour-") && k.indexOf("-test") > -1) return;
+ }
+
+ // only one test running
+ if (website.Tour.busy) return;
+
+ website.Tour.busy = true;
+ this.localStorage.setItem("tour-"+this.id+"-test", 0);
+
+ website.Tour.waitReady.call(this, function () {this._run(automatic, force);});
+ },
+ running: function () {
+ if (+this.localStorage.getItem("tour-"+this.id+"-test") >= this.steps.length) {
+ this.endTour();
+ return;
+ }
+
+ if (website.Tour.busy || !this.testUrl()) return;
+
+ var self = this;
+ website.Tour.waitReady.call(this, function () {
+ self._running();
+ }, 500);
+ },
+
+ _run: function (automatic, force) {
+ this.reset();
+ this.localStorage.setItem("tour-"+this.id+"-test", 0);
+ if (automatic) {
+ this.localStorage.setItem("tour-"+this.id+"-test-automatic", true);
+ }
+ this.nextStep(null, automatic ? this.autoNextStep : null, automatic ? 5000 : null);
+ },
+ _running: function () {
+ var stepId = this.localStorage.getItem("tour-"+this.id+"-test");
+ var automatic = !!this.localStorage.getItem("tour-"+this.id+"-test-automatic");
+
+ if (stepId || this.checkRuningUrl()) {
+
+ if (!this.check(this.step(stepId))) {
+ var step = this.next(stepId);
+ stepId = step ? step.stepId : stepId;
+ }
+ website.Tour.busy = true;
+ this.nextStep(stepId, automatic ? this.autoNextStep : null, automatic ? 5000 : null);
+ }
+ },
+
+ reset: function () {
+ website.Tour.busy = false;
+ for (var k in this.steps) {
+ this.steps[k].busy = false;
+ }
+ clearTimeout(self.timer);
+ clearTimeout(self.testtimer);
+
+ for (var k in this.localStorage) {
+ if (!k.indexOf("tour-") || !k.indexOf(this.id)) {
+ this.localStorage.removeItem(k);
+ }
+ }
+
+ $('.popover.tour').remove();
+ },
+ trigger: function (automatic) {
+ this.reset();
+ if (this.path) {
+ this.localStorage.setItem("tour-"+this.id+"-test", 0);
+ if (automatic) this.localStorage.setItem("tour-"+this.id+"-test-automatic", true);
+ var path = this.path.split('?');
+ window.location.href = path[0] + "?tutorial."+this.id+"=true" + path.slice(1, path.length).join("?");
+ } else {
+ this.run(automatic);
+ }
+ },
+ testUrl: function () {
+ return !this.testPath || this.testPath.test(window.location.href);
+ },
+ checkRuningUrl: function () {
+ return window.location.search.indexOf("tutorial."+this.id+"=true") > -1;
+ },
+
+ registerSteps: function () {
+ for (var index=0, len=this.steps.length; index<len; index++) {
+ var step = this.steps[index];
+ step.stepId = step.stepId || ""+index;
+
+ if (!step.waitNot && index > 0 && $(this.steps[index-1].template).has("button[data-role='next']").size()) {
+ step.waitNot = '.popover.tour';
+ }
+ if (!step.waitFor && index > 0 && this.steps[index-1].snippet) {
+ step.waitFor = '.oe_overlay_options .oe_options:visible';
+ }
+
+ step._title = step.title;
+ step.title = openerp.qweb.render('website.tour_popover_title', { title: step.title });
+ if (!step.element) step.orphan = true;
+ if (step.snippet) {
+ step.element = '#oe_snippets div.oe_snippet[data-snippet-id="'+step.snippet+'"] .oe_snippet_thumbnail';
+ }
+
+ }
+
+ if ($(this.steps[index-1].template).has("button[data-role='next']").size()) {
+ var step = {
+ stepId: index,
+ waitNot: '.popover.tour:visible'
+ };
+ this.steps.push(step);
+ }
+
+ this.tour.addSteps(this.steps);
+ },
+
+ popover: function (options) {
+ return openerp.qweb.render('website.tour_popover', options);
+ },
+
+ timer: null,
+ testtimer: null,
+ check: function (step) {
+ return (step &&
+ (!step.element || ($(step.element).size() && $(step.element).is(":visible") && !$(step.element).is(":hidden"))) &&
+ (!step.waitNot || !$(step.waitNot).size()) &&
+ (!step.waitFor || $(step.waitFor).size()));
+ },
+ waitNextStep: function (step, callback, overlaps) {
+ var self = this;
+ var time = new Date().getTime();
+ var timer;
+
+ window.onbeforeunload = function () {
+ clearTimeout(self.timer);
+ clearTimeout(self.testtimer);
+ };
+
+ // check popover activity
+ $(".popover.tour button")
+ .off()
+ .on("click", function () {
+ $(".popover.tour").remove();
+ if (step.busy) return;
+ if (!$(this).is("[data-role='next']")) {
+ clearTimeout(self.timer);
+ step.busy = true;
+ self.tour.end();
+ self.endTour(callback);
+ }
+ });
+
+ function checkNext () {
+ clearTimeout(self.timer);
+ if (step.busy) return;
+ if (self.check(step)) {
+ step.busy = true;
+ // use an other timeout for cke dom loading
+ setTimeout(function () {
+ self.nextStep(step.stepId, callback, overlaps);
+ }, self.defaultDelay);
+ } else if (!overlaps || new Date().getTime() - time < overlaps) {
+ self.timer = setTimeout(checkNext, self.defaultDelay);
+ } else {
+ self.reset();
+ throw new Error("Time overlaps to arrive to step " + step.stepId + ": '" + step._title + "'");
+ }
+ }
+ checkNext();
+ },
+ step: function (stepId) {
+ var steps = this.steps.slice(0,this.steps.length),
+ step;
+ while (step = steps.shift()) {
+ if (!stepId || step.stepId === stepId)
+ return step;
+ }
+ return null;
+ },
+ next: function (stepId) {
+ var steps = this.steps.slice(0,this.steps.length),
+ step, next, index=0;
+ while (step = steps.shift()) {
+ if (!stepId || step.stepId === stepId) {
+ // clear popover (fix for boostrap tour if the element is removed before destroy popover)
+ $(".popover.tour").remove();
+ // go to step in bootstrap tour
+ this.tour.goto(index);
+ if (step.callback) step.callback();
+ next = steps.shift();
+ break;
+ }
+ index++;
+ }
+ return next;
+ },
+ nextStep: function (stepId, callback, overlaps) {
+ var self = this;
+ if (!this.localStorage.getItem("tour-"+this.id+"-test")) return;
+
+ this.localStorage.setItem("tour-"+this.id+"-test", stepId || 0);
+
+ this.current = this.step(stepId);
+ var next = this.next(stepId);
+
+ if (next) {
+ setTimeout(function () {
+ self.waitNextStep(next, callback, overlaps);
+ if (callback) setTimeout(function(){callback.call(self, next);}, self.defaultDelay);
+ }, next && next.wait || 0);
+ } else {
+ this.endTour();
+ }
+ },
+ endTour: function () {
+ console.log('{ "event": "success" }');
+ this.reset();
+ },
+ autoNextStep: function () {
+ var self = this;
+ clearTimeout(self.testtimer);
+
+ function autoStep () {
+ var step = self.current;
+ if (!step) return;
+
+ if (step.autoComplete) {
+ step.autoComplete(tour);
+ }
+
+ var $popover = $(".popover.tour");
+ if ($popover.find("button[data-role='next']:visible").size()) {
+ $popover.find("button[data-role='next']:visible").click();
+ $popover.remove();
+ }
+
+ var $element = $(step.element);
+ if (!$element.size()) return;
+
+ if (step.snippet) {
+
+ var selector = '#oe_snippets div.oe_snippet[data-snippet-id="'+step.snippet+'"] .oe_snippet_thumbnail';
+ self.autoDragAndDropSnippet(selector);
+
+ } else if (step.element.match(/#oe_snippets .* \.oe_snippet_thumbnail/)) {
+
+ self.autoDragAndDropSnippet($element);
+
+ } else if (step.sampleText) {
+
+ $element.trigger($.Event("keydown", { srcElement: $element }));
+ if ($element.is("select") || $element.is("input") ) {
+ $element.val(step.sampleText);
+ } else {
+ $element.html(step.sampleText);
+ }
+ setTimeout(function () {
+ $element.trigger($.Event("keyup", { srcElement: $element }));
+ $element.trigger($.Event("change", { srcElement: $element }));
+ }, self.defaultDelay<<1);
+
+ } else if ($element.is(":visible")) {
+
+ $element.trigger($.Event("mouseenter", { srcElement: $element[0] }));
+ $element.trigger($.Event("mousedown", { srcElement: $element[0] }));
+
+ var evt = document.createEvent("MouseEvents");
+ evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
+ $element[0].dispatchEvent(evt);
+
+ // trigger after for step like: mouseenter, next step click on button display with mouseenter
+ setTimeout(function () {
+ $element.trigger($.Event("mouseup", { srcElement: $element[0] }));
+ $element.trigger($.Event("mouseleave", { srcElement: $element[0] }));
+ }, 1000);
+ }
+ }
+ self.testtimer = setTimeout(autoStep, 100);
+ },
+ autoDragAndDropSnippet: function (selector) {
+ var $thumbnail = $(selector).first();
+ var thumbnailPosition = $thumbnail.position();
+ $thumbnail.trigger($.Event("mousedown", { which: 1, pageX: thumbnailPosition.left, pageY: thumbnailPosition.top }));
+ $thumbnail.trigger($.Event("mousemove", { which: 1, pageX: document.body.scrollWidth/2, pageY: document.body.scrollHeight/2 }));
+ var $dropZone = $(".oe_drop_zone").first();
+ var dropPosition = $dropZone.position();
+ $dropZone.trigger($.Event("mouseup", { which: 1, pageX: dropPosition.left, pageY: dropPosition.top }));
+ },
+
+});
+
+
+website.Tour.tours = {};
+website.Tour.busy = false;
+website.Tour.add = function (tour) {
+ website.Tour.waitReady(function () {
+ tour = tour.id ? tour : new tour();
+ website.Tour.tours[tour.id] = tour;
+ });
+};
+website.Tour.get = function (id) {
+ return website.Tour.tours[id];
+};
+website.Tour.each = function (callback) {
+ website.Tour.waitReady(function () {
+ for (var k in website.Tour.tours) {
+ callback.call(website.Tour.tours[k]);
+ }
+ });
+};
+website.Tour.waitReady = function (callback) {
+ var self = this;
+ $(document).ready(function () {
+ if ($.ajaxBusy == null || $.ajaxBusy) {
+ $(document).ajaxStop(function() {
+ setTimeout(function () {
+ callback.call(self);
+ },0);
+ });
+ }
+ else {
+ setTimeout(function () {
+ callback.call(self);
+ },0);
+ }
+ });
+};
+website.Tour.run_test = function (id) {
+ website.Tour.get(id).trigger(true);
+};
+
+
+
+
+}());
diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js
new file mode 100644
index 00000000000..399e5e731b2
--- /dev/null
+++ b/addons/website/static/src/js/website.translator.js
@@ -0,0 +1,209 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ website.add_template_file('/website/static/src/xml/website.translator.xml');
+ var nodialog = 'website_translator_nodialog';
+
+ website.EditorBar.include({
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click a[data-action=edit_master]': 'edit_master',
+ }),
+ start: function () {
+ var self = this;
+ this.initial_content = {};
+ return this._super.apply(this, arguments).then(function () {
+ self.$("button[data-action=edit]").removeClass("hidden");
+ self.$('button[data-action=edit]')
+ .text("Translate");
+ if (website.is_editable_button) {
+ self.$('button[data-action=edit]')
+ .after(openerp.qweb.render('website.TranslatorAdditionalButtons'));
+ }
+ self.$('.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);
+ });
+ });
+ } else {
+ this.translate().then(function () {
+ mysuper.call(self);
+ });
+ }
+ },
+ 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';
+ window.location = link.attributes.href.value;
+ }
+ },
+ translate: function () {
+ var self = this;
+ this.translations = null;
+ return openerp.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 $editables = $('[data-oe-model="ir.ui.view"]')
+ .not('link, script')
+ .not('.oe_snippets,.oe_snippet, .oe_snippet *')
+ .not('[data-oe-type]');
+
+ $editables.each(function () {
+ var $node = $(this);
+ var view_id = $node.attr('data-oe-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('oe_dirty').removeClass('oe_translatable_todo');
+ }
+ }, 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.childNodes[0].data.trim();
+ var trans = this.translations.filter(function (t) {
+ return t.res_id === view_id && t.value === content;
+ });
+ if (trans.length) {
+ node.setAttribute('data-oe-translation-id', trans[0].id);
+ } else {
+ node.className += ' oe_translatable_todo';
+ }
+ node.contentEditable = true;
+ var nid = _.uniqueId();
+ $(node).attr('data-oe-nodeid', nid);
+ this.initial_content[nid] = content;
+ },
+ save: function () {
+ var self = this;
+ var mysuper = this._super;
+ var trans = {};
+ // this._super.apply(this, arguments);
+ $('.oe_translatable_text.oe_dirty').each(function () {
+ var $node = $(this);
+ var data = $node.data();
+ if (!trans[data.oeTranslationViewId]) {
+ trans[data.oeTranslationViewId] = [];
+ }
+ trans[data.oeTranslationViewId].push({
+ initial_content: self.getInitialContent(this),
+ new_content: $node.text(),
+ translation_id: data.oeTranslationId || null
+ });
+ });
+ openerp.jsonRpc('/website/set_translations', 'call', {
+ 'data': trans,
+ 'lang': website.get_context()['lang'],
+ }).then(function () {
+ mysuper.call(self);
+ }).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';
+ }
+ } 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);
+ }
+ }
+ }
+ },
+ });
+
+ 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"]');
+ }
+ });
+
+ website.TranslatorDialog = openerp.Widget.extend({
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'hidden.bs.modal': 'destroy',
+ 'click button[data-action=activate]': function (ev) {
+ this.trigger('activate');
+ },
+ }),
+ template: 'website.TranslatorDialog',
+ start: function () {
+ this.$el.modal();
+ },
+ });
+})();