summaryrefslogtreecommitdiff
path: root/addons/website/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/website/static/src/js')
-rw-r--r--addons/website/static/src/js/website.editor.js651
-rw-r--r--addons/website/static/src/js/website.js795
-rw-r--r--addons/website/static/src/js/website.mobile.js24
-rw-r--r--addons/website/static/src/js/website.seo.js301
-rw-r--r--addons/website/static/src/js/website.snippets.js340
5 files changed, 1338 insertions, 773 deletions
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..32e77acedab
--- /dev/null
+++ b/addons/website/static/src/js/website.editor.js
@@ -0,0 +1,651 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+
+ website.templates.push('/website/static/src/xml/website.editor.xml');
+ website.dom_ready.done(function () {
+ // $.fn.data automatically parses value, '0'|'1' -> 0|1
+ website.is_editable = $(document.documentElement).data('editable');
+ var is_smartphone = $(document.body)[0].clientWidth < 767;
+
+ if (website.is_editable && !is_smartphone) {
+ website.ready().then(website.init_editor);
+ }
+ });
+
+ function link_dialog(editor) {
+ return new website.editor.LinkDialog(editor).appendTo(document.body);
+ }
+ function image_dialog(editor) {
+ return new website.editor.ImageDialog(editor).appendTo(document.body);
+ }
+
+ website.init_editor = function () {
+ CKEDITOR.plugins.add('customdialogs', {
+ requires: 'link,image',
+ init: function (editor) {
+ editor.on('doubleclick', function (evt) {
+ if (evt.data.dialog === 'link') {
+ delete evt.data.dialog;
+ link_dialog(editor);
+ } else if(evt.data.dialog === 'image') {
+ delete evt.data.dialog;
+ image_dialog(editor);
+ }
+ // priority should be smaller than dialog (999) but bigger
+ // than link or image (default=10)
+ }, null, null, 500);
+
+ editor.addCommand('link', {
+ exec: function (editor, data) {
+ link_dialog(editor);
+ return true;
+ },
+ canUndo: false,
+ editorFocus: true,
+ });
+ editor.addCommand('image', {
+ exec: function (editor, data) {
+ image_dialog(editor);
+ return true;
+ },
+ canUndo: false,
+ editorFocus: true,
+ });
+
+ }
+ });
+ var editor = new website.EditorBar();
+ var $body = $(document.body);
+ editor.prependTo($body);
+ $body.css('padding-top', '50px'); // Not working properly: editor.$el.outerHeight());
+ };
+ /* ----- 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 button[data-action=cancel]': 'cancel',
+ },
+ container: 'body',
+ customize_setup: function() {
+ var self = this;
+ var view_name = $(document.documentElement).data('view-xmlid');
+ 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="icon-check%s"></strong> %s</a></li>',
+ item.id, item.active ? '' : '-empty', item.name));
+ }
+ });
+ // Adding Static Menus
+ menu.append('<li class="divider"></li><li><a href="/page/website.themes">Change Theme</a></li>');
+ }
+ );
+ });
+ menu.on('click', 'a', function (event) {
+ var view_id = $(event.currentTarget).data('view-id');
+ openerp.jsonRpc('/website/customize_template_toggle', 'call', {
+ 'view_id': view_id
+ }).then( function(result) {
+ 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'));
+
+ return $.when(
+ this._super.apply(this, arguments),
+ this.rte.prependTo(this.$('#website-top-edit .nav.pull-right'))
+ );
+ },
+ edit: function () {
+ var self = this;
+ 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.$buttons.cancel.add(this.$buttons.snippet).prop('disabled', false)
+ // .add(this.$buttons.save)
+ // .parent().show();
+ //
+ // TODO: span edition changing edition state (save button)
+ var $editables = $('[data-oe-model]')
+ .not('link, script')
+ // FIXME: propagation should make "meta" blocks non-editable in the first place...
+ .not('.oe_snippets,.oe_snippet, .oe_snippet *')
+ .prop('contentEditable', true)
+ .addClass('oe_editable');
+ var $rte_ables = $editables.not('[data-oe-type]');
+ var $raw_editables = $editables.not($rte_ables);
+
+ // temporary: on raw editables, links are still active so an
+ // editable link, containing a link or within a link becomes very
+ // hard to edit. Disable linking for these.
+ $raw_editables.parents('a')
+ .add($raw_editables.find('a'))
+ .on('click', function (e) {
+ e.preventDefault();
+ });
+
+ this.rte.start_edition($rte_ables);
+ $raw_editables.each(function () {
+ observer.observe(this, OBSERVER_CONFIG);
+ }).one('content_changed', function () {
+ $(this).addClass('oe_dirty');
+ self.rte_changed();
+ });
+ },
+ rte_changed: function () {
+ this.$buttons.save.prop('disabled', false);
+ },
+ save: function () {
+ var self = this;
+ var defs = [];
+ observer.disconnect();
+ $('.oe_dirty').each(function (i, v) {
+ var $el = $(this);
+ // TODO: Add a queue with concurrency limit in webclient
+ // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
+ var def = self.saving_mutex.exec(function () {
+ return self.saveElement($el).then(function () {
+ $el.removeClass('oe_dirty');
+ }).fail(function () {
+ var data = $el.data();
+ console.error(_.str.sprintf('Could not save %s#%d#%s', data.oeModel, data.oeId, data.oeField));
+ });
+ });
+ defs.push(def);
+ });
+ return $.when.apply(null, defs).then(function () {
+ window.location.reload();
+ });
+ },
+ saveElement: function ($el) {
+ var data = $el.data();
+ var html = $el.html();
+ var xpath = data.oeXpath;
+ if (xpath) {
+ var $w = $el.clone();
+ $w.removeClass('oe_dirty');
+ _.each(['model', 'id', 'field', 'xpath'], function(d) {$w.removeAttr('data-oe-' + d);});
+ $w
+ .removeClass('oe_editable')
+ .prop('contentEditable', false);
+ html = $w.wrap('<div>').parent().html();
+ }
+ return openerp.jsonRpc('/web/dataset/call', 'call', {
+ model: 'ir.ui.view',
+ method: 'save',
+ args: [data.oeModel, data.oeId, data.oeField, html, xpath]
+ });
+ },
+ cancel: function () {
+ window.location.reload();
+ },
+ });
+
+ /* ----- 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'}));
+
+ start_edition: function ($elements) {
+ var self = this;
+ $elements
+ .not('span, [data-oe-type]')
+ .each(function () {
+ var node = this;
+ var $node = $(node);
+ var editor = CKEDITOR.inline(this, self._config());
+ editor.on('instanceReady', function () {
+ observer.observe(node, OBSERVER_CONFIG);
+ });
+ $node.one('content_changed', function () {
+ $node.addClass('oe_dirty');
+ self.trigger('change');
+ });
+ });
+ },
+
+ _current_editor: function () {
+ return CKEDITOR.currentInstance;
+ },
+ _config: function () {
+ var removed_plugins = [
+ // remove custom context menu
+ 'contextmenu,tabletools,liststyle',
+ // magicline captures mousein/mouseout => draggable does not work
+ 'magicline'
+ ];
+ return {
+ // Disable auto-generated titles
+ // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
+ title: false,
+ removePlugins: removed_plugins.join(','),
+ uiColor: '',
+ // Ensure no config file is loaded
+ customConfig: '',
+ // Disable ACF
+ allowedContent: true,
+ // Don't insert paragraphs around content in e.g. <li>
+ autoParagraph: false,
+ filebrowserImageUploadUrl: "/website/attach",
+ // Support for sharedSpaces in 4.x
+ extraPlugins: 'sharedspace,customdialogs',
+ // 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", "Unlink", "Blockquote", "BulletedList",
+ "NumberedList", "Indent", "Outdent"
+ ]},{
+ name: 'justify', items: [
+ "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
+ ]},{
+ name: 'special', items: [
+ "Image", "Table"
+ ]},{
+ 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'},
+ // emphasis
+ {name: "Muted", element: 'span', attributes: {'class': 'text-muted'}},
+ {name: "Primary", element: 'span', attributes: {'class': 'text-primary'}},
+ {name: "Warning", element: 'span', attributes: {'class': 'text-warning'}},
+ {name: "Danger", element: 'span', attributes: {'class': 'text-danger'}},
+ {name: "Success", element: 'span', attributes: {'class': 'text-success'}},
+ {name: "Info", element: 'span', attributes: {'class': 'text-info'}}
+ ],
+ };
+ },
+ });
+
+ website.editor = { };
+ website.editor.Dialog = openerp.Widget.extend({
+ events: {
+ 'hidden.bs.modal': 'destroy',
+ 'click button.save': 'save',
+ },
+ init: function (editor) {
+ this._super();
+ this.editor = editor;
+ },
+ start: function () {
+ var sup = this._super();
+ this.$el.modal();
+ return sup;
+ },
+ save: 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 .url-source': function (e) { this.changed($(e.target)); },
+ 'click div.existing a': 'select_page',
+ }),
+ init: function (editor) {
+ this._super(editor);
+ // url -> name mapping for existing pages
+ this.pages = Object.create(null);
+ // name -> url mapping for the same
+ this.pages_by_name = Object.create(null);
+ },
+ start: function () {
+ var element;
+ if ((element = this.get_selected_link()) && element.hasAttribute('href')) {
+ this.editor.getSelection().selectElement(element);
+ }
+ this.element = element;
+
+ return $.when(
+ this.fetch_pages().done(this.proxy('fill_pages')),
+ this._super()
+ ).done(this.proxy('bind_data'));
+ },
+ /**
+ * 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);
+ } else {
+ var selection = this.editor.getSelection();
+ var range = selection.getRanges(true)[0];
+
+ if (range.collapsed) {
+ var text = new CKEDITOR.dom.text(label || url);
+ range.insertNode(text);
+ range.selectNodeContents(text);
+ }
+
+ 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);
+ }
+ },
+ save: function () {
+ var self = this, _super = this._super.bind(this);
+ var $e = this.$('.url-source').filter(function () { return !!this.value; });
+
+ var val = $e.val(), done = $.when();
+ if ($e.hasClass('email-address')) {
+ this.make_link('mailto:' + val, false, val);
+ } else if ($e.hasClass('pages')) {
+ // ``val`` is the *name* of the page
+ var url = this.pages_by_name[val];
+ if (!url) {
+ // Create the page, get the URL back
+ done = $.get(_.str.sprintf(
+ '/pagenew/%s?noredirect', encodeURIComponent(val)))
+ .then(function (response) {
+ url = response;
+ });
+ }
+ done.then(function () {
+ self.make_link(url, false, val);
+ });
+ } else {
+ this.make_link(val, this.$('input.window-new').prop('checked'));
+ }
+ done.then(_super);
+ },
+ bind_data: function () {
+ var href = this.element && (this.element.data( 'cke-saved-href')
+ || this.element.getAttribute('href'));
+ if (!href) { return; }
+
+ var match, $control;
+ if (match = /(mailto):(.+)/.exec(href)) {
+ $control = this.$('input.email-address').val(match[2]);
+ } else if(href in this.pages) {
+ $control = this.$('input.pages').val(this.pages[href]);
+ }
+ if (!$control) {
+ $control = this.$('input.url').val(href);
+ }
+
+ this.changed($control);
+
+ this.$('input.window-new').prop(
+ 'checked', this.element.getAttribute('target') === '_blank');
+ },
+ changed: function ($e) {
+ $e.closest('li.list-group-item').addClass('active')
+ .siblings().removeClass('active');
+ this.$('.url-source').not($e).val('');
+ },
+ /**
+ * Selected an existing page in dropdown
+ */
+ select_page: function (e) {
+ e.preventDefault();
+ e.stopPropagation();
+ var $target = $(e.target);
+ this.$('input.pages').val($target.text()).change();
+ // No #dropdown('close'), and using #dropdown('toggle') sur
+ // #closest('.dropdown') makes the dropdown not work correctly
+ $target.closest('.open').removeClass('open')
+ },
+ /**
+ * 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 () {
+ var sel = this.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);
+ return this.editor.elementPath(range.getCommonAncestor())
+ .contains('a');
+
+ },
+ fetch_pages: function () {
+ return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'website',
+ method: 'list_pages',
+ args: [],
+ kwargs: {}
+ });
+ },
+ fill_pages: function (results) {
+ var self = this;
+ var $pages = this.$('div.existing ul').empty();
+ _(results).each(function (result) {
+ self.pages[result.url] = result.name;
+ self.pages_by_name[result.name] = result.url;
+ var $link = $('<a>').attr('href', result.url).text(result.name);
+ $('<li>').append($link).appendTo($pages);
+ });
+ },
+ });
+ 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 .existing-attachments a': 'select_existing',
+ }),
+ start: function () {
+ var selection = this.editor.getSelection();
+ var el = selection && selection.getSelectedElement();
+ this.element = null;
+ if (el && el.is('img')) {
+ this.element = el;
+ this.set_image(el.getAttribute('src'));
+ }
+
+ return $.when(
+ this._super(),
+ this.fetch_existing().then(this.proxy('fetched_existing')));
+ },
+ save: function () {
+ var url = this.$('input.url').val();
+ var element, editor = this.editor;
+ if (!(element = this.element)) {
+ element = editor.document.createElement('img');
+ // 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);
+ }
+ element.setAttribute('src', url);
+ this._super();
+ },
+
+ /**
+ * 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 (e) {
+ 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 () {
+ var image = this.$('input.url').val();
+ if (!image) { return; }
+
+ this.$('img.image-preview').attr('src', image);
+ },
+
+ fetch_existing: function () {
+ // FIXME: lazy load attachments?
+ return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+ model: 'ir.attachment',
+ method: 'search_read',
+ args: [],
+ kwargs: {
+ fields: ['name'],
+ domain: [['res_model', '=', 'ir.ui.view']],
+ order: 'name',
+ }
+ });
+ },
+ fetched_existing: function (records) {
+ // Create rows of 3 records
+ var rows = _(records).chain()
+ .groupBy(function (_, index) { return Math.floor(index / 3); })
+ .values()
+ .value();
+ this.$('.existing-attachments').replaceWith(
+ openerp.qweb.render('website.editor.dialog.image.existing', {rows: rows}));
+ },
+ select_existing: function (e) {
+ e.preventDefault();
+ this.set_image(e.currentTarget.getAttribute('href'));
+ },
+ });
+
+
+ var Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver;
+ var OBSERVER_CONFIG = {
+ childList: true,
+ attributes: true,
+ characterData: true,
+ subtree: true,
+ attributeOldValue: true,
+ };
+ var observer = new 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) {
+ 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':
+ // <br type="_moz"> appears when focusing RTE in FF, ignore
+ return m.addedNodes.length !== 1 || m.addedNodes[0].nodeName !== 'BR';
+ 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'); })
+ });
+})();
diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js
index 2969e52ff63..c6d6f6c9f20 100644
--- a/addons/website/static/src/js/website.js
+++ b/addons/website/static/src/js/website.js
@@ -5,7 +5,7 @@
// The following line can be removed in 2017
openerp.website = website;
- var templates = [
+ var templates = website.templates = [
'/website/static/src/xml/website.xml'
];
@@ -31,301 +31,26 @@
return def;
};
- website.init_editor = function () {
- var editor = new website.EditorBar();
- editor.prependTo($('body'));
- $('body').css('padding-top', '50px'); // Not working properly: editor.$el.outerHeight());
- };
-
- /* ----- 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 button[data-action=cancel]': 'cancel',
- 'click button[data-action=snippet]': 'snippet',
- 'click a[data-action=show-mobile-preview]': 'mobilePreview',
- },
- container: 'body',
- customize_setup: function() {
- var self = this;
- var view_name = $('html').data('view-xmlid');
- 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="nav-header">' + item.name + '</li>');
- } else {
- menu.append(_.str.sprintf('<li><a href="#" data-view-id="%s"><strong class="icon-check%s"></strong> %s</a></li>',
- item.id, item.active ? '' : '-empty', item.name));
- }
- });
- }
- );
- });
- menu.on('click', 'a', function (event) {
- var view_id = $(event.target).data('view-id');
- openerp.jsonRpc('/website/customize_template_toggle', 'call', {
- 'view_id': view_id
- }).then( function(result) {
- window.location.reload();
- });
- });
- $('#mobile-preview').on('hidden', function () {
- $('body').removeClass('oe_stop_scrolling');
- });
- },
- 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]'),
- snippet: this.$('button[data-action=snippet]'),
- };
-
- this.rte = new website.RTE(this);
- this.rte.on('change', this, this.proxy('rte_changed'));
-
- this.snippets = new website.Snippets();
- this.snippets.appendTo($("body"));
- window.snippets = this.snippets;
-
- return $.when(
- this._super.apply(this, arguments),
- this.rte.insertBefore(this.$buttons.snippet.parent())
- );
- },
- edit: function () {
- this.$buttons.edit.prop('disabled', true);
- this.$('#website-top-view').hide();
- this.$('#website-top-edit').show();
-
- // this.$buttons.cancel.add(this.$buttons.snippet).prop('disabled', false)
- // .add(this.$buttons.save)
- // .parent().show();
- //
- // TODO: span edition changing edition state (save button)
- var $editables = $('[data-oe-model]')
- .not('link, script')
- // FIXME: propagation should make "meta" blocks non-editable in the first place...
- .not('.oe_snippets,.oe_snippet, .oe_snippet *')
- .prop('contentEditable', true)
- .addClass('oe_editable');
- var $rte_ables = $editables.not('[data-oe-type]');
- var $raw_editables = $editables.not($rte_ables);
-
- // temporary fix until we fix ckeditor
- $raw_editables.each(function () {
- $(this).parents().add($(this).find('*')).on('click', function(ev) {
- ev.preventDefault();
- ev.stopPropagation();
- });
- });
-
- this.rte.start_edition($rte_ables);
- $raw_editables.on('keydown keypress cut paste', function (e) {
- var $target = $(e.target);
- if ($target.hasClass('oe_dirty')) {
- return;
- }
-
- $target.addClass('oe_dirty');
- this.$buttons.save.prop('disabled', false);
- }.bind(this));
- },
- rte_changed: function () {
- this.$buttons.save.prop('disabled', false);
- },
- save: function () {
- var self = this;
- var defs = [];
- $('.oe_dirty').each(function (i, v) {
- var $el = $(this);
- // TODO: Add a queue with concurrency limit in webclient
- // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
- var def = self.saving_mutex.exec(function () {
- return self.saveElement($el).then(function () {
- $el.removeClass('oe_dirty');
- }).fail(function () {
- var data = $el.data();
- console.error(_.str.sprintf('Could not save %s#%d#%s', data.oeModel, data.oeId, data.oeField));
- });
- });
- defs.push(def);
- });
- return $.when.apply(null, defs).then(function () {
- window.location.reload();
- });
- },
- saveElement: function ($el) {
- var data = $el.data();
- var html = $el.html();
- var xpath = data.oeXpath;
- if (xpath) {
- var $w = $el.clone();
- $w.removeClass('oe_dirty');
- _.each(['model', 'id', 'field', 'xpath'], function(d) {$w.removeAttr('data-oe-' + d);});
- $w
- .removeClass('oe_editable')
- .prop('contentEditable', false);
- html = $w.wrap('<div>').parent().html();
- }
- return openerp.jsonRpc('/web/dataset/call', 'call', {
- model: 'ir.ui.view',
- method: 'save',
- args: [data.oeModel, data.oeId, data.oeField, html, xpath]
- });
- },
- cancel: function () {
- window.location.reload();
- },
- snippet: function (ev) {
- this.snippets.toggle();
- },
- mobilePreview: function () {
- $('body').addClass('oe_stop_scrolling');
- document.getElementById("mobile-viewport").src = window.location.href + "?mobile-preview=true";
-
- },
- });
-
- /* ----- 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'}));
-
- start_edition: function ($elements) {
- var self = this;
- this.snippet_carousel();
- $elements
- .not('span, [data-oe-type]')
- .each(function () {
- var $this = $(this);
- CKEDITOR.inline(this, self._config()).on('change', function () {
- $this.addClass('oe_dirty');
- self.trigger('change', this, null);
- });
- });
- },
-
- _current_editor: function () {
- return CKEDITOR.currentInstance;
- },
- _config: function () {
- var removed_plugins = [
- // remove custom context menu
- 'contextmenu,tabletools,liststyle',
- // magicline captures mousein/mouseout => draggable does not work
- 'magicline'
- ];
- return {
- // Disable auto-generated titles
- // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
- title: false,
- removePlugins: removed_plugins.join(','),
- uiColor: '',
- // Ensure no config file is loaded
- customConfig: '',
- // Disable ACF
- allowedContent: true,
- // Don't insert paragraphs around content in e.g. <li>
- autoParagraph: false,
- filebrowserImageUploadUrl: "/website/attach",
- // Support for sharedSpaces in 4.x
- extraPlugins: 'sharedspace',
- // 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", "Unlink", "Blockquote", "BulletedList",
- "NumberedList", "Indent", "Outdent"
- ]},{
- name: 'justify', items: [
- "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
- ]},{
- name: 'special', items: [
- "Image", "Table"
- ]},{
- name: 'styles', items: [
- "Format", "Styles"
- ]}
- ],
- // styles dropdown in toolbar
- stylesSet: [
- // emphasis
- {name: "Muted", element: 'span', attributes: {'class': 'text-muted'}},
- {name: "Primary", element: 'span', attributes: {'class': 'text-primary'}},
- {name: "Warning", element: 'span', attributes: {'class': 'text-warning'}},
- {name: "Danger", element: 'span', attributes: {'class': 'text-danger'}},
- {name: "Success", element: 'span', attributes: {'class': 'text-success'}},
- {name: "Info", element: 'span', attributes: {'class': 'text-info'}}
- ],
- };
- },
- // TODO clean
- snippet_carousel: function () {
- var self = this;
- $('.carousel .js_carousel_options .label').on('click', function (e) {
- e.preventDefault();
- var $button = $(e.currentTarget);
- var $c = $button.parents(".carousel:first");
-
- if($button.hasClass("js_add")) {
- var cycle = $c.find(".carousel-inner .item").size();
- $c.find(".carousel-inner").append(openerp.qweb.render("website.carousel"));
- $c.carousel(cycle);
- }
- else {
- var cycle = $c.find(".carousel-inner .item.active").remove();
- $c.find(".carousel-inner .item:first").addClass("active");
- $c.carousel(0);
- self.trigger('change', self, null);
- }
- });
- $('.carousel .js_carousel_options').show();
- }
- });
-
website.mutations = {
darken: function($el){
- if($el.parent().hasClass('dark')){
- $el.parent().replaceWith($el);
+ var $parent = $el.parent();
+ if($parent.hasClass('dark')){
+ $parent.replaceWith($el);
}else{
$el.replaceWith($("<div class='dark'></div>").append($el.clone()));
}
},
vomify: function($el){
- var hue=0;
+ var hue=0;
var beat = false;
- var a = setInterval(function(){
+ var a = setInterval(function(){
$el.css({'-webkit-filter':'hue-rotate('+hue+'deg)'}); hue += 5;
}, 10);
setTimeout(function(){
clearInterval(a);
- setInterval(function(){
+ setInterval(function(){
var filter = 'hue-rotate('+hue+'deg)'+ (beat ? ' invert()' : '');
- $('html').css({'-webkit-filter': filter}); hue += 5;
+ $(document.documentElement).css({'-webkit-filter': filter}); hue += 5;
if(hue % 35 === 0){
beat = !beat;
}
@@ -335,477 +60,8 @@
},
};
- /* ----- SNIPPET SELECTOR ---- */
- website.Snippets = openerp.Widget.extend({
- template: 'website.snippets',
- init: function () {
- this._super.apply(this, arguments);
- if(!$('#oe_manipulators').length){
- $("<div id='oe_manipulators'></div>").appendTo('body');
- }
- },
- start: function() {
- var self = this;
-
- $.ajax({
- type: "GET",
- url: "/page/website.snippets",
- dataType: "text",
- success: function(snippets){
- self.$el.html(snippets);
- self.$('.oe_snippet').each(function(index,snippet){
- self.make_snippet_draggable($(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($snippet){
- var self = this;
-
- $snippet.draggable({
- helper: 'clone',
- zIndex: '1000',
- appendTo: 'body',
- start: function(){
- var action = $snippet.data('action');
-
- self.deactivate_snippet_manipulators();
- if( action === 'insert'){
- self.activate_insertion_zones({
- siblings: $snippet.data('selector-siblings'),
- childs: $snippet.data('selector-childs')
- });
- }else if( action === 'mutate' ){
- self.activate_overlay_zones($snippet.data('selector'));
- }
-
- $('.oe_drop_zone').droppable({
- over: function(){
- // FIXME: stupid hack to prevent multiple droppable to activate at once ...
- // it's not even working properly but it's better than nothing.
- $(".oe_drop_zone.oe_hover").removeClass("oe_hover");
- $(this).addClass("oe_hover");
- },
- out: function(){
- $(this).removeClass("oe_hover");
- },
- drop: function(){
- if( action === 'insert' ){
- var $toInsert = $snippet.find('.oe_snippet_body').clone();
- $toInsert.removeClass('oe_snippet_body');
- $toInsert.addClass('oe_snippet_instance');
- $toInsert.data('snippet-id',$snippet.data('snippet-id'));
- $(".oe_drop_zone.oe_hover").replaceWith($toInsert);
- }else if( action === 'mutate' ){
- self.path_eval($snippet.data('action-function'))( $(".oe_drop_zone.oe_hover").data('target') );
- }
- },
- });
- },
- stop: function(){
- self.deactivate_zones();
- self.activate_snippet_manipulators();
- },
- });
- },
-
- // 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();
- },
- // WIP
- make_draggable_instance: function($instance){
- var self = this;
- var $snippet = get_snippet_from_id($instance.data('snippet-id'));
-
- $instance.draggable({
- helper: 'clone',
- zIndex: '1000',
- appendTo: 'body',
- start: function(){
- var action = $snippet.data('action');
- if(action === 'insert'){
-
- self.deactivate_snippet_manipulators();
- self.activate_insertion_zones({
- siblings: $snippet.data('selector-siblings'),
- child: $snippet.data('selector-childs')
- });
-
- }
- }
- });
- },
-
- // Create element insertion drop zones. two css selectors can be provided
- // selector.childs -> 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.childs || '';
- var sibling_selector = selector.siblings || '';
- var zone_template = "<div class='oe_drop_zone oe_insert'></div>";
-
- $('.oe_drop_zone').remove();
-
- if(child_selector){
- var $zones = $(child_selector);
- for( var i = 0, len = $zones.length; i < len; i++ ){
- $zones.eq(i).find('> *:not(.oe_drop_zone)').after(zone_template);
- $zones.eq(i).prepend(zone_template);
- }
- }
-
- if(sibling_selector){
- var $zones = $(sibling_selector);
- for( var i = 0, len = $zones.length; i < len; i++ ){
- if($zones.eq(i).prev('.oe_drop_zone').length === 0){
- $zones.eq(i).before(zone_template);
- }
- if($zones.eq(i).next('.oe_drop_zone').length === 0){
- $zones.eq(i).after(zone_template);
- }
- }
- }
-
- // Cleaning up unnecessary zones
- $('.oe_snippets .oe_drop_zone').remove(); // no zone in the snippet selector ...
- $('#website-top-view .oe_drop_zone').remove(); // no zone in the top bars ...
- $('#website-top-edit .oe_drop_zone').remove();
- 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').remove(); // no recusrive 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');
- for( var i = 0, len = $zones.length; i < len; i++ ){
- var zone = $zones.eq(i);
- var prev = zone.prev();
- var next = zone.next();
- var float_prev = zone.prev().css('float') || 'none';
- var float_next = zone.next().css('float') || 'none';
- var disp_prev = zone.prev().css('display') || null;
- var disp_next = zone.next().css('display') || null;
- if( (float_prev === 'left' || float_prev === 'right')
- && (float_next === 'left' || float_next === 'right') ){
- zone.remove();
- continue;
- }else if( !( disp_prev === null
- || disp_next === null
- || disp_prev === 'block'
- || disp_next === 'block' )){
- zone.remove();
- continue;
- }
- }
- },
- deactivate_zones: function(){
- $('.oe_drop_zone').remove();
- },
-
- // generate drop zones covering the elements selected by the selector
- activate_overlay_zones: function(selector){
- var $targets = $(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;
- });
-
- var zone_template = "<div class='oe_drop_zone oe_overlay'></div>";
- $('.oe_drop_zone').remove();
-
- for(var i = 0, len = $targets.length; i < len; i++){
- var $target = $targets.eq(i);
- var $zone = $(zone_template);
- this.cover_target($zone,$target);
- $zone.appendTo('#oe_manipulators');
- $zone.data('target',$target);
- }
- },
-
- // puts $el at the same absolute position as $target
- cover_target: function($el, $target){
- $el.css({
- 'position': 'absolute',
- 'width': $target.outerWidth(),
- 'height': $target.outerHeight(),
- });
- $el.css($target.offset());
- },
-
- // activates the manipulator boxes (with resizing handles) for all snippets
- activate_snippet_manipulators: function(){
- var self = this;
- // we generate overlay drop zones only to get an idea of where the snippet are, the drop
- // zones are replaced by manipulators
- this.activate_overlay_zones('#wrap .container');
-
- var $active_manipulator = null;
- var locked = false;
-
- $('.oe_drop_zone').each(function(){;
-
- var $zone = $(this);
- var $snippet = $zone.data('target');
- var $manipulator = $(openerp.qweb.render('website.snippet_manipulator'));
-
- self.cover_target($manipulator, $zone);
- $manipulator.data('target',$snippet);
- $manipulator.appendTo('#oe_manipulators');
- $zone.remove();
-
- $manipulator.mouseover(function(){
- if(!locked && $active_manipulator != $manipulator){
- if($active_manipulator){
- $active_manipulator.removeClass('oe_selected');
- }
- $active_manipulator = $manipulator;
- $manipulator.addClass('oe_selected');
- }
- });
- /*$manipulator.mouseleave(function(){
- if(!locked && $active_manipulator){
- $active_manipulator.removeClass('oe_selected');
- $active_manipulator = null;
- }
- });*/
- /*
- $manipulator.click(function(){
- if($active_manipulator === $manipulator){
- selected = !selected;
- $manipulator.toggleClass('oe_selected',selected);
- }
- });
- */
-
- $manipulator.find('.oe_handle').mousedown(function(event){
- locked = true;
- var $handle = $(this);
- var x = event.pageX;
- var y = event.pageY;
-
- var pt = $snippet.css('padding-top');
- var pb = $snippet.css('padding-bottom');
- pt = Number(pt.slice(0,pt.length - 2)) || 0; //FIXME something cleaner to remove 'px'
- pb = Number(pb.slice(0,pb.length - 2)) || 0;
-
- $manipulator.addClass('oe_hover');
- event.preventDefault();
-
- $('body').mousemove(function(event){
- var dx = event.pageX - x;
- var dy = event.pageY - y;
- event.preventDefault();
- if($handle.hasClass('n') || $handle.hasClass('nw') || $handle.hasClass('ne')){
- $snippet.css('padding-top',pt-dy+'px');
- self.cover_target($manipulator,$snippet);
- }else if($handle.hasClass('s') || $handle.hasClass('sw') || $handle.hasClass('se')){
- $snippet.css('padding-bottom',pb+dy+'px');
- self.cover_target($manipulator,$snippet);
- }
- });
-
- $('body').mouseup(function(){
- locked = false;
- $('body').unbind('mousemove');
- $('body').unbind('mouseup');
- self.deactivate_snippet_manipulators();
- self.activate_snippet_manipulators();
- });
- });
-
- });
- },
- deactivate_snippet_manipulators: function(){
- $('.oe_snippet_manipulator').remove();
- },
- toggle: function(){
- if(this.$el.hasClass('oe_hidden')){
- this.$el.removeClass('oe_hidden');
- this.activate_snippet_manipulators();
- }else{
- this.$el.addClass('oe_hidden');
- this.deactivate_snippet_manipulators();
- }
- },
- });
-
- function noop() {}
- var alter_dialog = {
- image: function (definition) {
- definition.removeContents('Link');
- definition.removeContents('advanced');
-
- var upload = definition.getContents('Upload');
- upload.add({
- type: 'select',
- label: 'Existing attachments',
- id: 'ir_attachment',
- items: [['']],
- /**
- * On dialog load, fetch all attachments (on ir.ui.view =>
- * previously uploaded images) and add them to the select's
- * options (items array & add method)
- */
- onLoad: function () {
- var field = this;
-
- // FIXME: fuck this garbage, also fuck openerp.Model
- return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
- model: 'ir.attachment',
- method: 'search_read',
- args: [],
- kwargs: {
- fields: ['name'],
- domain: [['res_model', '=', 'ir.ui.view']],
- order: 'name',
- }
- }).then(function (results) {
- _(results).each(function (result) {
- field.add(result.name, result.id);
- });
- });
- },
- /**
- * The image widgets uses "txtUrl" to do most of its stuff.
- * Synchronize select & txtUrl by generating the correct URL
- * and setting it there
- */
- onChange: function () {
- var id = this.getValue();
- var url = this.getDialog().getContentElement('info', 'txtUrl');
- if (!id) {
- url.setValue('');
- return;
- }
-
- url.setValue('/website/attachment/' + id);
- },
- });
- // Override uploadButton to send its information to the select
- // created above instead of directly to txtUrl. The select will
- // propagate to txtUrl
- upload.get('uploadButton').filebrowser = {
- onSelect: function (url) {
- var id = url.split('/').pop();
- var attachments = this.getDialog().getContentElement('Upload', 'ir_attachment');
- // TODO: return supplementary info to get image/attachment name?
- attachments.add(id, id);
- attachments.setValue(id);
- }
- };
-
- var old_show = definition.onShow;
- definition.onShow = function () {
- // CKEDITOR does not *override* onShow, is smashes the existing
- // one instead, so override "by hand"
- if (old_show) {
- old_show.call(this);
- }
- // Assloads of code in the image plugin just go and tear into
- // the info tab without a care, so can't just remove the tab or
- // its content. Hide the tab instead, the effect is roughly the
- // same.
- this.hidePage('info');
- // Force the dialog to always and only display the Upload tab
- this.selectPage('Upload');
- this.on('selectPage', function (e) {
- setTimeout(function () {
- if (e.data.page !== 'Upload') {
- this.selectPage('Upload');
- }
- }.bind(this), 0);
- });
- }
- },
- link: function (definition) {
- definition.removeContents('target');
- definition.removeContents('advanced');
-
- var info = definition.getContents('info');
- info.remove('linkType');
- info.remove('anchorOptions');
- info.remove('emailOptions');
-
- info.get('urlOptions').children[0].widths = [ '0%', '100%' ];
- info.get('protocol').style = 'display: none';
- // TODO: sync edition of url to website_pages?
- info.add({
- type: 'select',
- label: "Existing page",
- id: 'website_pages',
- items: [['']],
- /**
- * onload fetch all the pages existing in the website, then
- * display that.
- */
- onLoad: function () {
- var field = this;
-
- return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
- model: 'website',
- method: 'list_pages',
- args: [],
- kwargs: {}
- }).then(function (results) {
- _(results).each(function (result) {
- field.add(result.name, result.url);
- });
- });
- },
- onChange: function () {
- var url = this.getValue();
- var url_field = this.getDialog().getContentElement('info', 'url');
- if (!url) {
- url_field.setValue('');
- return;
- }
-
- url_field.setValue(url);
- }
- })
- }
- };
- CKEDITOR.on('dialogDefinition', function (ev) {
- (alter_dialog[ev.data.name] || noop)(ev.data.definition);
- });
-
var all_ready = null;
- var dom_ready = $.Deferred();
+ var dom_ready = website.dom_ready = $.Deferred();
$(dom_ready.resolve);
website.init_kanban = function ($kanban) {
@@ -880,28 +136,21 @@
};
dom_ready.then(function () {
- website.is_editable = $('html').attr('data-editable') === '1';
- var is_smartphone = $('body')[0].clientWidth < 767;
-
- if (website.is_editable && !is_smartphone) {
- website.ready().then(website.init_editor);
- }
-
/* ----- PUBLISHING STUFF ---- */
- $(document).on('click', '.js_publish, .js_unpublish', function (e) {
+ $('[data-publish]:has([data-publish])').each(function () {
+ var $pub = $("[data-publish]", this);
+ if($pub.size())
+ $(this).attr("data-publish", $pub.attr("data-publish"));
+ else
+ $(this).removeAttr("data-publish");
+ });
+
+ $(document).on('click', '.js_publish', function (e) {
e.preventDefault();
- var $link = $(this).parent();
- $link.find('.js_publish, .js_unpublish').addClass("hidden");
- var $unp = $link.find(".js_unpublish");
- var $p = $link.find(".js_publish");
- $.post('/website/publish', {'id': $link.data('id'), 'object': $link.data('object')}, function (result) {
- if (+result) {
- $p.addClass("hidden");
- $unp.removeClass("hidden");
- } else {
- $p.removeClass("hidden");
- $unp.addClass("hidden");
- }
+ var $data = $(":first", this).parents("[data-publish]");
+ $data.attr("data-publish", $data.first().attr("data-publish") == 'off' ? 'on' : 'off');
+ $.post('/website/publish', {'id': $(this).data('id'), 'object': $(this).data('object')}, function (result) {
+ $data.attr("data-publish", +result ? 'on' : 'off');
});
});
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..6487b2b457b
--- /dev/null
+++ b/addons/website/static/src/js/website.mobile.js
@@ -0,0 +1,24 @@
+(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.href + "?mobile-preview=true";
+ this.$el.modal();
+ },
+ });
+})();
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..61561964d67
--- /dev/null
+++ b/addons/website/static/src/js/website.seo.js
@@ -0,0 +1,301 @@
+(function () {
+ 'use strict';
+ // TODO l10n ?
+
+ var website = openerp.website;
+ website.templates.push('/website/static/src/xml/website.seo.xml');
+
+ website.EditorBar.include({
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click a[data-action=promote-current-page]': 'promotePage',
+ }),
+ promotePage: function () {
+ (new website.seo.Configurator()).appendTo($(document.body));
+ },
+ });
+
+ website.seo = {};
+
+ website.seo.PageParser = openerp.Class.extend({
+ url: function () {
+ var url = window.location.href;
+ var hashIndex = url.indexOf('#');
+ return hashIndex >= 0 ? url.substring(0, hashIndex) : url;
+ },
+ title: function () {
+ return $(document.title).text();
+ },
+ 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');
+ },
+ });
+
+ 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.Keyword = openerp.Widget.extend({
+ template: 'website.seo_keyword',
+ events: {
+ 'click a[data-action=remove-keyword]': 'destroy',
+ },
+ maxWordsPerKeyword: 4, // TODO Check
+ types: {
+ // cf. http://getbootstrap.com/components/#labels
+ // default, primary, success, info, warning, danger
+ perfect: 'success',
+ advised: 'primary',
+ used: 'warning',
+ new: 'default',
+ },
+ init: function (parent, options) {
+ this.keyword = options.keyword;
+ this.type = this.types[options.type || 'default'];
+ this._super(parent);
+ },
+ destroy: function () {
+ this.trigger('removed');
+ this._super();
+ },
+ });
+
+ website.seo.KeywordList = openerp.Widget.extend({
+ template: 'website.seo_list',
+ maxKeywords: 10,
+ keywords: function () {
+ var result = [];
+ this.$el.find('.js_seo_keyword').each(function () {
+ result.push($(this).data('keyword'));
+ });
+ return result;
+ },
+ suggestions: function () {
+ // TODO Refactor (Ugly)
+ return $('.js_seo_suggestion').map(function () {
+ return $(this).data('keyword');
+ });
+ },
+ isKeywordListFull: function () {
+ return this.keywords().length >= this.maxKeywords;
+ },
+ isExistingKeyword: function (word) {
+ return _.contains(this.keywords(), word);
+ },
+ add: function (candidate) {
+ var self = this;
+ // TODO Refine
+ var word = candidate ? candidate.replace(/[,;.:<>]+/g, " ").replace(/ +/g, " ").trim() : "";
+ if (word && !this.isKeywordListFull() && !this.isExistingKeyword(word)) {
+ var type = _.contains(this.suggestions(), word) ? 'advised' : 'new';
+ var keyword = new website.seo.Keyword(this, {
+ keyword: word,
+ type: type,
+ });
+ keyword.on('removed', null, function () {
+ self.trigger('list-not-full');
+ });
+ keyword.appendTo(this.$el);
+ }
+ if (this.isKeywordListFull()) {
+ self.trigger('list-full');
+ }
+ },
+ });
+
+ website.seo.Suggestion = openerp.Widget.extend({
+ template: 'website.seo_suggestion',
+ events: {
+ 'click .js_seo_suggestion': 'select'
+ },
+ types: {
+ // cf. http://getbootstrap.com/components/#labels
+ // default, primary, success, info, warning, danger
+ primary: 'primary',
+ secondary: 'info',
+
+ },
+ init: function (parent, options) {
+ this.keyword = options.keyword;
+ this.type = this.types[options.type || 'primary'];
+ this._super(parent);
+ },
+ select: function () {
+ this.trigger('selected');
+ },
+ });
+
+ 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({
+ start: function () {
+ var self = this;
+ new website.seo.PageParser().images().each(function (index, image) {
+ new website.seo.Image(self, image).appendTo(self.$el);
+ });
+ },
+ images: function () {
+ var result = [];
+ this.$el.find('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.Configurator = openerp.Widget.extend({
+ template: 'website.seo_configuration',
+ events: {
+ 'keypress input[name=seo_page_keywords]': 'confirmKeyword',
+ 'click button[data-action=add]': 'addKeyword',
+ 'click button[data-action=update]': 'update',
+ 'hidden.bs.modal': 'destroy'
+ },
+ maxTitleSize: 65,
+ maxDescriptionSize: 155,
+ start: function () {
+ var $modal = this.$el;
+ var pageParser = new website.seo.PageParser();
+ $modal.find('.js_seo_page_url').text(pageParser.url());
+ $modal.find('input[name=seo_page_title]').val(pageParser.title());
+ this.suggestImprovements(pageParser);
+ this.displayKeywordSuggestions(pageParser);
+ this.imageList = new website.seo.ImageList(this);
+ this.imageList.appendTo($modal.find('.js_seo_image_list'));
+ this.keywordList = new website.seo.KeywordList(this);
+ this.keywordList.appendTo($modal.find('.js_seo_keywords_list'));
+ this.keywordList.on('list-full', null, 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');
+ });
+ this.keywordList.on('list-not-full', null, function () {
+ $modal.find('input[name=seo_page_keywords]')
+ .removeAttr('readonly').attr('placeholder', "");
+ $modal.find('button[data-action=add]')
+ .prop('disabled', false).removeClass('disabled');
+ });
+ $modal.modal();
+ },
+ suggestImprovements: function (parser) {
+ var tips = [];
+ var self = this;
+ function displayTip(message, type) {
+ new website.seo.Tip(this, {
+ message: message,
+ type: type
+ }).appendTo(self.$el.find('.js_seo_tips'));
+ }
+ var pageParser = parser || new website.seo.PageParser();
+ if (!pageParser.headers('h1').length === 0) {
+ tips.push({
+ type: 'warning',
+ message: "You don't have an &lt;h1&gt; tag on your page.",
+ });
+ }
+ if (pageParser.headers('h1').length > 1) {
+ tips.push({
+ type: 'warning',
+ message: "You have more than one &lt;h1&gt; tag on your page.",
+ });
+ }
+ if (tips.length > 0) {
+ _.each(tips, function (tip) {
+ displayTip(tip.message, tip.type);
+ });
+ } else {
+ displayTip("Your page makup is appropriate for search engines.", 'success');
+ }
+ },
+ displayKeywordSuggestions: function (pageParser) {
+ var $modal = this.$el;
+ var self = this;
+ $modal.find('.js_seo_company_suggestions').append("Loading...");
+ var companyName = pageParser.company().toLowerCase();
+ // cf. https://github.com/ddm/seo
+ // TODO Try with /recommend/
+ $.getJSON("http://seo.eu01.aws.af.cm/suggest/"+encodeURIComponent(companyName), function (list) {
+ $modal.find('.js_seo_company_suggestions').empty();
+ // TODO Improve algorithm + Ajust based on custom user keywords
+ var nameRegex = new RegExp(companyName, "gi");
+ var cleanList = _.map(list, function removeCompanyName (word) {
+ return word.replace(nameRegex, "").trim();
+ });
+ // TODO Order properly ?
+ cleanList.push(companyName);
+ _.each(_.uniq(cleanList), function (keyword) {
+ if (keyword) {
+ var suggestion = new website.seo.Suggestion(self, {
+ keyword: keyword
+ });
+ suggestion.on('selected', self, function () {
+ self.addKeyword(keyword);
+ });
+ suggestion.appendTo($modal.find('.js_seo_company_suggestions'));
+ }
+ });
+ });
+ },
+ confirmKeyword: function (e) {
+ if (e.keyCode == 13) {
+ this.addKeyword(this.$el.find('input[name=seo_page_keywords]').val());
+ }
+ },
+ addKeyword: function (word) {
+ var $input = this.$el.find('input[name=seo_page_keywords]');
+ var keyword = _.isString(word) ? word : $input.val();
+ this.keywordList.add(keyword);
+ $input.val("");
+ },
+ update: function () {
+ var data = {
+ title: this.$el.find('input[name=seo_page_title]').val(),
+ description: this.$el.find('input[name=seo_page_title]').val(),
+ keywords: this.keywordList.keywords(),
+ images: this.imageList.images(),
+ };
+ console.log(data);
+ // TODO Persist changes
+ },
+ });
+})();
diff --git a/addons/website/static/src/js/website.snippets.js b/addons/website/static/src/js/website.snippets.js
new file mode 100644
index 00000000000..5ebf7aecc8d
--- /dev/null
+++ b/addons/website/static/src/js/website.snippets.js
@@ -0,0 +1,340 @@
+(function () {
+ 'use strict';
+
+ var website = openerp.website;
+ website.EditorBar.include({
+ events: _.extend({}, website.EditorBar.prototype.events, {
+ 'click button[data-action=snippet]': 'snippet',
+ }),
+ start: function () {
+ return this._super().then(function () {
+ this.$buttons.snippet = this.$('button[data-action=snippet]');
+ window.snippets = this.snippets = new website.Snippets();
+ this.snippets.appendTo($(document.body));
+ }.bind(this));
+ },
+ snippet: function (ev) {
+ this.snippets.toggle();
+ },
+ });
+
+ website.RTE.include({
+ start_edition: function ($elements) {
+ this.snippet_carousel();
+ return this._super($elements);
+ },
+ // TODO clean
+ snippet_carousel: function () {
+ var self = this;
+ $('.carousel').each(function () {
+ var $carousel = $(this);
+ var $options = $('.js_carousel_options', $carousel);
+ $options.on('click', '.label', function (e) {
+ e.preventDefault();
+ var $button = $(e.currentTarget);
+ var $c = $button.parents(".carousel:first");
+ var $carousel_inner = $c.find('.carousel-inner');
+
+ if($button.hasClass("js_add")) {
+ var cycle = $carousel_inner.find('.item').size();
+ $carousel_inner.append(openerp.qweb.render('website.carousel'));
+ $c.carousel(cycle);
+ }
+ else if ($carousel_inner.find('.item').size() > 1) {
+ $carousel_inner
+ .find('.item.active').remove().end()
+ .find('.item:first').addClass('active');
+ $c.carousel(0);
+ self.trigger('change', self, null);
+ }
+ });
+ $options.on('change', 'select[name="carousel-background"]', function () {
+ $('.carousel-inner .item.active', $carousel).css('background-image', 'url(' + $(this).val() + ')');
+ $(this).val("");
+ });
+ $options.on('change', 'select[name="carousel-style"]', function () {
+ var $container = $('.carousel-inner .item.active .container', $carousel);
+ $('.content_image', $container).remove();
+ switch ($(this).val()) {
+ case 'no_image':
+ $('.content', $container).attr("class", "content");
+ break;
+ case 'image_left':
+ $('.content', $container).attr("class", "content col-md-6")
+ .before('<div class="content_image col-md-5"><img class="img-rounded img-responsive" src="/website/static/src/img/china.jpg"></div>');
+ break;
+ case 'image_right':
+ $('.content', $container).attr("class", "content col-md-6")
+ .after('<div class="content_image col-md-5 col-lg-offset-1"><img class="img-rounded img-responsive" src="/website/static/src/img/china.jpg"></div>');
+ break;
+ }
+ $(this).val("");
+ });
+ $options.show();
+ });
+ },
+ });
+
+ /* ----- SNIPPET SELECTOR ---- */
+ website.Snippets = openerp.Widget.extend({
+ template: 'website.snippets',
+ init: function () {
+ this._super.apply(this, arguments);
+ },
+ start: function() {
+ var self = this;
+
+ $.ajax({
+ type: "GET",
+ url: "/page/website.snippets",
+ dataType: "text",
+ success: function(snippets){
+ self.$el.html(snippets);
+ self.start_snippets();
+ },
+ });
+
+ },
+ path_eval: function(path){
+ var obj = window;
+ path = path.split('.');
+ do{
+ obj = obj[path.shift()];
+ }while(path.length && obj);
+ return obj;
+ },
+
+ // setup widget and drag and drop
+ start_snippets: function(){
+ var self = this;
+
+ this.$('.oe_snippet').draggable({
+ helper: 'clone',
+ zIndex: '1000',
+ appendTo: 'body',
+ start: function(){
+ var snippet = $(this);
+ var action = snippet.data('action');
+
+ self.deactivate_snippet_manipulators();
+ if( action === 'insert'){
+ self.activate_insertion_zones({
+ siblings: snippet.data('selector-siblings'),
+ childs: snippet.data('selector-childs')
+ });
+ }else if( action === 'mutate' ){
+ self.activate_overlay_zones(snippet.data('selector'));
+ }
+
+ $('.oe_drop_zone').droppable({
+ over: function(){
+ // FIXME: stupid hack to prevent multiple droppable to activate at once ...
+ // it's not even working properly but it's better than nothing.
+ $(".oe_drop_zone.oe_hover").removeClass("oe_hover");
+ $(this).addClass("oe_hover");
+ },
+ out: function(){
+ $(this).removeClass("oe_hover");
+ },
+ drop: function(){
+ if( action === 'insert' ){
+ $(".oe_drop_zone.oe_hover")
+ .replaceWith(snippet.find('.oe_snippet_body').clone())
+ .removeClass('oe_snippet_body');
+ }else if( action === 'mutate' ){
+ self.path_eval(snippet.data('action-function'))( $(".oe_drop_zone.oe_hover").data('target') );
+ }
+ },
+ });
+ },
+ stop: function(){
+ self.deactivate_zones();
+ self.activate_snippet_manipulators();
+ },
+ });
+
+ },
+ // Create element insertion drop zones. two css selectors can be provided
+ // selector.childs -> 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 i, len, $zones;
+ var child_selector = selector.childs || '';
+ var sibling_selector = selector.siblings || '';
+ var zone_template = "<div class='oe_drop_zone oe_insert'></div>";
+ var $drop_zone = $('.oe_drop_zone');
+
+ $drop_zone.remove();
+
+ if(child_selector){
+ $zones = $(child_selector);
+ for(i = 0, len = $zones.length; i < len; i++ ){
+ $zones.eq(i).find('> *:not(.oe_drop_zone)').after(zone_template);
+ $zones.eq(i).prepend(zone_template);
+ }
+ }
+
+ if(sibling_selector){
+ $zones = $(sibling_selector);
+ for(i = 0, len = $zones.length; i < len; i++ ){
+ if($zones.eq(i).prev('.oe_drop_zone').length === 0){
+ $zones.eq(i).before(zone_template);
+ }
+ if($zones.eq(i).next('.oe_drop_zone').length === 0){
+ $zones.eq(i).after(zone_template);
+ }
+ }
+ }
+
+ // Cleaning up unnecessary zones
+ $('.oe_snippets .oe_drop_zone').remove(); // no zone in the snippet selector ...
+ $('#website-top-view').find('.oe_drop_zone').remove(); // no zone in the top bars ...
+ $('#website-top-edit').find('.oe_drop_zone').remove();
+ var count;
+ do {
+ count = 0;
+ $zones = $('.oe_drop_zone + .oe_drop_zone'); // no two consecutive zones
+ count += $zones.length;
+ $zones.remove();
+
+ $zones = $('.oe_drop_zone > .oe_drop_zone').remove(); // no recusrive 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.
+ $zones = $drop_zone;
+ for(i = 0, len = $zones.length; i < len; i++ ){
+ var zone = $zones.eq(i);
+ 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();
+ }
+ }
+ },
+ deactivate_zones: function(){
+ $('.oe_drop_zone').remove();
+ },
+
+ activate_overlay_zones: function(selector){
+ var $targets = $(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;
+ });
+
+ var zone_template = "<div class='oe_drop_zone oe_overlay'></div>";
+ $('.oe_drop_zone').remove();
+
+ for(var i = 0, len = $targets.length; i < len; i++){
+ var $target = $targets.eq(i);
+ var $zone = $(zone_template);
+ this.cover_target($zone,$target);
+ $zone.appendTo('body');
+ $zone.data('target',$target);
+ }
+ },
+ cover_target: function($el, $target){
+ $el.css({
+ 'position': 'absolute',
+ 'width': $target.outerWidth(),
+ 'height': $target.outerHeight(),
+ });
+ $el.css($target.offset());
+ },
+ activate_snippet_manipulators: function(){
+ var self = this;
+ this.activate_overlay_zones('#wrap .container');
+ var $snippets = $('.oe_drop_zone');
+
+ for(var i = 0, len = $snippets.length; i < len; i++){
+ var $snippet = $snippets.eq(i);
+ var $manipulator = $(openerp.qweb.render('website.snippet_manipulator'));
+ $manipulator.css({
+ 'top': $snippet.css('top'),
+ 'left': $snippet.css('left'),
+ 'width': $snippet.css('width'),
+ 'height': $snippet.css('height'),
+ });
+ $manipulator.data('target',$snippet.data('target'));
+ $manipulator.appendTo('body');
+ $snippet.remove();
+
+ $manipulator.find('.oe_handle').mousedown(function(event){
+ var $handle = $(this);
+ var $manipulator = $handle.parent();
+ var $snippet = $manipulator.data('target');
+ var x = event.pageX;
+ var y = event.pageY;
+
+ var pt = $snippet.css('padding-top');
+ var pb = $snippet.css('padding-bottom');
+ pt = Number(pt.slice(0, -2)) || 0; //FIXME something cleaner to remove 'px'
+ pb = Number(pb.slice(0, -2)) || 0;
+
+ $manipulator.addClass('oe_hover');
+ event.preventDefault();
+
+ $(document.body).on({
+ mousemove: function(event){
+ var dx = event.pageX - x;
+ var dy = event.pageY - y;
+ event.preventDefault();
+ if($handle.hasClass('n') || $handle.hasClass('nw') || $handle.hasClass('ne')){
+ $snippet.css('padding-top',pt-dy+'px');
+ self.cover_target($manipulator,$snippet);
+ }else if($handle.hasClass('s') || $handle.hasClass('sw') || $handle.hasClass('se')){
+
+ $snippet.css('padding-bottom',pb+dy+'px');
+ self.cover_target($manipulator,$snippet);
+ }
+ },
+ mouseup: function(){
+ $body.off('mouseup mousemove');
+ self.deactivate_snippet_manipulators();
+ self.activate_snippet_manipulators();
+ }
+ });
+ });
+
+ }
+ },
+ deactivate_snippet_manipulators: function(){
+ $('.oe_snippet_manipulator').remove();
+ },
+ toggle: function(){
+ if(this.$el.hasClass('hide')){
+ this.$el.removeClass('hide');
+ this.activate_snippet_manipulators();
+ }else{
+ this.$el.addClass('hide');
+ this.deactivate_snippet_manipulators();
+ }
+ },
+ });
+})();