(function () { 'use strict'; var website = openerp.website; var _t = openerp._t; website.no_editor = !!$(document.documentElement).data('editable-no-editor'); website.add_template_file('/website/static/src/xml/website.editor.xml'); website.dom_ready.done(function () { var is_smartphone = $(document.body)[0].clientWidth < 767; if (!is_smartphone) { website.ready().then(website.init_editor); } else { var resize_smartphone = function () { is_smartphone = $(document.body)[0].clientWidth < 767; if (!is_smartphone) { $(window).off("resize", resize_smartphone); website.init_editor(); } }; $(window).on("resize", resize_smartphone); } $(document).on('click', 'a.js_link2post', function (ev) { ev.preventDefault(); website.form(this.pathname, 'POST'); }); $(document).on('click', '.cke_editable label', function (ev) { ev.preventDefault(); }); $(document).on('submit', '.cke_editable form', function (ev) { // Disable form submition in editable mode ev.preventDefault(); }); $(document).on('hide.bs.dropdown', '.dropdown', function (ev) { // Prevent dropdown closing when a contenteditable children is focused if (ev.originalEvent && $(ev.target).has(ev.originalEvent.target).length && $(ev.originalEvent.target).is('[contenteditable]')) { ev.preventDefault(); } }); }); /** * An editing host is an HTML element with @contenteditable=true, or the * child of a document in designMode=on (but that one's not supported) * * https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#editing-host */ function is_editing_host(element) { return element.getAttribute('contentEditable') === 'true'; } /** * Checks that both the element's content *and the element itself* are * editable: an editing host is considered non-editable because its content * is editable but its attributes should not be considered editable */ function is_editable_node(element) { return !(element.data('oe-model') === 'ir.ui.view' || element.data('cke-realelement') || (is_editing_host(element) && element.getAttribute('attributeEditable') !== 'true') || element.isReadOnly()); } function link_dialog(editor) { return new website.editor.RTELinkDialog(editor).appendTo(document.body); } function image_dialog(editor, image) { return new website.editor.MediaDialog(editor, image).appendTo(document.body); } // only enable editors manually CKEDITOR.disableAutoInline = true; // EDIT ALL THE THINGS CKEDITOR.dtd.$editable = _.omit( $.extend({}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline), // well maybe not *all* the things 'ul', 'ol', 'li', 'table', 'tr', 'th', 'td'); // Disable removal of empty elements on CKEDITOR activation. Empty // elements are used for e.g. support of FontAwesome icons CKEDITOR.dtd.$removeEmpty = {}; website.init_editor = function () { CKEDITOR.plugins.add('customdialogs', { // requires: 'link,image', init: function (editor) { editor.on('doubleclick', function (evt) { var element = evt.data.element; if ((element.is('img') || element.$.className.indexOf(' fa-') != -1) && is_editable_node(element)) { image_dialog(editor, element); return; } var parent = new CKEDITOR.dom.element(element.$.parentNode); if (parent.$.className.indexOf('media_iframe_video') != -1 && is_editable_node(parent)) { image_dialog(editor, parent); return; } element = get_selected_link(editor) || evt.data.element; if (!(element.is('a') && is_editable_node(element))) { return; } editor.getSelection().selectElement(element); link_dialog(editor); }, null, null, 500); //noinspection JSValidateTypes editor.addCommand('link', { exec: function (editor) { link_dialog(editor); return true; }, canUndo: false, editorFocus: true, context: 'a', }); //noinspection JSValidateTypes editor.addCommand('cimage', { exec: function (editor) { image_dialog(editor); return true; }, canUndo: false, editorFocus: true, context: 'img', }); editor.ui.addButton('Link', { label: 'Link', command: 'link', toolbar: 'links,10', }); editor.ui.addButton('Image', { label: 'Image', command: 'cimage', toolbar: 'insert,10', }); editor.setKeystroke(CKEDITOR.CTRL + 76 /*L*/, 'link'); } }); CKEDITOR.plugins.add( 'tablebutton', { requires: 'panelbutton,floatpanel', init: function( editor ) { var label = "Table"; editor.ui.add('TableButton', CKEDITOR.UI_PANELBUTTON, { label: label, title: label, // use existing 'table' icon icon: 'table', modes: { wysiwyg: true }, editorFocus: true, // panel opens in iframe, @css is CSS file -ed within // frame document, @attributes are set on iframe itself. panel: { css: '/website/static/src/css/editor.css', attributes: { 'role': 'listbox', 'aria-label': label, }, }, onBlock: function (panel, block) { block.autoSize = true; block.element.setHtml(openerp.qweb.render('website.editor.table.panel', { rows: 5, cols: 5, })); var $table = $(block.element.$).on('mouseenter', 'td', function (e) { var $e = $(e.target); var y = $e.index() + 1; var x = $e.closest('tr').index() + 1; $table .find('td').removeClass('selected').end() .find('tr:lt(' + String(x) + ')') .children().filter(function () { return $(this).index() < y; }) .addClass('selected'); }).on('click', 'td', function (e) { var $e = $(e.target); //noinspection JSPotentiallyInvalidConstructorUsage var table = new CKEDITOR.dom.element( $(openerp.qweb.render('website.editor.table', { rows: $e.closest('tr').index() + 1, cols: $e.index() + 1, }))[0]); editor.insertElement(table); setTimeout(function () { //noinspection JSPotentiallyInvalidConstructorUsage var firstCell = new CKEDITOR.dom.element(table.$.rows[0].cells[0]); var range = editor.createRange(); range.moveToPosition(firstCell, CKEDITOR.POSITION_AFTER_START); range.select(); }, 0); }); block.element.getDocument().getBody().setStyle('overflow', 'hidden'); CKEDITOR.ui.fire('ready', this); }, }); } }); CKEDITOR.plugins.add('oeref', { requires: 'widget', init: function (editor) { var specials = { // Can't find the correct ACL rule to only allow img tags image: { content: '*' }, html: { text: '*' }, monetary: { text: { selector: 'span.oe_currency_value', allowedContent: { } } } }; _(specials).each(function (editable, type) { editor.widgets.add(type, { draggable: false, editables: editable, upcast: function (el) { return el.attributes['data-oe-type'] === type; } }); }); editor.widgets.add('oeref', { draggable: false, editables: { text: { selector: '*', allowedContent: { } }, }, upcast: function (el) { var type = el.attributes['data-oe-type']; if (!type || (type in specials)) { return false; } if (el.attributes['data-oe-original']) { while (el.children.length) { el.children[0].remove(); } el.add(new CKEDITOR.htmlParser.text( el.attributes['data-oe-original'] )); } return true; } }); editor.widgets.add('icons', { draggable: false, init: function () { this.on('edit', function () { new website.editor.MediaDialog(editor, this.element) .appendTo(document.body); }); }, upcast: function (el) { return el.hasClass('fa') // ignore ir.ui.view (other data-oe-model should // already have been matched by oeref and // monetary? && !el.attributes['data-oe-model']; } }); } }); var editor = new website.EditorBar(); var $body = $(document.body); editor.prependTo($body).then(function () { if (location.search.indexOf("enable_editor") >= 0) { editor.edit(); } }); website.editor_bar = editor; }; /* ----- TOP EDITOR BAR FOR ADMIN ---- */ website.EditorBar = openerp.Widget.extend({ template: 'website.editorbar', events: { 'click button[data-action=save]': 'save', 'click a[data-action=cancel]': 'cancel', }, start: function() { var self = this; this.saving_mutex = new openerp.Mutex(); this.$buttons = { edit: this.$el.parents().find('button[data-action=edit]'), save: this.$('button[data-action=save]'), cancel: this.$('button[data-action=cancel]'), }; this.$('#website-top-edit').hide(); this.$('#website-top-view').show(); var $edit_button = this.$buttons.edit .prop('disabled', website.no_editor); if (website.no_editor) { var help_text = $(document.documentElement).data('editable-no-editor'); $edit_button.parent() // help must be set on form above button because it does // not appear on disabled button .attr('title', help_text); } $('.dropdown-toggle').dropdown(); this.$buttons.edit.click(function(ev) { self.edit(); }); this.rte = new website.RTE(this); this.rte.on('change', this, this.proxy('rte_changed')); this.rte.on('rte:ready', this, function () { self.setup_hover_buttons(); self.trigger('rte:ready'); }); this.rte.appendTo(this.$('#website-top-edit .nav.js_editor_placeholder')); return this._super.apply(this, arguments); }, edit: function () { this.$buttons.edit.prop('disabled', true); this.$('#website-top-view').hide(); this.$el.show(); this.$('#website-top-edit').show(); $('.css_non_editable_mode_hidden').removeClass("css_non_editable_mode_hidden"); this.rte.start_edition(); this.trigger('rte:called'); }, rte_changed: function () { this.$buttons.save.prop('disabled', false); }, save: function () { var self = this; observer.disconnect(); var editor = this.rte.editor; var root = editor.element && editor.element.$; try { editor.destroy(); } catch(err) { // Hack to avoid the lost of all changes because ckeditor fails in destroy console.log("Error in editor.destroy() : " + err.toString() + "\n " + err.stack); } // FIXME: select editables then filter by dirty? var defs = this.rte.fetch_editables(root) .filter('.oe_dirty') .removeAttr('contentEditable') .removeClass('oe_dirty oe_editable cke_focus oe_carlos_danger') .map(function () { var $el = $(this); // TODO: Add a queue with concurrency limit in webclient // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js return self.saving_mutex.exec(function () { return self.saveElement($el) .then(undefined, function (thing, response) { // because ckeditor regenerates all the dom, // we can't just setup the popover here as // everything will be destroyed by the DOM // regeneration. Add markings instead, and // returns a new rejection with all relevant // info var id = _.uniqueId('carlos_danger_'); $el.addClass('oe_dirty oe_carlos_danger'); $el.addClass(id); return $.Deferred().reject({ id: id, error: response.data, }); }); }); }).get(); return $.when.apply(null, defs).then(function () { website.reload(); }, function (failed) { // If there were errors, re-enable edition self.rte.start_edition(true).then(function () { // jquery's deferred being a pain in the ass if (!_.isArray(failed)) { failed = [failed]; } _(failed).each(function (failure) { var html = failure.error.exception_type === "except_osv"; if (html) { var msg = $("
").text(failure.error.message).html(); var data = msg.substring(3,msg.length-2).split(/', u'/); failure.error.message = '' + data[0] + '
' + data[1]; } $(root).find('.' + failure.id) .removeClass(failure.id) .popover({ html: html, trigger: 'hover', content: failure.error.message, placement: 'auto top', }) // Force-show popovers so users will notice them. .popover('show'); }); }); }); }, /** * Saves an RTE content, which always corresponds to a view section (?). */ saveElement: function ($el) { // escape text nodes for xml saving var escaped_el = $el.clone(); var to_escape = escaped_el.find('*').addBack(); to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack()); to_escape.contents().each(function(){ if(this.nodeType == 3) { this.nodeValue = $('
').text(this.nodeValue).html(); } }); var markup = escaped_el.prop('outerHTML'); return openerp.jsonRpc('/web/dataset/call', 'call', { model: 'ir.ui.view', method: 'save', args: [$el.data('oe-id'), markup, $el.data('oe-xpath') || null, website.get_context()], }); }, cancel: function () { new $.Deferred(function (d) { var $dialog = $(openerp.qweb.render('website.editor.discard')).appendTo(document.body); $dialog.on('click', '.btn-danger', function () { d.resolve(); }).on('hidden.bs.modal', function () { d.reject(); }); d.always(function () { $dialog.remove(); }); $dialog.modal('show'); }).then(function () { website.reload(); }) }, /** * Creates a "hover" button for link edition * * @param {Function} editfn edition function, called when clicking the button * @returns {jQuery} */ make_hover_button_link: function (editfn) { return $(openerp.qweb.render('website.editor.hoverbutton.link', {})) .hide() .click(function (e) { e.preventDefault(); e.stopPropagation(); editfn.call(this, e); }) .appendTo(document.body); }, /** * Creates a "hover" button for image * * @param {Function} editfn edition function, called when clicking the button * @param {Function} stylefn edition style function, called when clicking the button * @returns {jQuery} */ make_hover_button_image: function (editfn, stylefn) { var $div = $(openerp.qweb.render('website.editor.hoverbutton.media', {})) .hide() .appendTo(document.body); $div.find('[data-toggle="dropdown"]').dropdown(); $div.find(".hover-edition-button").click(function (e) { e.preventDefault(); e.stopPropagation(); editfn.call(this, e); }); if (stylefn) { $div.find(".hover-style-button").click(function (e) { e.preventDefault(); e.stopPropagation(); stylefn.call(this, e); }); } return $div; }, /** * For UI clarity, during RTE edition when the user hovers links and * images a small button should appear to make the capability clear, * as not all users think of double-clicking the image or link. */ setup_hover_buttons: function () { var editor = this.rte.editor; var $link_button = this.make_hover_button_link(function () { var sel = new CKEDITOR.dom.element(previous); editor.getSelection().selectElement(sel); if(sel.hasClass('fa')) { new website.editor.MediaDialog(editor, previous) .appendTo(document.body); } else if (previous.tagName.toUpperCase() === 'A') { link_dialog(editor); } $link_button.hide(); previous = null; }); function is_icons_widget(element) { var w = editor.widgets.getByElement(element); return w && w.name === 'icons'; } // previous is the state of the button-trigger: it's the // currently-ish hovered element which can trigger a button showing. // -ish, because when moving to the button itself ``previous`` is // still set to the element having triggered showing the button. var previous; $(editor.element.$).on('mouseover', 'a', function () { // Back from edit button -> ignore if (previous && previous === this) { return; } // hover button should appear for "editable" links and images // (img and a nodes whose *attributes* are editable, they // can not be "editing hosts") *or* for non-editing-host // elements bearing an ``fa`` class. These should have been // made into CKE widgets which are editing hosts by // definition, so instead check if the element has been // converted/upcasted to an fa widget var selected = new CKEDITOR.dom.element(this); if (!(is_editable_node(selected) || is_icons_widget(selected))) { return; } previous = this; var $selected = $(this); var position = $selected.offset(); $link_button.show().offset({ top: $selected.outerHeight() + position.top, left: $selected.outerWidth() / 2 + position.left - $link_button.outerWidth() / 2 }) }).on('mouseleave', 'a, img, .fa', function (e) { var current = document.elementFromPoint(e.clientX, e.clientY); if (current === $link_button[0] || $(current).parent()[0] === $link_button[0]) { return; } $link_button.hide(); previous = null; }); } }); website.EditorBarCustomize = openerp.Widget.extend({ events: { 'mousedown a.dropdown-toggle': 'load_menu', 'click ul a[data-view-id]': 'do_customize', }, start: function() { var self = this; this.$menu = self.$el.find('ul'); this.view_name = $(document.documentElement).data('view-xmlid'); if (!this.view_name) { this.$el.hide(); } this.loaded = false; }, load_menu: function () { var self = this; if(this.loaded) { return; } openerp.jsonRpc('/website/customize_template_get', 'call', { 'xml_id': this.view_name }).then( function(result) { _.each(result, function (item) { if (item.xml_id === "website.debugger" && !window.location.search.match(/[&?]debug(&|$)/)) return; if (item.header) { self.$menu.append(''); } else { self.$menu.append(_.str.sprintf('
  • %s
  • ', item.id, item.active ? '-check' : '', item.name)); } }); self.loaded = true; } ); }, do_customize: function (event) { var view_id = $(event.currentTarget).data('view-id'); return openerp.jsonRpc('/web/dataset/call_kw', 'call', { model: 'ir.ui.view', method: 'toggle', args: [], kwargs: { ids: [parseInt(view_id, 10)], context: website.get_context() } }).then( function() { window.location.reload(); }); }, }); $(document).ready(function() { var editorBarCustomize = new website.EditorBarCustomize(); editorBarCustomize.setElement($('li[id=customize-menu]')); editorBarCustomize.start(); }); var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(','); /* ----- RICH TEXT EDITOR ---- */ website.RTE = openerp.Widget.extend({ tagName: 'li', id: 'oe_rte_toolbar', className: 'oe_right oe_rte_toolbar', // editor.ui.items -> possible commands &al // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'})); init: function (EditorBar) { this.EditorBar = EditorBar; this._super.apply(this, arguments); }, /** * In Webkit-based browsers, triple-click will select a paragraph up to * the start of the next "paragraph" including any empty space * inbetween. When said paragraph is removed or altered, it nukes * the empty space and brings part of the content of the next * "paragraph" (which may well be e.g. an image) into the current one, * completely fucking up layouts and breaking snippets. * * Try to fuck around with selections on triple-click to attempt to * fix this garbage behavior. * * Note: for consistent behavior we may actually want to take over * triple-clicks, in all browsers in order to ensure consistent cross- * platform behavior instead of being at the mercy of rendering engines * & platform selection quirks? */ webkitSelectionFixer: function (root) { root.addEventListener('click', function (e) { // only webkit seems to have a fucked up behavior, ignore others // FIXME: $.browser goes away in jquery 1.9... if (!$.browser.webkit) { return; } // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mouseevents // The detail attribute indicates the number of times a mouse button has been pressed // we just want the triple click if (e.detail !== 3) { return; } e.preventDefault(); // Get closest block-level element to the triple-clicked // element (using ckeditor's block list because why not) var $closest_block = $(e.target).closest(blocks_selector); // manually set selection range to the content of the // triple-clicked block-level element, to avoid crossing over // between block-level elements document.getSelection().selectAllChildren($closest_block[0]); }); }, tableNavigation: function (root) { var self = this; $(root).on('keydown', function (e) { // ignore non-TAB if (e.which !== 9) { return; } if (self.handleTab(e)) { e.preventDefault(); } }); }, /** * Performs whatever operation is necessary on a [TAB] hit, returns * ``true`` if the event's default should be cancelled (if the TAB was * handled by the function) */ handleTab: function (event) { var forward = !event.shiftKey; var root = window.getSelection().getRangeAt(0).commonAncestorContainer; var $cell = $(root).closest('td,th'); if (!$cell.length) { return false; } var cell = $cell[0]; // find cell in same row var row = cell.parentNode; var sibling = row.cells[cell.cellIndex + (forward ? 1 : -1)]; if (sibling) { document.getSelection().selectAllChildren(sibling); return true; } // find cell in previous/next row var table = row.parentNode; var sibling_row = table.rows[row.rowIndex + (forward ? 1 : -1)]; if (sibling_row) { var new_cell = sibling_row.cells[forward ? 0 : sibling_row.cells.length - 1]; document.getSelection().selectAllChildren(new_cell); return true; } // at edge cells, copy word/openoffice behavior: if going backwards // from first cell do nothing, if going forwards from last cell add // a row if (forward) { var row_size = row.cells.length; var new_row = document.createElement('tr'); while(row_size--) { var newcell = document.createElement('td'); // zero-width space newcell.textContent = '\u200B'; new_row.appendChild(newcell); } table.appendChild(new_row); document.getSelection().selectAllChildren(new_row.cells[0]); } return true; }, /** * Makes the page editable * * @param {Boolean} [restart=false] in case the edition was already set * up once and is being re-enabled. * @returns {$.Deferred} deferred indicating when the RTE is ready */ start_edition: function (restart) { var self = this; // create a single editor for the whole page var root = document.getElementById('wrapwrap'); if (!restart) { $(root).on('dragstart', 'img', function (e) { e.preventDefault(); }); this.webkitSelectionFixer(root); this.tableNavigation(root); } var def = $.Deferred(); var editor = this.editor = CKEDITOR.inline(root, self._config()); editor.on('instanceReady', function () { $("[data-oe-type=selection]").attr("contenteditable",false); editor.setReadOnly(false); // ckeditor set root to editable, disable it (only inner // sections are editable) // FIXME: are there cases where the whole editor is editable? editor.editable().setReadOnly(true); self.setup_editables(root); try { // disable firefox's broken table resizing thing document.execCommand("enableObjectResizing", false, "false"); document.execCommand("enableInlineTableEditing", false, "false"); } catch (e) {} // detect & setup any CKEDITOR widget within a newly dropped // snippet. There does not seem to be a simple way to do it for // HTML not inserted via ckeditor APIs: // https://dev.ckeditor.com/ticket/11472 $(document.body) .off('snippet-dropped') .on('snippet-dropped', function (e, el) { // CKEDITOR data processor extended by widgets plugin // to add wrappers around upcasting elements el.innerHTML = editor.dataProcessor.toHtml(el.innerHTML, { fixForBody: false, dontFilter: true, }); // then repository.initOnAll() handles the conversion // from wrapper to actual widget instance (or something // like that). setTimeout(function () { editor.widgets.initOnAll(); }, 0); }); self.trigger('rte:ready'); def.resolve(); }); return def; }, setup_editables: function (root) { // selection of editable sub-items was previously in // EditorBar#edit, but for some unknown reason the elements were // apparently removed and recreated (?) at editor initalization, // and observer setup was lost. var self = this; // setup dirty-marking for each editable element this.fetch_editables(root) .addClass('oe_editable') .each(function () { var node = this; var $node = $(node); // only explicitly set contenteditable on view sections, // cke widgets system will do the widgets themselves if ($node.data('oe-model') === 'ir.ui.view') { node.contentEditable = true; } observer.observe(node, OBSERVER_CONFIG); $node.one('content_changed', function () { $node.addClass('oe_dirty'); self.trigger('change'); }); }); }, fetch_editables: function (root) { return $(root).find('[data-oe-model]') .not('[data-oe-type = "selection"]') .not('link, script') .not('.oe_snippet_editor'); }, _current_editor: function () { return CKEDITOR.currentInstance; }, _config: function () { // base plugins minus // - magicline (captures mousein/mouseout -> breaks draggable) // - contextmenu & tabletools (disable contextual menu) // - bunch of unused plugins var plugins = [ 'a11yhelp', 'basicstyles', 'blockquote', 'clipboard', 'colorbutton', 'colordialog', 'dialogadvtab', 'elementspath', /*'enterkey',*/ 'entities', 'filebrowser', 'find', 'floatingspace','format', 'htmlwriter', 'iframe', 'indentblock', 'indentlist', 'justify', 'list', 'pastefromword', 'pastetext', 'preview', 'removeformat', 'resize', 'save', 'selectall', 'stylescombo', 'table', 'templates', 'toolbar', 'undo', 'wysiwygarea' ]; return { // FIXME language: 'en', // Disable auto-generated titles // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label title: false, plugins: plugins.join(','), uiColor: '', // FIXME: currently breaks RTE? // Ensure no config file is loaded customConfig: '', // Disable ACF allowedContent: true, // Don't insert paragraphs around content in e.g.
  • autoParagraph: false, // Don't automatically add   or
    in empty block-level // elements when edition starts fillEmptyBlocks: false, filebrowserImageUploadUrl: "/website/attach", // Support for sharedSpaces in 4.x extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref', // 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", "Blockquote", "BulletedList", "NumberedList", "Indent", "Outdent" ]},{ name: 'justify', items: [ "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock" ]},{ name: 'special', items: [ "Image", "TableButton" ]},{ name: 'styles', items: [ "Styles" ]} ], // styles dropdown in toolbar stylesSet: [ {name: "Normal", element: 'p'}, {name: "Heading 1", element: 'h1'}, {name: "Heading 2", element: 'h2'}, {name: "Heading 3", element: 'h3'}, {name: "Heading 4", element: 'h4'}, {name: "Heading 5", element: 'h5'}, {name: "Heading 6", element: 'h6'}, {name: "Formatted", element: 'pre'}, {name: "Address", element: 'address'} ], }; }, }); website.editor = { }; website.editor.Dialog = openerp.Widget.extend({ events: { 'hidden.bs.modal': 'destroy', 'click button.save': 'save', 'click button[data-dismiss="modal"]': 'cancel', }, init: function (editor) { this._super(); this.editor = editor; }, start: function () { var sup = this._super(); this.$el.modal({backdrop: 'static'}); this.$('input:first').focus(); return sup; }, save: function () { this.close(); this.trigger("saved"); }, cancel: function () { this.trigger("cancel"); }, close: function () { this.$el.modal('hide'); }, destroy: function () { this.$el.modal('hide').remove(); if($(".modal.in").length>0){ $('body').addClass('modal-open'); } }, }); website.editor.LinkDialog = website.editor.Dialog.extend({ template: 'website.editor.dialog.link', events: _.extend({}, website.editor.Dialog.prototype.events, { 'change :input.url-source': 'changed', 'keyup :input.url': 'onkeyup', 'keyup :input': 'preview', 'mousedown': function (e) { var $target = $(e.target).closest('.list-group-item:has(.url-source)'); if (!$target.length || $target.hasClass('active')) { // clicked outside groups, or clicked in active groups return; } $target.find("input.url-source").change(); }, 'click button.remove': 'remove_link', 'change input#link-text': function (e) { this.text = $(e.target).val(); }, 'change .link-style': function (e) { this.preview(); }, }), init: function (editor) { this._super(editor); this.text = null; // Store last-performed request to be able to cancel/abort it. this.page_exists_req = null; this.search_pages_req = null; }, start: function () { var self = this; var last; this.$('#link-page').select2({ minimumInputLength: 1, placeholder: _t("New or existing page"), query: function (q) { if (q.term == last) return; last = q.term; $.when( self.page_exists(q.term), self.fetch_pages(q.term) ).then(function (exists, results) { var rs = _.map(results, function (r) { return { id: r.loc, text: r.loc, }; }); if (!exists) { rs.push({ create: true, id: q.term, text: _.str.sprintf(_t("Create page '%s'"), q.term), }); } q.callback({ more: false, results: rs }); }, function () { q.callback({more: false, results: []}); }); }, }); return this._super().then(this.proxy('bind_data')); }, get_data: function (test) { var self = this, def = new $.Deferred(), $e = this.$('.active input.url-source').filter(':input'), val = $e.val(), label = this.$('#link-text').val() || val; if (test !== false && (!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.$("input[name='link-style-size']:checked").val(); var classes = (style && style.length ? "btn " : "") + style + " " + size; if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) { def.resolve('mailto:' + val, false, label, classes); } else if ($e.val() && $e.val().length && $e.hasClass('page')) { var data = $e.select2('data'); if (!data.create) { def.resolve(data.id, false, label || data.text, classes); } else { // Create the page, get the URL back $.get(_.str.sprintf( '/website/add/%s?noredirect=1', encodeURI(data.id))) .then(function (response) { def.resolve(response, false, data.id, classes); }); } } else { def.resolve(val, this.$('input.window-new').prop('checked'), label, classes); } return def; }, save: function () { var self = this; var _super = this._super.bind(this); return this.get_data() .then(function (url, new_window, label, classes) { self.make_link(url, new_window, label, classes); }).then(_super); }, make_link: function (url, new_window, label, classes) { }, bind_data: function () { var self = this; var href = this.element && (this.element.data( 'cke-saved-href') || this.element.getAttribute('href')); var new_window = this.element ? this.element.getAttribute('target') === '_blank' : false; var text = this.element ? this.element.getText() : ''; if (!text.length) { if (this.editor) { text = this.editor.getSelection().getSelectedText(); } else { text = this.data.name; href = this.data.url; new_window = this.data.new_window; } } this.$('input#link-text').val(text); this.$('input.window-new').prop('checked', new_window); var classes = this.element && this.element.$.className; if (classes) { this.$('input[value!=""]').each(function () { var $option = $(this); if (classes.indexOf($option.val()) !== -1) { $option.attr("checked", "checked"); } }); } var match, $control; if (href && (match = /mailto:(.+)/.exec(href))) { this.$('input.email-address').val(match[1]).change(); } if (href && !$control) { this.page_exists(href).then(function (exist) { if (exist) { self.$('#link-page').select2('data', {'id': href, 'text': href}); } else { self.$('input.url').val(href).change(); self.$('input.window-new').closest("div").show(); } }); } this.preview(); }, changed: function (e) { var $e = $(e.target); this.$('.url-source').filter(':input').not($e).val('') .filter(function () { return !!$(this).data('select2'); }) .select2('data', null); $e.closest('.list-group-item') .addClass('active') .siblings().removeClass('active') .addBack().removeClass('has-error'); this.preview(); }, call: function (method, args, kwargs) { var self = this; var req = method + '_req'; if (this[req]) { this[req].abort(); } return this[req] = openerp.jsonRpc('/web/dataset/call_kw', 'call', { model: 'website', method: method, args: args, kwargs: kwargs, }).always(function () { self[req] = null; }); }, page_exists: function (term) { return this.call('page_exists', [null, term], { context: website.get_context(), }); }, fetch_pages: function (term) { return this.call('search_pages', [null, term], { limit: 9, context: website.get_context(), }); }, onkeyup: function (e) { var $e = $(e.target); var is_link = ($e.val()||'').length && $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(false).then(function (url, new_window, label, classes) { $preview.attr("target", new_window ? '_blank' : "") .text((label && label.length ? label : url)) .attr("class", classes); }); } }); website.editor.RTELinkDialog = website.editor.LinkDialog.extend({ start: function () { var element; if ((element = this.get_selected_link()) && element.hasAttribute('href')) { this.editor.getSelection().selectElement(element); } this.element = element; if (element) { this.add_removal_button(); } return this._super(); }, add_removal_button: function () { this.$('.modal-footer').prepend( openerp.qweb.render( 'website.editor.dialog.link.footer-button')); }, remove_link: function () { var editor = this.editor; // same issue as in make_link setTimeout(function () { editor.removeStyle(new CKEDITOR.style({ element: 'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: true, })); }, 0); this.close(); }, /** * Greatly simplified version of CKEDITOR's * plugins.link.dialogs.link.onOk. * * @param {String} url * @param {Boolean} [new_window=false] * @param {String} [label=null] */ make_link: function (url, new_window, label, classes) { var attributes = {href: url, 'data-cke-saved-href': url}; var to_remove = []; if (new_window) { attributes['target'] = '_blank'; } else { to_remove.push('target'); } if (classes && classes.length) { attributes['class'] = classes; } if (this.element) { this.element.setAttributes(attributes); this.element.removeAttributes(to_remove); if (this.text) { this.element.setText(this.text); } } else { var selection = this.editor.getSelection(); var range = selection.getRanges(true)[0]; if (range.collapsed) { //noinspection JSPotentiallyInvalidConstructorUsage var text = new CKEDITOR.dom.text( this.text || label || url); range.insertNode(text); range.selectNodeContents(text); } //noinspection JSPotentiallyInvalidConstructorUsage new CKEDITOR.style({ type: CKEDITOR.STYLE_INLINE, element: 'a', attributes: attributes, }).applyToRange(range); // focus dance between RTE & dialog blow up the stack in Safari // and Chrome, so defer select() until dialog has been closed setTimeout(function () { range.select(); }, 0); } }, /** * CKEDITOR.plugins.link.getSelectedLink ignores the editor's root, * if the editor is set directly on a link it will thus not work. */ get_selected_link: function () { return get_selected_link(this.editor); }, }); website.editor.Media = openerp.Widget.extend({ init: function (parent, editor, media) { this._super(); this.parent = parent; this.editor = editor; this.media = media; }, start: function () { this.$preview = this.$('.preview-container').detach(); return this._super(); }, search: function (needle) { }, save: function () { }, clear: function () { }, cancel: function () { }, close: function () { }, }); website.editor.MediaDialog = website.editor.Dialog.extend({ template: 'website.editor.dialog.media', events : _.extend({}, website.editor.Dialog.prototype.events, { 'input input#icon-search': 'search', }), init: function (editor, media) { this._super(editor); this.editor = editor; this.page = 0; this.media = media; }, start: function () { var self = this; if (this.editor.getSelection) { var selection = this.editor.getSelection(); this.range = selection.getRanges(true)[0]; } this.imageDialog = new website.editor.RTEImageDialog(this, this.editor, this.media); this.imageDialog.appendTo(this.$("#editor-media-image")); this.iconDialog = new website.editor.FontIconsDialog(this, this.editor, this.media); this.iconDialog.appendTo(this.$("#editor-media-icon")); this.videoDialog = new website.editor.VideoDialog(this, this.editor, this.media); this.videoDialog.appendTo(this.$("#editor-media-video")); this.active = this.imageDialog; $('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-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"); } }); if (this.media) { if (this.media.$.nodeName === "IMG") { this.$('[href="#editor-media-image"]').tab('show'); } else if (this.media.$.className.match(/(^|\s)media_iframe_video($|\s)/)) { this.$('[href="#editor-media-video"]').tab('show'); } else if (this.media.$.className.match(/(^|\s)fa($|\s)/)) { this.$('[href="#editor-media-icon"]').tab('show'); } if ($(this.media.$).parent().data("oe-field") === "image") { this.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); } } return this._super(); }, save: function () { var self = this; if (self.media) { this.media.$.innerHTML = ""; if (this.active !== this.imageDialog) { this.imageDialog.clear(); } if (this.active !== this.iconDialog) { this.iconDialog.clear(); } if (this.active !== this.videoDialog) { this.videoDialog.clear(); } } else { this.media = new CKEDITOR.dom.element("img"); self.range.insertNode(this.media); self.range.selectNodeContents(this.media); this.active.media = this.media; } var $el = $(self.active.media.$); this.active.save(); this.media.$.className = this.media.$.className.replace(/\s+/g, ' '); setTimeout(function () { if(self.range) self.range.select(); $el.trigger("saved", self.active.media.$); $(document.body).trigger("media-saved", [$el[0], self.active.media.$]); },0); this._super(); }, 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. Lets users change an image, including uploading a * new image in OpenERP or selecting the image style (if supported by * the caller). * * Initialized as usual, but the caller can hook into two events: * * @event start({url, style}) called during dialog initialization and * opening, the handler can *set* the ``url`` * and ``style`` properties on its parameter * to provide these as default values to the * dialog * @event save({url, style}) called during dialog finalization, the handler * is provided with the image url and style * selected by the users (or possibly the ones * originally passed in) */ var IMAGES_PER_ROW = 6; var IMAGES_ROWS = 2; website.editor.ImageDialog = website.editor.Media.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 () { 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", //'change select.image-style': 'preview_image', 'click .existing-attachments img': 'select_existing', 'click .existing-attachment-remove': 'try_remove', }), init: function (parent, editor, media) { this.page = 0; this._super(parent, editor, media); }, start: function () { var self = this; var res = this._super(); var o = { url: null }; // avoid typos, prevent addition of new properties to the object Object.preventExtensions(o); this.trigger('start', o); this.parent.$(".pager > li").click(function (e) { e.preventDefault(); var $target = $(e.currentTarget); if ($target.hasClass('disabled')) { return; } self.page += $target.hasClass('previous') ? -1 : 1; self.display_attachments(); }); this.set_image(o.url); return res; }, save: function () { if (!this.link) { this.link = this.$(".existing-attachments img:first").attr('src'); } this.trigger('save', { url: this.link }); this.media.renameNode("img"); $(this.media).attr('src', this.link); return this._super(); }, clear: function () { this.media.$.className = this.media.$.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' '); }, cancel: function () { this.trigger('cancel'); }, 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"); } }, search: function (needle) { var self = this; this.fetch_existing(needle).then(function () { self.selected_existing(self.$('input.url').val()); }); }, set_image: function (url, error) { var self = this; if (url) this.link = url; this.$('input.url').val(''); this.fetch_existing().then(function () { self.selected_existing(url); }); }, form_submit: function (event) { var self = this; var $form = this.$('form[action="/website/attach"]'); if (!$form.find('input[name="upload"]').val().length) { var url = $form.find('input[name="url"]').val(); if (this.selected_existing(url).size()) { event.preventDefault(); return false; } } var callback = _.uniqueId('func_'); this.$('input[name=func]').val(callback); window[callback] = function (url, error) { delete window[callback]; self.file_selected(url, error); }; }, file_selection: function () { this.$el.addClass('nosave'); this.$('form').removeClass('has-error').find('.help-block').empty(); this.$('button.filepicker').removeClass('btn-danger btn-success'); this.$('form').submit(); }, file_selected: function(url, error) { var $button = this.$('button.filepicker'); if (!error) { $button.addClass('btn-success'); } else { url = null; this.$('form').addClass('has-error') .find('.help-block').text(error); $button.addClass('btn-danger'); } this.set_image(url, error); // auto save and close popup this.parent.save(); }, fetch_existing: function (needle) { var domain = [['res_model', '=', 'ir.ui.view'], '|', ['mimetype', '=', false], ['mimetype', '=like', 'image/%']]; if (needle && needle.length) { domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]); } return openerp.jsonRpc('/web/dataset/call_kw', 'call', { model: 'ir.attachment', method: 'search_read', args: [], kwargs: { fields: ['name', 'website_url'], domain: domain, order: 'id desc', context: website.get_context(), } }).then(this.proxy('fetched_existing')); }, fetched_existing: function (records) { this.records = records; this.display_attachments(); }, display_attachments: function () { this.$('.help-block').empty(); var per_screen = IMAGES_PER_ROW * IMAGES_ROWS; var from = this.page * per_screen; var records = this.records; // Create rows of 3 records var rows = _(records).chain() .slice(from, from + per_screen) .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); }) .values() .value(); this.$('.existing-attachments').replaceWith( openerp.qweb.render( 'website.editor.dialog.image.existing.content', {rows: rows})); this.parent.$('.pager') .find('li.previous').toggleClass('disabled', (from === 0)).end() .find('li.next').toggleClass('disabled', (from + per_screen >= records.length)); }, select_existing: function (e) { var link = $(e.currentTarget).attr('src'); this.link = link; this.selected_existing(link); }, selected_existing: function (link) { this.$('.existing-attachment-cell.media_selected').removeClass("media_selected"); var $select = this.$('.existing-attachment-cell img').filter(function () { return $(this).attr("src") == link; }).first(); $select.parent().addClass("media_selected"); 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 openerp.jsonRpc('/web/dataset/call_kw', 'call', { model: 'ir.attachment', method: 'try_remove', args: [], kwargs: { ids: [id], context: website.get_context() } }).then(function (prevented) { if (_.isEmpty(prevented)) { self.records = _.without(self.records, attachment); self.display_attachments(); return; } $both.css({borderWidth: "", borderColor: ""}); $help_block.replaceWith(openerp.qweb.render( 'website.editor.dialog.image.existing.error', { views: prevented[id] } )); }); }, }); website.editor.RTEImageDialog = website.editor.ImageDialog.extend({ init: function (parent, editor, media) { this._super(parent, editor, media); this.on('start', this, this.proxy('started')); this.on('save', this, this.proxy('saved')); }, started: function (holder) { if (!this.media) { var selection = this.editor.getSelection(); this.media = selection && selection.getSelectedElement(); } var el = this.media; if (!el || !el.is('img')) { return; } holder.url = el.getAttribute('src'); }, saved: function (data) { var element, editor = this.editor; if (!(element = this.media)) { element = editor.document.createElement('img'); element.addClass('img'); element.addClass('img-responsive'); // focus event handler interactions between bootstrap (modal) // and ckeditor (RTE) lead to blowing the stack in Safari and // Chrome (but not FF) when this is done synchronously => // defer insertion so modal has been hidden & destroyed before // it happens setTimeout(function () { editor.insertElement(element); }, 0); } var style = data.style; element.setAttribute('src', data.url); element.removeAttribute('data-cke-saved-src'); if (style) { element.addClass(style); } }, }); function get_selected_link(editor) { var sel = editor.getSelection(), el = sel.getSelectedElement(); if (el && el.is('a')) { return el; } var range = sel.getRanges(true)[0]; if (!range) { return null; } range.shrink(CKEDITOR.SHRINK_TEXT); var commonAncestor = range.getCommonAncestor(); var viewRoot = editor.elementPath(commonAncestor).contains(function (element) { return element.data('oe-model') === 'ir.ui.view'; }); if (!viewRoot) { return null; } // if viewRoot is the first link, don't edit it. return new CKEDITOR.dom.elementPath(commonAncestor, viewRoot) .contains('a', true); } website.editor.FontIconsDialog = website.editor.Media.extend({ template: 'website.editor.dialog.font-icons', events : _.extend({}, website.editor.Dialog.prototype.events, { change: 'update_preview', 'click .font-icons-icon': function (e) { e.preventDefault(); e.stopPropagation(); this.$('#fa-icon').val(e.target.getAttribute('data-id')); this.update_preview(); }, 'click #fa-preview span': function (e) { e.preventDefault(); e.stopPropagation(); this.$('#fa-size').val(e.target.getAttribute('data-size')); this.update_preview(); }, }), // List of FontAwesome icons in 4.0.3, extracted from the cheatsheet. // Each icon provides the unicode codepoint as ``text`` and the class // name as ``id`` so the whole thing can be fed directly to select2 // without post-processing and do the right thing (except for the part // where we still need to implement ``initSelection``) // TODO: add id/name to the text in order to allow FAYT selection of icons? icons: [{"text": "\uf000", "id": "fa-glass"}, {"text": "\uf001", "id": "fa-music"}, {"text": "\uf002", "id": "fa-search"}, {"text": "\uf003", "id": "fa-envelope-o"}, {"text": "\uf004", "id": "fa-heart"}, {"text": "\uf005", "id": "fa-star"}, {"text": "\uf006", "id": "fa-star-o"}, {"text": "\uf007", "id": "fa-user"}, {"text": "\uf008", "id": "fa-film"}, {"text": "\uf009", "id": "fa-th-large"}, {"text": "\uf00a", "id": "fa-th"}, {"text": "\uf00b", "id": "fa-th-list"}, {"text": "\uf00c", "id": "fa-check"}, {"text": "\uf00d", "id": "fa-times"}, {"text": "\uf00e", "id": "fa-search-plus"}, {"text": "\uf010", "id": "fa-search-minus"}, {"text": "\uf011", "id": "fa-power-off"}, {"text": "\uf012", "id": "fa-signal"}, {"text": "\uf013", "id": "fa-cog"}, {"text": "\uf014", "id": "fa-trash-o"}, {"text": "\uf015", "id": "fa-home"}, {"text": "\uf016", "id": "fa-file-o"}, {"text": "\uf017", "id": "fa-clock-o"}, {"text": "\uf018", "id": "fa-road"}, {"text": "\uf019", "id": "fa-download"}, {"text": "\uf01a", "id": "fa-arrow-circle-o-down"}, {"text": "\uf01b", "id": "fa-arrow-circle-o-up"}, {"text": "\uf01c", "id": "fa-inbox"}, {"text": "\uf01d", "id": "fa-play-circle-o"}, {"text": "\uf01e", "id": "fa-repeat"}, {"text": "\uf021", "id": "fa-refresh"}, {"text": "\uf022", "id": "fa-list-alt"}, {"text": "\uf023", "id": "fa-lock"}, {"text": "\uf024", "id": "fa-flag"}, {"text": "\uf025", "id": "fa-headphones"}, {"text": "\uf026", "id": "fa-volume-off"}, {"text": "\uf027", "id": "fa-volume-down"}, {"text": "\uf028", "id": "fa-volume-up"}, {"text": "\uf029", "id": "fa-qrcode"}, {"text": "\uf02a", "id": "fa-barcode"}, {"text": "\uf02b", "id": "fa-tag"}, {"text": "\uf02c", "id": "fa-tags"}, {"text": "\uf02d", "id": "fa-book"}, {"text": "\uf02e", "id": "fa-bookmark"}, {"text": "\uf02f", "id": "fa-print"}, {"text": "\uf030", "id": "fa-camera"}, {"text": "\uf031", "id": "fa-font"}, {"text": "\uf032", "id": "fa-bold"}, {"text": "\uf033", "id": "fa-italic"}, {"text": "\uf034", "id": "fa-text-height"}, {"text": "\uf035", "id": "fa-text-width"}, {"text": "\uf036", "id": "fa-align-left"}, {"text": "\uf037", "id": "fa-align-center"}, {"text": "\uf038", "id": "fa-align-right"}, {"text": "\uf039", "id": "fa-align-justify"}, {"text": "\uf03a", "id": "fa-list"}, {"text": "\uf03b", "id": "fa-outdent"}, {"text": "\uf03c", "id": "fa-indent"}, {"text": "\uf03d", "id": "fa-video-camera"}, {"text": "\uf03e", "id": "fa-picture-o"}, {"text": "\uf040", "id": "fa-pencil"}, {"text": "\uf041", "id": "fa-map-marker"}, {"text": "\uf042", "id": "fa-adjust"}, {"text": "\uf043", "id": "fa-tint"}, {"text": "\uf044", "id": "fa-pencil-square-o"}, {"text": "\uf045", "id": "fa-share-square-o"}, {"text": "\uf046", "id": "fa-check-square-o"}, {"text": "\uf047", "id": "fa-arrows"}, {"text": "\uf048", "id": "fa-step-backward"}, {"text": "\uf049", "id": "fa-fast-backward"}, {"text": "\uf04a", "id": "fa-backward"}, {"text": "\uf04b", "id": "fa-play"}, {"text": "\uf04c", "id": "fa-pause"}, {"text": "\uf04d", "id": "fa-stop"}, {"text": "\uf04e", "id": "fa-forward"}, {"text": "\uf050", "id": "fa-fast-forward"}, {"text": "\uf051", "id": "fa-step-forward"}, {"text": "\uf052", "id": "fa-eject"}, {"text": "\uf053", "id": "fa-chevron-left"}, {"text": "\uf054", "id": "fa-chevron-right"}, {"text": "\uf055", "id": "fa-plus-circle"}, {"text": "\uf056", "id": "fa-minus-circle"}, {"text": "\uf057", "id": "fa-times-circle"}, {"text": "\uf058", "id": "fa-check-circle"}, {"text": "\uf059", "id": "fa-question-circle"}, {"text": "\uf05a", "id": "fa-info-circle"}, {"text": "\uf05b", "id": "fa-crosshairs"}, {"text": "\uf05c", "id": "fa-times-circle-o"}, {"text": "\uf05d", "id": "fa-check-circle-o"}, {"text": "\uf05e", "id": "fa-ban"}, {"text": "\uf060", "id": "fa-arrow-left"}, {"text": "\uf061", "id": "fa-arrow-right"}, {"text": "\uf062", "id": "fa-arrow-up"}, {"text": "\uf063", "id": "fa-arrow-down"}, {"text": "\uf064", "id": "fa-share"}, {"text": "\uf065", "id": "fa-expand"}, {"text": "\uf066", "id": "fa-compress"}, {"text": "\uf067", "id": "fa-plus"}, {"text": "\uf068", "id": "fa-minus"}, {"text": "\uf069", "id": "fa-asterisk"}, {"text": "\uf06a", "id": "fa-exclamation-circle"}, {"text": "\uf06b", "id": "fa-gift"}, {"text": "\uf06c", "id": "fa-leaf"}, {"text": "\uf06d", "id": "fa-fire"}, {"text": "\uf06e", "id": "fa-eye"}, {"text": "\uf070", "id": "fa-eye-slash"}, {"text": "\uf071", "id": "fa-exclamation-triangle"}, {"text": "\uf072", "id": "fa-plane"}, {"text": "\uf073", "id": "fa-calendar"}, {"text": "\uf074", "id": "fa-random"}, {"text": "\uf075", "id": "fa-comment"}, {"text": "\uf076", "id": "fa-magnet"}, {"text": "\uf077", "id": "fa-chevron-up"}, {"text": "\uf078", "id": "fa-chevron-down"}, {"text": "\uf079", "id": "fa-retweet"}, {"text": "\uf07a", "id": "fa-shopping-cart"}, {"text": "\uf07b", "id": "fa-folder"}, {"text": "\uf07c", "id": "fa-folder-open"}, {"text": "\uf07d", "id": "fa-arrows-v"}, {"text": "\uf07e", "id": "fa-arrows-h"}, {"text": "\uf080", "id": "fa-bar-chart-o"}, {"text": "\uf081", "id": "fa-twitter-square"}, {"text": "\uf082", "id": "fa-facebook-square"}, {"text": "\uf083", "id": "fa-camera-retro"}, {"text": "\uf084", "id": "fa-key"}, {"text": "\uf085", "id": "fa-cogs"}, {"text": "\uf086", "id": "fa-comments"}, {"text": "\uf087", "id": "fa-thumbs-o-up"}, {"text": "\uf088", "id": "fa-thumbs-o-down"}, {"text": "\uf089", "id": "fa-star-half"}, {"text": "\uf08a", "id": "fa-heart-o"}, {"text": "\uf08b", "id": "fa-sign-out"}, {"text": "\uf08c", "id": "fa-linkedin-square"}, {"text": "\uf08d", "id": "fa-thumb-tack"}, {"text": "\uf08e", "id": "fa-external-link"}, {"text": "\uf090", "id": "fa-sign-in"}, {"text": "\uf091", "id": "fa-trophy"}, {"text": "\uf092", "id": "fa-github-square"}, {"text": "\uf093", "id": "fa-upload"}, {"text": "\uf094", "id": "fa-lemon-o"}, {"text": "\uf095", "id": "fa-phone"}, {"text": "\uf096", "id": "fa-square-o"}, {"text": "\uf097", "id": "fa-bookmark-o"}, {"text": "\uf098", "id": "fa-phone-square"}, {"text": "\uf099", "id": "fa-twitter"}, {"text": "\uf09a", "id": "fa-facebook"}, {"text": "\uf09b", "id": "fa-github"}, {"text": "\uf09c", "id": "fa-unlock"}, {"text": "\uf09d", "id": "fa-credit-card"}, {"text": "\uf09e", "id": "fa-rss"}, {"text": "\uf0a0", "id": "fa-hdd-o"}, {"text": "\uf0a1", "id": "fa-bullhorn"}, {"text": "\uf0f3", "id": "fa-bell"}, {"text": "\uf0a3", "id": "fa-certificate"}, {"text": "\uf0a4", "id": "fa-hand-o-right"}, {"text": "\uf0a5", "id": "fa-hand-o-left"}, {"text": "\uf0a6", "id": "fa-hand-o-up"}, {"text": "\uf0a7", "id": "fa-hand-o-down"}, {"text": "\uf0a8", "id": "fa-arrow-circle-left"}, {"text": "\uf0a9", "id": "fa-arrow-circle-right"}, {"text": "\uf0aa", "id": "fa-arrow-circle-up"}, {"text": "\uf0ab", "id": "fa-arrow-circle-down"}, {"text": "\uf0ac", "id": "fa-globe"}, {"text": "\uf0ad", "id": "fa-wrench"}, {"text": "\uf0ae", "id": "fa-tasks"}, {"text": "\uf0b0", "id": "fa-filter"}, {"text": "\uf0b1", "id": "fa-briefcase"}, {"text": "\uf0b2", "id": "fa-arrows-alt"}, {"text": "\uf0c0", "id": "fa-users"}, {"text": "\uf0c1", "id": "fa-link"}, {"text": "\uf0c2", "id": "fa-cloud"}, {"text": "\uf0c3", "id": "fa-flask"}, {"text": "\uf0c4", "id": "fa-scissors"}, {"text": "\uf0c5", "id": "fa-files-o"}, {"text": "\uf0c6", "id": "fa-paperclip"}, {"text": "\uf0c7", "id": "fa-floppy-o"}, {"text": "\uf0c8", "id": "fa-square"}, {"text": "\uf0c9", "id": "fa-bars"}, {"text": "\uf0ca", "id": "fa-list-ul"}, {"text": "\uf0cb", "id": "fa-list-ol"}, {"text": "\uf0cc", "id": "fa-strikethrough"}, {"text": "\uf0cd", "id": "fa-underline"}, {"text": "\uf0ce", "id": "fa-table"}, {"text": "\uf0d0", "id": "fa-magic"}, {"text": "\uf0d1", "id": "fa-truck"}, {"text": "\uf0d2", "id": "fa-pinterest"}, {"text": "\uf0d3", "id": "fa-pinterest-square"}, {"text": "\uf0d4", "id": "fa-google-plus-square"}, {"text": "\uf0d5", "id": "fa-google-plus"}, {"text": "\uf0d6", "id": "fa-money"}, {"text": "\uf0d7", "id": "fa-caret-down"}, {"text": "\uf0d8", "id": "fa-caret-up"}, {"text": "\uf0d9", "id": "fa-caret-left"}, {"text": "\uf0da", "id": "fa-caret-right"}, {"text": "\uf0db", "id": "fa-columns"}, {"text": "\uf0dc", "id": "fa-sort"}, {"text": "\uf0dd", "id": "fa-sort-asc"}, {"text": "\uf0de", "id": "fa-sort-desc"}, {"text": "\uf0e0", "id": "fa-envelope"}, {"text": "\uf0e1", "id": "fa-linkedin"}, {"text": "\uf0e2", "id": "fa-undo"}, {"text": "\uf0e3", "id": "fa-gavel"}, {"text": "\uf0e4", "id": "fa-tachometer"}, {"text": "\uf0e5", "id": "fa-comment-o"}, {"text": "\uf0e6", "id": "fa-comments-o"}, {"text": "\uf0e7", "id": "fa-bolt"}, {"text": "\uf0e8", "id": "fa-sitemap"}, {"text": "\uf0e9", "id": "fa-umbrella"}, {"text": "\uf0ea", "id": "fa-clipboard"}, {"text": "\uf0eb", "id": "fa-lightbulb-o"}, {"text": "\uf0ec", "id": "fa-exchange"}, {"text": "\uf0ed", "id": "fa-cloud-download"}, {"text": "\uf0ee", "id": "fa-cloud-upload"}, {"text": "\uf0f0", "id": "fa-user-md"}, {"text": "\uf0f1", "id": "fa-stethoscope"}, {"text": "\uf0f2", "id": "fa-suitcase"}, {"text": "\uf0a2", "id": "fa-bell-o"}, {"text": "\uf0f4", "id": "fa-coffee"}, {"text": "\uf0f5", "id": "fa-cutlery"}, {"text": "\uf0f6", "id": "fa-file-text-o"}, {"text": "\uf0f7", "id": "fa-building-o"}, {"text": "\uf0f8", "id": "fa-hospital-o"}, {"text": "\uf0f9", "id": "fa-ambulance"}, {"text": "\uf0fa", "id": "fa-medkit"}, {"text": "\uf0fb", "id": "fa-fighter-jet"}, {"text": "\uf0fc", "id": "fa-beer"}, {"text": "\uf0fd", "id": "fa-h-square"}, {"text": "\uf0fe", "id": "fa-plus-square"}, {"text": "\uf100", "id": "fa-angle-double-left"}, {"text": "\uf101", "id": "fa-angle-double-right"}, {"text": "\uf102", "id": "fa-angle-double-up"}, {"text": "\uf103", "id": "fa-angle-double-down"}, {"text": "\uf104", "id": "fa-angle-left"}, {"text": "\uf105", "id": "fa-angle-right"}, {"text": "\uf106", "id": "fa-angle-up"}, {"text": "\uf107", "id": "fa-angle-down"}, {"text": "\uf108", "id": "fa-desktop"}, {"text": "\uf109", "id": "fa-laptop"}, {"text": "\uf10a", "id": "fa-tablet"}, {"text": "\uf10b", "id": "fa-mobile"}, {"text": "\uf10c", "id": "fa-circle-o"}, {"text": "\uf10d", "id": "fa-quote-left"}, {"text": "\uf10e", "id": "fa-quote-right"}, {"text": "\uf110", "id": "fa-spinner"}, {"text": "\uf111", "id": "fa-circle"}, {"text": "\uf112", "id": "fa-reply"}, {"text": "\uf113", "id": "fa-github-alt"}, {"text": "\uf114", "id": "fa-folder-o"}, {"text": "\uf115", "id": "fa-folder-open-o"}, {"text": "\uf118", "id": "fa-smile-o"}, {"text": "\uf119", "id": "fa-frown-o"}, {"text": "\uf11a", "id": "fa-meh-o"}, {"text": "\uf11b", "id": "fa-gamepad"}, {"text": "\uf11c", "id": "fa-keyboard-o"}, {"text": "\uf11d", "id": "fa-flag-o"}, {"text": "\uf11e", "id": "fa-flag-checkered"}, {"text": "\uf120", "id": "fa-terminal"}, {"text": "\uf121", "id": "fa-code"}, {"text": "\uf122", "id": "fa-reply-all"}, {"text": "\uf122", "id": "fa-mail-reply-all"}, {"text": "\uf123", "id": "fa-star-half-o"}, {"text": "\uf124", "id": "fa-location-arrow"}, {"text": "\uf125", "id": "fa-crop"}, {"text": "\uf126", "id": "fa-code-fork"}, {"text": "\uf127", "id": "fa-chain-broken"}, {"text": "\uf128", "id": "fa-question"}, {"text": "\uf129", "id": "fa-info"}, {"text": "\uf12a", "id": "fa-exclamation"}, {"text": "\uf12b", "id": "fa-superscript"}, {"text": "\uf12c", "id": "fa-subscript"}, {"text": "\uf12d", "id": "fa-eraser"}, {"text": "\uf12e", "id": "fa-puzzle-piece"}, {"text": "\uf130", "id": "fa-microphone"}, {"text": "\uf131", "id": "fa-microphone-slash"}, {"text": "\uf132", "id": "fa-shield"}, {"text": "\uf133", "id": "fa-calendar-o"}, {"text": "\uf134", "id": "fa-fire-extinguisher"}, {"text": "\uf135", "id": "fa-rocket"}, {"text": "\uf136", "id": "fa-maxcdn"}, {"text": "\uf137", "id": "fa-chevron-circle-left"}, {"text": "\uf138", "id": "fa-chevron-circle-right"}, {"text": "\uf139", "id": "fa-chevron-circle-up"}, {"text": "\uf13a", "id": "fa-chevron-circle-down"}, {"text": "\uf13b", "id": "fa-html5"}, {"text": "\uf13c", "id": "fa-css3"}, {"text": "\uf13d", "id": "fa-anchor"}, {"text": "\uf13e", "id": "fa-unlock-alt"}, {"text": "\uf140", "id": "fa-bullseye"}, {"text": "\uf141", "id": "fa-ellipsis-h"}, {"text": "\uf142", "id": "fa-ellipsis-v"}, {"text": "\uf143", "id": "fa-rss-square"}, {"text": "\uf144", "id": "fa-play-circle"}, {"text": "\uf145", "id": "fa-ticket"}, {"text": "\uf146", "id": "fa-minus-square"}, {"text": "\uf147", "id": "fa-minus-square-o"}, {"text": "\uf148", "id": "fa-level-up"}, {"text": "\uf149", "id": "fa-level-down"}, {"text": "\uf14a", "id": "fa-check-square"}, {"text": "\uf14b", "id": "fa-pencil-square"}, {"text": "\uf14c", "id": "fa-external-link-square"}, {"text": "\uf14d", "id": "fa-share-square"}, {"text": "\uf14e", "id": "fa-compass"}, {"text": "\uf150", "id": "fa-caret-square-o-down"}, {"text": "\uf151", "id": "fa-caret-square-o-up"}, {"text": "\uf152", "id": "fa-caret-square-o-right"}, {"text": "\uf153", "id": "fa-eur"}, {"text": "\uf154", "id": "fa-gbp"}, {"text": "\uf155", "id": "fa-usd"}, {"text": "\uf156", "id": "fa-inr"}, {"text": "\uf157", "id": "fa-jpy"}, {"text": "\uf158", "id": "fa-rub"}, {"text": "\uf159", "id": "fa-krw"}, {"text": "\uf15a", "id": "fa-btc"}, {"text": "\uf15b", "id": "fa-file"}, {"text": "\uf15c", "id": "fa-file-text"}, {"text": "\uf15d", "id": "fa-sort-alpha-asc"}, {"text": "\uf15e", "id": "fa-sort-alpha-desc"}, {"text": "\uf160", "id": "fa-sort-amount-asc"}, {"text": "\uf161", "id": "fa-sort-amount-desc"}, {"text": "\uf162", "id": "fa-sort-numeric-asc"}, {"text": "\uf163", "id": "fa-sort-numeric-desc"}, {"text": "\uf164", "id": "fa-thumbs-up"}, {"text": "\uf165", "id": "fa-thumbs-down"}, {"text": "\uf166", "id": "fa-youtube-square"}, {"text": "\uf167", "id": "fa-youtube"}, {"text": "\uf168", "id": "fa-xing"}, {"text": "\uf169", "id": "fa-xing-square"}, {"text": "\uf16a", "id": "fa-youtube-play"}, {"text": "\uf16b", "id": "fa-dropbox"}, {"text": "\uf16c", "id": "fa-stack-overflow"}, {"text": "\uf16d", "id": "fa-instagram"}, {"text": "\uf16e", "id": "fa-flickr"}, {"text": "\uf170", "id": "fa-adn"}, {"text": "\uf171", "id": "fa-bitbucket"}, {"text": "\uf172", "id": "fa-bitbucket-square"}, {"text": "\uf173", "id": "fa-tumblr"}, {"text": "\uf174", "id": "fa-tumblr-square"}, {"text": "\uf175", "id": "fa-long-arrow-down"}, {"text": "\uf176", "id": "fa-long-arrow-up"}, {"text": "\uf177", "id": "fa-long-arrow-left"}, {"text": "\uf178", "id": "fa-long-arrow-right"}, {"text": "\uf179", "id": "fa-apple"}, {"text": "\uf17a", "id": "fa-windows"}, {"text": "\uf17b", "id": "fa-android"}, {"text": "\uf17c", "id": "fa-linux"}, {"text": "\uf17d", "id": "fa-dribbble"}, {"text": "\uf17e", "id": "fa-skype"}, {"text": "\uf180", "id": "fa-foursquare"}, {"text": "\uf181", "id": "fa-trello"}, {"text": "\uf182", "id": "fa-female"}, {"text": "\uf183", "id": "fa-male"}, {"text": "\uf184", "id": "fa-gittip"}, {"text": "\uf185", "id": "fa-sun-o"}, {"text": "\uf186", "id": "fa-moon-o"}, {"text": "\uf187", "id": "fa-archive"}, {"text": "\uf188", "id": "fa-bug"}, {"text": "\uf189", "id": "fa-vk"}, {"text": "\uf18a", "id": "fa-weibo"}, {"text": "\uf18b", "id": "fa-renren"}, {"text": "\uf18c", "id": "fa-pagelines"}, {"text": "\uf18d", "id": "fa-stack-exchange"}, {"text": "\uf18e", "id": "fa-arrow-circle-o-right"}, {"text": "\uf190", "id": "fa-arrow-circle-o-left"}, {"text": "\uf191", "id": "fa-caret-square-o-left"}, {"text": "\uf192", "id": "fa-dot-circle-o"}, {"text": "\uf193", "id": "fa-wheelchair"}, {"text": "\uf194", "id": "fa-vimeo-square"}, {"text": "\uf195", "id": "fa-try"}, {"text": "\uf196", "id": "fa-plus-square-o"}], /** * Initializes select2: in Chrome and Safari,