diff options
Diffstat (limited to 'addons/website/static/src/js')
| -rw-r--r-- | addons/website/static/src/js/website.editor.js | 651 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.js | 795 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.mobile.js | 24 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.seo.js | 301 | ||||
| -rw-r--r-- | addons/website/static/src/js/website.snippets.js | 340 |
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 <h1> tag on your page.", + }); + } + if (pageParser.headers('h1').length > 1) { + tips.push({ + type: 'warning', + message: "You have more than one <h1> 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(); + } + }, + }); +})(); |
