diff options
Diffstat (limited to 'addons/web_editor/static/src/js/widgets/widgets.js')
| -rw-r--r-- | addons/web_editor/static/src/js/widgets/widgets.js | 1177 |
1 files changed, 1177 insertions, 0 deletions
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js new file mode 100644 index 00000000000..3bfae6ce240 --- /dev/null +++ b/addons/web_editor/static/src/js/widgets/widgets.js @@ -0,0 +1,1177 @@ +odoo.define('web_editor.widget', function (require) { +'use strict'; + +var core = require('web.core'); +var ajax = require('web.ajax'); +var Dialog = require('web.Dialog'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var rte = require('web_editor.rte'); + +var QWeb = core.qweb; +var range = $.summernote.core.range; +var dom = $.summernote.core.dom; + +var _t = core._t; + +/** + * Extend Dialog class to handle save/cancel of edition components. + */ +Dialog = Dialog.extend({ + init: function (parent, options) { + options = options || {}; + this._super(parent, _.extend({}, { + buttons: [ + {text: options.save_text || _t("Save"), classes: "btn-primary o_save_button", click: this.save}, + {text: _t("Discard"), close: true} + ] + }, options)); + + this.destroyAction = "cancel"; + + var self = this; + this.opened().then(function () { + self.$('input:first').focus(); + }); + this.on("closed", this, function () { + this.trigger(this.destroyAction, this.final_data || null); + }); + }, + save: function () { + this.destroyAction = "save"; + this.close(); + }, +}); + +/** + * alt widget. Lets users change a alt & title on a media + */ +var alt = Dialog.extend({ + template: 'web_editor.dialog.alt', + init: function (parent, options, $editable, media) { + this._super(parent, _.extend({}, { + title: _t("Change media description and tooltip") + }, options)); + this.$editable = $editable; + this.media = media; + this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"'); + this.title = ($(this.media).attr('title') || "").replace(/"/g, '"'); + }, + save: function () { + range.createFromNode(this.media).select(); + this.$editable.data('NoteHistory').recordUndo(); + var alt = this.$('#alt').val(); + var title = this.$('#title').val(); + $(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null); + _.defer((function () { + click_event(this.media, "mouseup"); + }).bind(this)); + return this._super.apply(this, arguments); + }, +}); + +var click_event = function (el, type) { + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); + el.dispatchEvent(evt); +}; + +/** + * MediaDialog widget. Lets users change a media, including uploading a + * new image, font awsome or video and can change a media into an other + * media + * + * options: select_images: allow the selection of more of one image + */ +var MediaDialog = Dialog.extend({ + template: 'web_editor.dialog.media', + events : _.extend({}, Dialog.prototype.events, { + 'input input#icon-search': 'search', + }), + init: function (parent, options, $editable, media) { + this._super(parent, _.extend({}, { + title: _t("Select a Media"), + }, options)); + if ($editable) { + this.$editable = $editable; + this.rte = this.$editable.rte || this.$editable.data('rte'); + } + this.options = options || {}; + this.old_media = media; + this.media = media; + this.isNewMedia = !media; + this.range = range.create(); + }, + start: function () { + var self = this; + + this.$modal.addClass('note-image-dialog'); + this.$modal.find('.modal-dialog').addClass('o_select_media_dialog'); + + this.only_images = this.options.only_images || this.options.select_images || (this.media && ($(this.media).parent().data("oe-field") === "image" || $(this.media).parent().data("oe-type") === "image")); + if (this.only_images) { + this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); + } + + this.opened((function () { + if (this.media) { + if (this.media.nodeName === "IMG") { + this.$('[href="#editor-media-image"]').tab('show'); + } else if ($(this.media).is('a.o_image')) { + this.$('[href="#editor-media-document"]').tab('show'); + } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) { + this.$('[href="#editor-media-video"]').tab('show'); + } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) { + this.media = this.media.parentNode; + this.$('[href="#editor-media-video"]').tab('show'); + } else if (this.media.className.match(/(^|\s)fa($|\s)/)) { + this.$('[href="#editor-media-icon"]').tab('show'); + } + } + }).bind(this)); + + this.imageDialog = new ImageDialog(this, this.media, this.options); + this.imageDialog.appendTo(this.$("#editor-media-image")); + this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options)); + this.documentDialog.appendTo(this.$("#editor-media-document")); + if (!this.only_images) { + this.iconDialog = new fontIconsDialog(this, this.media, this.options); + this.iconDialog.appendTo(this.$("#editor-media-icon")); + this.videoDialog = new VideoDialog(this, this.media, this.options); + this.videoDialog.appendTo(this.$("#editor-media-video")); + } + + this.active = this.imageDialog; + + this.$('a[data-toggle="tab"]').on('shown.bs.tab', function (event) { + if ($(event.target).is('[href="#editor-media-image"]')) { + self.active = self.imageDialog; + self.$('li.search, li.previous, li.next').removeClass("hidden"); + } else if ($(event.target).is('[href="#editor-media-document"]')) { + self.active = self.documentDialog; + self.$('li.search, li.previous, li.next').removeClass("hidden"); + } else if ($(event.target).is('[href="#editor-media-icon"]')) { + self.active = self.iconDialog; + self.$('li.search, li.previous, li.next').removeClass("hidden"); + self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden"); + } else if ($(event.target).is('[href="#editor-media-video"]')) { + self.active = self.videoDialog; + self.$('.nav-tabs li.search').addClass("hidden"); + } + }); + + return this._super.apply(this, arguments); + }, + save: function () { + if (this.options.select_images) { + this.final_data = this.active.save(); + this._super.apply(this, arguments); + return; + } + if(this.rte) { + this.range.select(); + this.rte.historyRecordUndo(this.media); + } + + var self = this; + if (self.media) { + this.media.innerHTML = ""; + if (this.active !== this.imageDialog && this.active !== this.documentDialog) { + this.imageDialog.clear(); + } + // if not mode only_images + if (this.iconDialog && this.active !== this.iconDialog) { + this.iconDialog.clear(); + } + if (this.videoDialog && this.active !== this.videoDialog) { + this.videoDialog.clear(); + } + } else { + this.media = document.createElement("img"); + this.range.insertNode(this.media, true); + this.active.media = this.media; + } + this.active.save(); + + if (this.active.add_class) { + $(this.active.media).addClass(this.active.add_class); + } + var media = this.active.media; + + this.final_data = [media, self.old_media]; + $(document.body).trigger("media-saved", this.final_data); + $(self.old_media).trigger("save", this.final_data); + + // Update editor bar after image edition (in case the image change to icon or other) + _.defer(function () { + if (!media.parentNode) return; + range.createFromNode(media).select(); + click_event(media, "mousedown"); + click_event(media, "mouseup"); + }); + + this._super.apply(this, arguments); + }, + searchTimer: null, + search: function () { + var self = this; + var needle = this.$("input#icon-search").val(); + clearTimeout(this.searchTimer); + this.searchTimer = setTimeout(function () { + self.active.search(needle || ""); + },250); + } +}); + +/** + * ImageDialog widget. Let users change an image, including uploading a + * new image in odoo or selecting the image style (if supported by + * the caller). + */ +var ImageDialog = Widget.extend({ + template: 'web_editor.dialog.image', + IMAGES_PER_ROW: 6, + IMAGES_ROWS: 2, + events: _.extend({}, Dialog.prototype.events, { + 'change .url-source': function (e) { + this.changed($(e.target)); + }, + 'click button.filepicker': function () { + var filepicker = this.$('input[type=file]'); + if (!_.isEmpty(filepicker)) { + filepicker[0].click(); + } + }, + 'click .js_disable_optimization': function () { + this.$('input[name="disable_optimization"]').val('1'); + var filepicker = this.$('button.filepicker'); + if (!_.isEmpty(filepicker)) { + filepicker[0].click(); + } + }, + 'change input[type=file]': 'file_selection', + 'submit form': 'form_submit', + 'change input.url': "change_input", + 'keyup input.url': "change_input", + 'click .existing-attachments [data-src]': 'select_existing', + 'dblclick .existing-attachments [data-src]': function () { + this.getParent().save(); + }, + 'click .o_existing_attachment_remove': 'try_remove', + 'keydown.dismiss.bs.modal': function () {}, + }), + init: function (parent, media, options) { + this._super.apply(this, arguments); + this.options = options || {}; + this.accept = this.options.accept || this.options.document ? "*/*" : "image/*"; + this.domain = this.options.domain || ['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]; + this.parent = parent; + this.old_media = media; + this.media = media; + this.images = []; + this.page = 0; + }, + start: function () { + this.$preview = this.$('.preview-container').detach(); + var self = this; + var res = this._super.apply(this, arguments); + var o = {url: null, alt: null}; + + if ($(this.media).is("img")) { + o.url = this.media.getAttribute('src'); + } else if ($(this.media).is("a.o_image")) { + o.url = this.media.getAttribute('href').replace(/[?].*/, ''); + o.id = +o.url.match(/\/web\/content\/([0-9]*)/, '')[1]; + } + this.parent.$(".pager > li").click(function (e) { + if(!self.$el.is(':visible')) { + return; + } + e.preventDefault(); + var $target = $(e.currentTarget); + if ($target.hasClass('disabled')) { + return; + } + self.page += $target.hasClass('previous') ? -1 : 1; + self.display_attachments(); + }); + this.fetch_existing().then(function () { + if (o.url) { + self.set_image(_.find(self.records, function (record) { return record.url === o.url;}) || o); + } + }); + return res; + }, + push: function (attachment) { + if (this.options.select_images) { + var img = _.select(this.images, function (v) { return v.id === attachment.id; }); + if (img.length) { + this.images.splice(this.images.indexOf(img[0]),1); + } else { + this.images.push(attachment); + } + } else { + this.images = [attachment]; + } + }, + save: function () { + if (this.options.select_images) { + return this.images; + } + + var img = this.images[0]; + if (!img) { + var id = this.$(".existing-attachments [data-src]:first").data('id'); + img = _.find(this.images, function (img) { return img.id === id;}); + } + + var media; + if (!img.is_document) { + if (this.media.tagName !== "IMG" || !this.old_media) { + this.add_class = "pull-left"; + this.style = {"width": "100%"}; + } + if(this.media.tagName !== "IMG") { + media = document.createElement('img'); + $(this.media).replaceWith(media); + this.media = media; + } + this.media.setAttribute('src', img.src); + } else { + if (this.media.tagName !== "A") { + $('.note-control-selection').hide(); + media = document.createElement('a'); + $(this.media).replaceWith(media); + this.media = media; + } + this.media.setAttribute('href', '/web/content/' + img.id + '?unique=' + img.checksum + '&download=true'); + $(this.media).addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype); + } + + $(this.media).attr('alt', img.alt); + var style = this.style; + if (style) { $(this.media).css(style); } + + return this.media; + }, + clear: function () { + this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' '); + }, + change_input: function (e) { + var $input = $(e.target); + var $button = $input.parent().find("button"); + var emptyValue = ($input.val() === ""); + $button.toggleClass("btn-default", emptyValue).toggleClass("btn-primary", !emptyValue); + }, + search: function (needle) { + var self = this; + this.fetch_existing(needle).then(function () { + self.selected_existing(); + }); + }, + set_image: function (attachment) { + this.push(attachment); + this.$('input.url').val(''); + this.search(); + this.images = []; + }, + form_submit: function (event) { + var self = this; + var $form = this.$('form[action="/web_editor/attachment/add"]'); + if (!$form.find('input[name="upload"]').val().length) { + if (this.selected_existing().size()) { + event.preventDefault(); + return false; + } + } + $form.find('.well > div').hide().last().after('<span class="fa fa-spin fa-3x fa-refresh"/>'); + + var callback = _.uniqueId('func_'); + this.$('input[name=func]').val(callback); + window[callback] = function (attachments, error) { + delete window[callback]; + $form.find('.well > span').remove(); + $form.find('.well > div').show(); + _.each(attachments, function (record) { + record.src = record.url || '/web/image/' + record.id; + record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype)); + }); + if (error || !attachments.length) { + self.file_selected(null, error || !attachments.length); + } + self.images = attachments; + for (var i=0; i<attachments.length; i++) { + self.file_selected(attachments[i], error); + } + }; + }, + file_selection: function () { + var $form = this.$('form'); + this.$el.addClass('nosave'); + $form.removeClass('has-error').find('.help-block').empty(); + this.$('button.filepicker').removeClass('btn-danger btn-success'); + $form.submit(); + }, + file_selected: function (attachment, error) { + var $button = this.$('button.filepicker'); + if (!error) { + $button.addClass('btn-success'); + this.set_image(attachment); + } else { + this.$('form').addClass('has-error').find('.help-block').text(error); + $button.addClass('btn-danger'); + } + + if (!this.options.select_images) { + this.parent.save(); // auto save and close popup + } + }, + fetch_existing: function (needle) { + var domain = [['res_model', '=', 'ir.ui.view']].concat(this.domain); + if (needle && needle.length) { + domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]); + } + return ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'ir.attachment', + method: 'search_read', + args: [], + kwargs: { + domain: domain, + fields: ['name', 'mimetype', 'checksum', 'url', 'type'], + order: 'id desc', + context: base.get_context() + } + }).then(this.proxy('fetched_existing')); + }, + fetched_existing: function (records) { + this.records = _.uniq(_.filter(records, function (r) { + return (r.type === "binary" || r.url && r.url.length > 0); + }), function (r) { + return (r.url || r.id); + }); + _.each(this.records, function (record) { + record.src = record.url || '/web/image/' + record.id; + record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype)); + }); + this.display_attachments(); + }, + display_attachments: function () { + var self = this; + var per_screen = this.IMAGES_PER_ROW * this.IMAGES_ROWS; + var from = this.page * per_screen; + var records = this.records; + + // Create rows of 3 records + var rows = _(records).chain() + .slice(from, from + per_screen) + .groupBy(function (_, index) { return Math.floor(index / self.IMAGES_PER_ROW); }) + .values() + .value(); + + this.$('.help-block').empty(); + + this.$('.existing-attachments').replaceWith(QWeb.render('web_editor.dialog.image.existing.content', {rows: rows})); + this.parent.$('.pager') + .find('li.previous a').toggleClass('disabled', (from === 0)).end() + .find('li.next a').toggleClass('disabled', (from + per_screen >= records.length)); + + this.$el.find('.o_image').each(function () { + var $div = $(this); + if (/gif|jpe|jpg|png/.test($div.data('mimetype'))) { + var $img = $('<img/>').addClass('img img-responsive').attr('src', $div.data('url') || $div.data('src')); + $div.addClass('o_webimage').append($img); + } + }); + this.selected_existing(); + }, + select_existing: function (e) { + var $img = $(e.currentTarget); + if (!this.options.select_images) { + this.$('.o_existing_attachment_cell.o_selected').removeClass("o_selected"); + $img.closest('.o_existing_attachment_cell').addClass('o_selected'); + } else { + $img.closest('.o_existing_attachment_cell').toggleClass('o_selected'); + } + var attachment = _.find(this.records, function (record) { return record.id === $img.data('id'); }); + this.push(attachment); + this.selected_existing(); + }, + selected_existing: function () { + var self = this; + var $select = this.$('.o_existing_attachment_cell [data-src]').filter(function () { + var $img = $(this); + return !!_.find(self.images, function (v) { + return (v.url === $img.data("src") || ($img.data("url") && v.url === $img.data("url")) || v.id === $img.data("id")); + }); + }); + return $select; + }, + try_remove: function (e) { + var $help_block = this.$('.help-block').empty(); + var self = this; + var $a = $(e.target); + var id = parseInt($a.data('id'), 10); + var attachment = _.findWhere(this.records, {id: id}); + var $both = $a.parent().children(); + + $both.css({borderWidth: "5px", borderColor: "#f00"}); + + return ajax.jsonRpc('/web_editor/attachment/remove', 'call', {'ids': [id]}).then(function (prevented) { + if (_.isEmpty(prevented)) { + self.records = _.without(self.records, attachment); + self.display_attachments(); + return; + } + $both.css({borderWidth: "", borderColor: ""}); + $help_block.replaceWith(QWeb.render('web_editor.dialog.image.existing.error', { + views: prevented[id] + })); + }); + }, +}); + +/* list of font icons to load by editor. The icons are displayed in the media editor and + * identified like font and image (can be colored, spinned, resized with fa classes). + * To add font, push a new object {base, parser} + * - base: class who appear on all fonts (eg: fa fa-refresh) + * - parser: regular expression used to select all font in css style sheets + */ +var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}]; + +var cacheCssSelectors = {}; +var getCssSelectors = function (filter) { + var css = []; + if (cacheCssSelectors[filter]) { + return cacheCssSelectors[filter]; + } + var sheets = document.styleSheets; + for(var i = 0; i < sheets.length; i++) { + var rules; + if (sheets[i].rules) { + rules = sheets[i].rules; + } else { + //try...catch because Firefox not able to enumerate document.styleSheets[].cssRules[] for cross-domain stylesheets. + try { + rules = sheets[i].cssRules; + } catch(e) { + console.warn("Can't read the css rules of: " + sheets[i].href, e); + continue; + } + } + if (rules) { + for(var r = 0; r < rules.length; r++) { + var selectorText = rules[r].selectorText; + if (selectorText) { + selectorText = selectorText.split(/\s*,\s*/); + var data = null; + for(var s = 0; s < selectorText.length; s++) { + var match = selectorText[s].match(filter); + if (match) { + var clean = match[1].slice(1).replace(/::?before$/, ''); + if (!data) { + data = [match[1], rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, ''), clean, [clean]]; + } else { + data[0] += (", " + match[1]); + data[3].push(clean); + } + } + } + if (data) { + css.push(data); + } + } + } + } + } + return cacheCssSelectors[filter] = css; +}; +var computeFonts = _.once(function () { + _.each(fontIcons, function (data) { + data.cssData = getCssSelectors(data.parser); + data.alias = []; + data.icons = _.map(data.cssData, function (css) { + data.alias.push.apply(data.alias, css[3]); + return css[2]; + }); + }); +}); + +rte.Class.include({ + init: function () { + this._super.apply(this, arguments); + computeFonts(); + }, + onEnableEditableArea: function ($editable) { + if ($editable.data('oe-type') === "monetary") { + $editable.attr('contenteditable', false); + $editable.find('.oe_currency_value').attr('contenteditable', true); + } + if ($editable.is('[data-oe-model]') && !$editable.is('[data-oe-model="ir.ui.view"]') && !$editable.is('[data-oe-type="html"]')) { + $editable.data('layoutInfo').popover().find('.btn-group:not(.note-history)').remove(); + } + }, +}); + +/** + * FontIconsDialog widget. Lets users change a font awesome, support all + * font awesome loaded in the css files. + */ +var fontIconsDialog = Widget.extend({ + template: 'web_editor.dialog.font-icons', + events : _.extend({}, Dialog.prototype.events, { + 'click .font-icons-icon': function (e) { + e.preventDefault(); + e.stopPropagation(); + + this.$('#fa-icon').val(e.target.getAttribute('data-id')); + $(".font-icons-icon").removeClass("o_selected"); + $(e.target).addClass("o_selected"); + }, + 'dblclick .font-icons-icon': function () { + this.getParent().save(); + }, + 'keydown.dismiss.bs.modal': function () {}, + }), + + init: function (parent, media) { + this._super.apply(this, arguments); + this.parent = parent; + this.media = media; + computeFonts(); + }, + start: function () { + return this._super.apply(this, arguments).then(this.proxy('load_data')); + }, + renderElement: function () { // extract list of font (like awesome) from the cheatsheet. + this.iconsParser = fontIcons; + this.icons = _.flatten(_.map(fontIcons, function (data) { // TODO maybe useless now + return data.icons; + })); + this.alias = _.flatten(_.map(fontIcons, function (data) { + return data.alias; + })); + this._super.apply(this, arguments); + }, + search: function (needle) { + var iconsParser = this.iconsParser; + if (needle) { + iconsParser = []; + _.filter(this.iconsParser, function (data) { + var cssData = _.filter(data.cssData, function (cssData) { + return _.find(cssData[3], function (alias) { + return alias.indexOf(needle) !== -1; + }); + }); + if (cssData.length) { + iconsParser.push({ + base: data.base, + cssData: cssData + }); + } + }); + } + this.$('div.font-icons-icons').html( + QWeb.render('web_editor.dialog.font-icons.icons', {'iconsParser': iconsParser})); + }, + /** + * Removes existing FontAwesome classes on the bound element, and sets + * all the new ones if necessary. + */ + save: function () { + var self = this; + var style = this.media.attributes.style ? this.media.attributes.style.value : ''; + var classes = (this.media.className||"").split(/\s+/); + var custom_classes = /^fa(-[1-5]x|spin|rotate-(9|18|27)0|flip-(horizont|vertic)al|fw|border)?$/; + var non_fa_classes = _.reject(classes, function (cls) { + return self.getFont(cls) || custom_classes.test(cls); + }); + var final_classes = non_fa_classes.concat(this.get_fa_classes()); + if (this.media.tagName !== "SPAN") { + var media = document.createElement('span'); + $(media).data($(this.media).data()); + $(this.media).replaceWith(media); + this.media = media; + style = style.replace(/\s*width:[^;]+/, ''); + } + $(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style); + + return this.media; + }, + /** + * return the data font object (with base, parser and icons) or null + */ + getFont: function (classNames) { + if (!(classNames instanceof Array)) { + classNames = (classNames||"").split(/\s+/); + } + var fontIcon, cssData; + for (var k=0; k<this.iconsParser.length; k++) { + fontIcon = this.iconsParser[k]; + for (var s=0; s<fontIcon.cssData.length; s++) { + cssData = fontIcon.cssData[s]; + if (_.intersection(classNames, cssData[3]).length) { + return { + 'base': fontIcon.base, + 'parser': fontIcon.parser, + 'icons': fontIcon.icons, + 'font': cssData[2] + }; + } + } + } + return null; + }, + + /** + * Looks up the various FontAwesome classes on the bound element and + * sets the corresponding template/form elements to the right state. + * If multiple classes of the same category are present on an element + * (e.g. fa-lg and fa-3x) the last one occurring will be selected, + * which may not match the visual look of the element. + */ + load_data: function () { + var classes = (this.media&&this.media.className||"").split(/\s+/); + for (var i = 0; i < classes.length; i++) { + var cls = classes[i]; + switch (cls) { + case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x': + // size classes + this.$('#fa-size').val(cls); + continue; + case 'fa-spin': + case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270': + case 'fa-flip-horizontal':case 'fa-rotate-vertical': + this.$('#fa-rotation').val(cls); + continue; + case 'fa-fw': + continue; + case 'fa-border': + this.$('#fa-border').prop('checked', true); + continue; + case '': continue; + default: + $(".font-icons-icon").removeClass("o_selected").filter("[data-alias*=',"+cls+",']").addClass("o_selected"); + if (this.alias.indexOf(cls) !== -1) { + this.$('#fa-icon').val(cls); + } + } + } + }, + /** + * Serializes the dialog to an array of FontAwesome classes. Includes the base ``fa``. + */ + get_fa_classes: function () { + var font = this.getFont(this.$('#fa-icon').val()); + return [ + font ? font.base : 'fa', + font ? font.font : "", + this.$('#fa-size').val(), + this.$('#fa-rotation').val(), + this.$('#fa-border').prop('checked') ? 'fa-border' : '' + ]; + }, + clear: function () { + this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' '); + }, +}); + + +function createVideoNode(url, options) { + options = options || {}; + + // video url patterns(youtube, instagram, vimeo, dailymotion, youku) + var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/; + var ytMatch = url.match(ytRegExp); + + var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/; + var igMatch = url.match(igRegExp); + + var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/; + var vMatch = url.match(vRegExp); + + var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/; + var vimMatch = url.match(vimRegExp); + + var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/; + var dmMatch = url.match(dmRegExp); + + var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/; + var youkuMatch = url.match(youkuRegExp); + + var $video = $('<iframe>'); + if (ytMatch && ytMatch[1].length === 11) { + var youtubeId = ytMatch[1]; + $video = $('<iframe>', { + src: '//www.youtube.com/embed/' + youtubeId, + width: '640', + height: '360' + }); + } else if (igMatch && igMatch[0].length) { + $video = $('<iframe>', { + src: igMatch[0] + '/embed/', + width: '612', + height: '710', + scrolling: 'no', + allowtransparency: 'true' + }); + } else if (vMatch && vMatch[0].length) { + $video = $('<iframe>', { + src: vMatch[0] + '/embed/simple', + width: '600', + height: '600', + class: 'vine-embed' + }); + } else if (vimMatch && vimMatch[3].length) { + $video = $('<iframe>', { + src: '//player.vimeo.com/video/' + vimMatch[3], + width: '640', + height: '360' + }); + } else if (dmMatch && dmMatch[2].length) { + $video = $('<iframe>', { + src: '//www.dailymotion.com/embed/video/' + dmMatch[2], + width: '640', + height: '360' + }); + } else if (youkuMatch && youkuMatch[1].length) { + $video = $('<iframe>', { + height: '498', + width: '510', + src: '//player.youku.com/embed/' + youkuMatch[1] + }); + } else { + // this is not a known video link. Now what, Cat? Now what? + $video = $('<iframe>', { + width: '640', + height: '360', + src: url + }); + } + + if (options.autoplay) { + $video.attr("src", $video.attr("src") + "?autoplay=1"); + } + + $video.attr('frameborder', 0); + + return $video; +} + +/** + * VideoDialog widget. Lets users change a video, support all summernote + * video, and embled iframe + */ +var VideoDialog = Widget.extend({ + template: 'web_editor.dialog.video', + events : _.extend({}, Dialog.prototype.events, { + 'click input#urlvideo ~ button': 'get_video', + 'click input#embedvideo ~ button': 'get_embed_video', + 'change input#autoplay': 'get_video', + 'change input#urlvideo': 'change_input', + 'keyup input#urlvideo': 'change_input', + 'change input#embedvideo': 'change_input', + 'keyup input#embedvideo': 'change_input', + 'keydown.dismiss.bs.modal': function () {}, + }), + init: function (parent, media) { + this._super.apply(this, arguments); + this.parent = parent; + this.media = media; + }, + start: function () { + this.$preview = this.$('.preview-container').detach(); + this.$iframe = this.$("iframe"); + var $media = $(this.media); + if ($media.hasClass("media_iframe_video")) { + var src = $media.data('src'); + this.$("input#urlvideo").val(src); + this.$("input#autoplay").prop("checked", (src || "").indexOf("autoplay") >= 0); + this.get_video(); + } + return this._super.apply(this, arguments); + }, + change_input: function (e) { + var $input = $(e.target); + var $button = $input.parent().find("button"); + if ($input.val() === "") { + $button.addClass("btn-default").removeClass("btn-primary"); + } else { + $button.removeClass("btn-default").addClass("btn-primary"); + } + }, + get_embed_video: function (event) { + event.preventDefault(); + var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/); + if (embedvideo) { + this.$("input#urlvideo").val(embedvideo[1]); + this.get_video(event); + } + return false; + }, + get_video: function (event) { + if (event) event.preventDefault(); + var $video = createVideoNode(this.$("input#urlvideo").val(), {autoplay: this.$("input#autoplay").is(":checked")}); + this.$iframe.replaceWith($video); + this.$iframe = $video; + return false; + }, + save: function () { + var video_id = this.$("#video_id").val(); + if (!video_id) { + this.$("button.btn-primary").click(); + video_id = this.$("#video_id").val(); + } + var $iframe = $( + '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+ + '<div class="css_editable_mode_display"> </div>'+ + '<div class="media_iframe_video_size" contentEditable="false"> </div>'+ + '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+ + '</div>'); + $(this.media).replaceWith($iframe); + this.media = $iframe[0]; + + return this.media; + }, + clear: function () { + if (this.media.dataset.src) { + try { + delete this.media.dataset.src; + } catch(e) { + this.media.dataset.src = undefined; + } + } + this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' '); + }, +}); + +/** + * The Link Dialog allows to customize link content and style. + */ +var LinkDialog = Dialog.extend({ + template: 'web_editor.dialog.link', + events: _.extend({}, Dialog.prototype.events, { + 'change :input.url-source': 'changed', + 'keyup :input.url': 'onkeyup', + 'keyup :input': 'preview', + 'click button.remove': 'remove_link', + 'change input#o_link_dialog_label_input': function (e) { + this.text = $(e.target).val(); + }, + 'change .link-style': function (e) { + this.preview(); + }, + }), + init: function (parent, options, editable, linkInfo) { + this._super(parent, _.extend({}, { + title: _t("Link to") + }, options || {})); + this.editable = editable; + this.data = linkInfo || {}; + + this.data.className = ""; + if (this.data.range) { + this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || ""; + this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' '); + + var is_link = this.data.range.isOnAnchor(); + var r = this.data.range; + + var sc = r.sc; + var so = r.so; + var ec = r.ec; + var eo = r.eo; + + var nodes; + if (!is_link) { + if (sc.tagName) { + sc = dom.firstChild(so ? sc.childNodes[so] : sc); + so = 0; + } else if (so !== sc.textContent.length) { + if (sc === ec) { + ec = sc = sc.splitText(so); + eo -= so; + } else { + sc = sc.splitText(so); + } + so = 0; + } + if (ec.tagName) { + ec = dom.lastChild(eo ? ec.childNodes[eo-1] : ec); + eo = ec.textContent.length; + } else if (eo !== ec.textContent.length) { + ec.splitText(eo); + } + + nodes = dom.listBetween(sc, ec); + + // browsers can't target a picture or void node + if (dom.isVoid(sc) || dom.isImg(sc)) { + so = dom.listPrev(sc).length-1; + sc = sc.parentNode; + } + if (dom.isBR(ec)) { + eo = dom.listPrev(ec).length-1; + ec = ec.parentNode; + } else if (dom.isVoid(ec) || dom.isImg(sc)) { + eo = dom.listPrev(ec).length; + ec = ec.parentNode; + } + + this.data.range = range.create(sc, so, ec, eo); + this.data.range.select(); + } else { + nodes = dom.ancestor(sc, dom.isAnchor).childNodes; + } + + if (dom.isImg(sc) && nodes.indexOf(sc) === -1) { + nodes.push(sc); + } + if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) { + var text = ""; + this.data.images = []; + for (var i=0; i<nodes.length; i++) { + if (dom.ancestor(nodes[i], dom.isImg)) { + this.data.images.push(dom.ancestor(nodes[i], dom.isImg)); + text += '[IMG]'; + } else if (!is_link && i===0) { + text += nodes[i].textContent.slice(so, Infinity); + } else if (!is_link && i===nodes.length-1) { + text += nodes[i].textContent.slice(0, eo); + } else { + text += nodes[i].textContent; + } + } + this.data.text = text; + } + } + + this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' '); + }, + start: function () { + this.bind_data(); + this.$('input.url-source:eq(1)').closest('.list-group-item').addClass('active'); + this.$('#o_link_dialog_label_input').focus(); + return this._super.apply(this, arguments); + }, + get_data: function (test) { + var self = this; + var def = new $.Deferred(); + var $e = this.$('.active input.url-source'); + if (!$e.length) { + $e = this.$('input.url-source:first'); + } + var val = $e.val(); + var label = this.$('#o_link_dialog_label_input').val() || val; + + if (label && this.data.images) { + for(var i=0; i<this.data.images.length; i++) { + label = label.replace(/</, "<").replace(/>/, ">").replace(/\[IMG\]/, this.data.images[i].outerHTML); + } + } + + if (!test && (!val || !$e[0].checkValidity())) { + // FIXME: error message + $e.closest('.form-group').addClass('has-error'); + $e.focus(); + def.reject(); + } + + var style = this.$("input[name='link-style-type']:checked").val() || ''; + var size = this.$("select.link-style").val() || ''; + var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size; + var isNewWindow = this.$('input.window-new').prop('checked'); + + if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) { + self.get_data_buy_mail(def, $e, isNewWindow, label, classes, test); + } else { + self.get_data_buy_url(def, $e, isNewWindow, label, classes, test); + } + return def; + }, + get_data_buy_mail: function (def, $e, isNewWindow, label, classes, test) { + var val = $e.val(); + def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes); + }, + get_data_buy_url: function (def, $e, isNewWindow, label, classes, test) { + def.resolve($e.val(), isNewWindow, label, classes); + }, + save: function () { + var self = this; + var _super = this._super.bind(this); + return this.get_data().then(function (url, new_window, label, classes) { + self.data.url = url; + self.data.isNewWindow = new_window; + self.data.text = label; + self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, ''); + if (classes.replace(/(^|[ ])(btn-default|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi, ' ')) { + self.data.style = {'background-color': '', 'color': ''}; + } + self.final_data = self.data; + }).then(_super); + }, + bind_data: function () { + var href = this.data.url; + var new_window = this.data.isNewWindow; + var text = this.data.text; + var classes = this.data.iniClassName; + + this.$('input#o_link_dialog_label_input').val(text); + this.$('input.window-new').prop('checked', new_window); + this.$('input.link-style').prop('checked', false).first().prop("checked", true); + + if (classes) { + this.$('input.link-style, select.link-style > option').each(function () { + var $option = $(this); + if ($option.val() && classes.indexOf($option.val()) >= 0) { + if ($option.is("input")) { + $option.prop("checked", true); + } else { + $option.parent().val($option.val()); + } + } + }); + } + + if (href) { + var match = /mailto:(.+)/.exec(href); + if (match) { + this.$('input.email-address').val(match = /mailto:(.+)/.exec(href) ? match[1] : ''); + } else { + this.$('input.url').val(href); + } + } + this.preview(); + }, + changed: function (e) { + $(e.target).closest('.list-group-item') + .addClass('active') + .siblings().removeClass('active') + .addBack().removeClass('has-error'); + this.preview(); + }, + onkeyup: function (e) { + var $e = $(e.target); + var is_link = $e.val().indexOf("@") === -1; + this.$('input.window-new').closest("div").toggle(is_link); + this.preview(); + }, + preview: function () { + var $preview = this.$("#link-preview"); + this.get_data(true).then(function (url, new_window, label, classes) { + $preview.attr("target", new_window ? '_blank' : "") + .attr("href", url && url.length ? url : "#") + .html((label && label.length ? label : url)) + .attr("class", classes.replace(/pull-\w+/, '') + " o_btn_preview"); + }); + } +}); + +return { + Dialog: Dialog, + alt: alt, + MediaDialog: MediaDialog, + LinkDialog: LinkDialog, + getCssSelectors: getCssSelectors, + fontIcons: fontIcons, +}; +}); |
