diff options
| author | Antony Lesuisse <al@openerp.com> | 2014-01-31 01:51:57 +0100 |
|---|---|---|
| committer | Antony Lesuisse <al@openerp.com> | 2014-01-31 01:51:57 +0100 |
| commit | 4b30f1fcbd8701f82dbbe4e5c8386ee432b063ac (patch) | |
| tree | 93b17a03b45927c20fbb045cda8c8aa09d853409 /addons/website/static/src/js | |
| parent | ba2d4c90307661a79d78e6ce3b274b76b4c62717 (diff) | |
| parent | 1c283ddf242f0681c1d060f3b8658efa3fcaa0a8 (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.js | 302 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.backend.js | 32 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.editor.js | 1692 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.editor.newpage.js | 29 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.js | 294 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.menu.js | 199 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.mobile.js | 28 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.seo.js | 527 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.snippets.animation.js | 122 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.snippets.editor.js | 1443 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.tests.js | 40 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.tour.banner.js | 118 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.tour.js | 439 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.translator.js | 209 |
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 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(); + }, + }); +})(); |
