_.each(this.events, function (method, event) { // If the method is a function, use the default Widget system if (typeof method !== 'string') { events[event] = method; return; } // If the method is only a function name without options, use the // default Widget system var methodOptions = method.split(' '); if (methodOptions.length <= 1) { events[event] = method; return; } // If the method has no meaningful options, use the default Widget // system var isAsync = _.contains(methodOptions, 'async'); if (!isAsync) { events[event] = method; return; } method = self.proxy(methodOptions[methodOptions.length - 1]); if (_.str.startsWith(event, 'click')) { // Protect click handler to be called multiple times by // mistake by the user and add a visual disabling effect // for buttons. method = dom.makeButtonHandler(method); } else { // Protect all handlers to be recalled while the previous // async handler call is not finished. method = dom.makeAsyncHandler(method); } events[event] = method; });
_onScrollTo: function (ev) { var offset = {top: ev.data.top, left: ev.data.left || 0}; if (!offset.top) { offset = dom.getPosition(document.querySelector(ev.data.selector)); // Substract the position of the action_manager as it is the scrolling part offset.top -= dom.getPosition(this.action_manager.el).top; } this.action_manager.el.scrollTop = offset.top; this.action_manager.el.scrollLeft = offset.left; },
focusField: function (id, fieldName, offset) { this.editRecord(id); if (typeof offset === "number") { var field = _.findWhere(this.allFieldWidgets[id], {name: fieldName}); dom.setSelectionRange(field.getFocusableElement().get(0), {start: offset, end: offset}); } },
start: function () { var self = this; this._$attachmentButton = this.$('.o_composer_button_add_attachment'); this._$attachmentsList = this.$('.o_composer_attachments_list'); this.$input = this.$('.o_composer_input textarea'); this.$input.focus(function () { self.trigger('input_focused'); }); this.$input.val(this.options.defaultBody); dom.autoresize(this.$input, { parent: this, min_height: this.options.inputMinHeight }); // Attachments this._renderAttachments(); $(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this)); this.on('change:attachment_ids', this, this._renderAttachments); this.call('mail_service', 'getMailBus') .on('update_typing_partners', this, this._onUpdateTypingPartners); // Mention this._mentionManager.prependTo(this.$('.o_composer')); // Drag-Drop files // Allowing body to detect dragenter and dragleave for display var $body = $('body'); this._dropZoneNS = _.uniqueId('o_dz_'); // For event namespace used when multiple chat window is open $body.on('dragleave.' + this._dropZoneNS, this._onBodyFileDragLeave.bind(this)); $body.on("dragover." + this._dropZoneNS, this._onBodyFileDragover.bind(this)); $body.on("drop." + this._dropZoneNS, this._onBodyFileDrop.bind(this)); return this._super(); },
return $.when(this.inner_widget.appendTo(new_widget_fragment)).done(function() { // Detach the fragment of the previous action and store it within the action if (old_action) { old_action.set_fragment(old_action.detach()); } if (!widget.need_control_panel) { // Hide the main ControlPanel for widgets that do not use it self.main_control_panel.do_hide(); } // most of the time, the self.$el element should already be empty, // because we detached the old action just a few line up. However, // it may happen that it is not empty, for example when a view // manager was unable to load a view because of a crash. In any // case, this is done as a safety measure to avoid the 'double view' // situation that we had when the web client was unable to recover // from a crash. self.$el.empty(); dom.append(self.$el, new_widget_fragment, { in_DOM: self.is_in_DOM, callbacks: [{widget: self.inner_widget}], }); if (actions_to_destroy) { self.clear_action_stack(actions_to_destroy); } self.toggle_fullscreen(); self.trigger_up('current_action_updated', {action: new_action}); }).fail(function () {
return controlPanel.appendTo(fragment).then(function () { dom.prepend($action, fragment, { callbacks: [{ widget: controlPanel }], in_DOM: true, }); return controlPanel; });
_onScrollTo: function (ev) { var offset; if (ev.data.selector) { offset = dom.getPosition(document.querySelector(ev.data.selector)); // substract the position of the ActionManager as it is the // scrolling element var actionManagerOffset = dom.getPosition(this.action_manager.el); offset.left -= actionManagerOffset.left; offset.top -= actionManagerOffset.top; } else { offset = {top: ev.data.top || 0, left: ev.data.left || 0}; } this.action_manager.el.scrollTop = offset.top; this.action_manager.el.scrollLeft = offset.left; },
return actionManager.appendTo(fragment).then(function () { dom.append(widget.$el, fragment, { callbacks: [{ widget: actionManager }], in_DOM: true, }); return actionManager; });
self.opened().then(function () { dom.append(self.$el, fragment, { callbacks: [{widget: self.list_controller}], in_DOM: true, }); self.set_buttons(self.__buttons); });
destroy: function () { this._super.apply(this, arguments); if (!this.noAutohide) { $(window).off('.autohideMenu'); dom.destroyAutoMoreMenu(this.$el); } },
_detachCurrentController: function () { var currentController = this.getCurrentController(); if (currentController) { currentController.scrollPosition = this._getScrollPosition(); dom.detach([{widget: currentController.widget}]); } },
.then(function (messages) { if (self.messages_separator_position === 'top') { self.messages_separator_position = undefined; // reset value to re-compute separator position } self.thread.render(messages, self._getThreadRenderingOptions(messages)); offset += dom.getPosition(document.querySelector(oldestMsgSelector)).top; self.thread.scroll_to({offset: offset}); });
_renderSelector: function (tag, disableInput) { var $content = dom.renderCheckbox(); if (disableInput) { $content.find("input[type='checkbox']").prop('disabled', disableInput); } return $('<' + tag + '>') .addClass('o_list_record_selector') .append($content); },
self.opened().always(function () { if ($buttons.children().length) { self.$footer.empty().append($buttons.contents()); } dom.append(self.$el, fragment, { callbacks: [{widget: self.form_view}], in_DOM: true, }); });
return view.appendTo(fragment).then(function () { dom.append($view_manager, fragment, { callbacks: [{widget: view}], in_DOM: true, }); view.$el.on('click', 'a', function (ev) { ev.preventDefault(); }); }).then(function () {
_renderButtonBox: function (node) { var self = this; var $result = $('<' + node.tag + '>', { 'class': 'o_not_full' }); var buttons = _.map(node.children, function (child) { if (child.tag === 'button') { return self._renderStatButton(child); } else { return self._renderNode(child); } }); var buttons_partition = _.partition(buttons, function ($button) { return $button.is('.o_invisible_modifier'); }); var invisible_buttons = buttons_partition[0]; var visible_buttons = buttons_partition[1]; // Get the unfolded buttons according to window size var nb_buttons = [2, 2, 4, 6][config.device.size_class] || 7; var unfolded_buttons = visible_buttons.slice(0, nb_buttons).concat(invisible_buttons); // Get the folded buttons var folded_buttons = visible_buttons.slice(nb_buttons); if (folded_buttons.length === 1) { unfolded_buttons = buttons; folded_buttons = []; } // Toggle class to tell if the button box is full (CSS requirement) var full = (visible_buttons.length > nb_buttons); $result.toggleClass('o_full', full).toggleClass('o_not_full', !full); // Add the unfolded buttons _.each(unfolded_buttons, function ($button) { $button.appendTo($result); }); // Add the dropdown with folded buttons if any if (folded_buttons.length) { $result.append(dom.renderButton({ attrs: { class: 'oe_stat_button o_button_more dropdown-toggle', 'data-toggle': 'dropdown', }, text: _t("More"), })); var $dropdown = $("<div>", {'class': "dropdown-menu o_dropdown_more", role: "menu"}); _.each(folded_buttons, function ($button) { $button.addClass('dropdown-item').appendTo($dropdown); }); $dropdown.appendTo($result); } this._handleAttributes($result, node); this._registerModifiers(node, this.state, $result); return $result; },
return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true}).then(function () { // restore the cursor position currentRowID = self.state.data[newRowIndex].id; currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex]; focusedElement = currentWidget.getFocusableElement().get(0); if (selectionRange) { dom.setSelectionRange(focusedElement, selectionRange); } });
start: function () { if (!this.isSection) { if (this.mode === 'edit') { dom.autoresize(this.$el, {parent: this}); this.$el = this.$el.add(this._renderTranslateButton()); } } return this._super.apply(this, arguments); },
_attachComponent: function (childInfo, $from) { var self = this; var $elements = dom.cssFind($from || this.$el, childInfo.selector); var defs = _.map($elements, function (element) { var w = new childInfo.Widget(self); self._widgets.push(w); return w.attachTo(element); }); return $.when.apply($, defs); },
_appendController: function (controller) { dom.append(this.$el, controller.widget.$el, { in_DOM: this.isInDOM, callbacks: [{widget: controller.widget}], }); if (controller.scrollPosition) { this.trigger_up('scrollTo', controller.scrollPosition); } },
.then(function () { // restore the selection range currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex]; if (currentWidget) { focusedElement = currentWidget.getFocusableElement().get(0); if (selectionRange) { dom.setSelectionRange(focusedElement, selectionRange); } } });
init: function(parent, options) { this._super.apply(this, arguments); this.options = _.extend(options || {}, { }); // TODO simplify this using the 'async' keyword in the events // property definition as soon as this widget is converted in // frontend widget. this.payEvent = dom.makeButtonHandler(this.payEvent); },
/** * @todo Really? it returns a jQuery element... We should try to avoid this and * let DOM utility functions handle this directly. And replace this with a * function that returns a string so we can get rid of the forceString. * * @param {boolean} value * @param {Object} [field] * a description of the field (note: this parameter is ignored) * @param {Object} [options] additional options * @param {boolean} [options.forceString=false] if true, returns a string * representation of the boolean rather than a jQueryElement * @returns {jQuery|string} */ function formatBoolean(value, field, options) { if (options && options.forceString) { return value ? _t('True') : _t('False'); } return dom.renderCheckbox({ prop: { checked: value, disabled: true, }, }); }
self.dialog.opened().then(function () { dom.append(self.dialog.$el, fragment, { in_DOM: true, callbacks: [{widget: self.dialog_widget}], }); if ($dialogFooter) { self.dialog.$footer.empty().append($dialogFooter.contents()); } if (options.state && self.dialog_widget.do_load_state) { return self.dialog_widget.do_load_state(options.state); } })
return dialog.open().opened(function () { self.currentDialogController = controller; dom.append(dialog.$el, widget.$el, { in_DOM: true, callbacks: [{widget: dialog}, {widget: controller.widget}], }); widget.renderButtons(dialog.$footer); dialog.rebindButtonBehavior(); return action; });
_renderTagButton: function (node) { var $button = dom.renderButton({ attrs: _.omit(node.attrs, 'icon', 'string'), icon: node.attrs.icon, text: (node.attrs.string || '').replace(/_/g, ''), }); $button.append(_.map(node.children, this._renderNode.bind(this))); this._addOnClickAction($button, node); this._handleAttributes($button, node); this._registerModifiers(node, this.state, $button); return $button; },
/** * Create an autoresize text area with 'border-box' as box sizing rule. * The minimum height of this autoresize text are is 1px. * * @param {Object} [options={}] * @param {integer} [options.borderBottomWidth=0] * @param {integer} [options.borderTopWidth=0] * @param {integer} [options.padding=0] */ function prepareAutoresizeTextArea(options) { options = options || {}; var $textarea = $('<textarea>'); $textarea.css('box-sizing', 'border-box'); $textarea.css({ padding: options.padding || 0, borderTopWidth: options.borderTopWidth || 0, borderBottomWidth: options.borderBottomWidth || 0, }); $textarea.appendTo($('#qunit-fixture')); dom.autoresize($textarea, { min_height: 1 }); return $textarea; }
_display_view: function (old_view) { var view_controller = this.active_view.controller; var view_fragment = this.active_view.$fragment; // Prepare the ControlPanel content and update it var view_control_elements = this.render_view_control_elements(); var cp_status = { active_view_selector: '.o_cp_switch_' + this.active_view.type, breadcrumbs: this.action_manager && this.action_manager.get_breadcrumbs(), cp_content: _.extend({}, this.searchview_elements, view_control_elements), hidden: this.flags.headless, searchview: this.searchview, search_view_hidden: !this.active_view.searchable || this.active_view.searchview_hidden, }; this.update_control_panel(cp_status); // Detach the old view and store it if (old_view && old_view !== this.active_view) { // Store the scroll position if (this.action_manager && this.action_manager.webclient) { old_view.controller.setScrollTop(this.action_manager.webclient.getScrollTop()); } // Do not detach ui-autocomplete elements to let jquery-ui garbage-collect them var $to_detach = this.$el.contents().not('.ui-autocomplete'); old_view.$fragment = dom.detach([{widget: old_view.controller}], {$to_detach: $to_detach}); } // If the user switches from a multi-record to a mono-record view, // the action manager should be scrolled to the top. if (old_view && old_view.controller.multi_record === true && view_controller.multi_record === false) { view_controller.setScrollTop(0); } // Append the view fragment to this.$el dom.append(this.$el, view_fragment, { in_DOM: this.is_in_DOM, callbacks: [{widget: view_controller}], }); },
return Promise.all(defs).then(function () { // update registered modifiers to edit 'mode' because the call to // _renderBody set baseModeByRecord as 'readonly' _.each(self.columns, function (node) { self._registerModifiers(node, record, null, {mode: 'edit'}); }); // store the selection range to restore it once the table will // be re-rendered, and the current cell re-selected var currentRowID, currentWidget, focusedElement, selectionRange; if (self.currentRow !== null) { currentRowID = self._getRecordID(self.currentRow); currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex]; if (currentWidget) { focusedElement = currentWidget.getFocusableElement().get(0); if (currentWidget.formatType !== 'boolean') { selectionRange = dom.getSelectionRange(focusedElement); } } } // remove all data rows except the one being edited, and insert // data rows of the re-rendered body before and after it var $editedRow = self._getRow(id); $editedRow.nextAll('.o_data_row').remove(); $editedRow.prevAll('.o_data_row').remove(); var $newRow = $newBody.find('.o_data_row[data-id="' + id + '"]'); $newRow.prevAll('.o_data_row').get().reverse().forEach(function (row) { $(row).insertBefore($editedRow); }); $newRow.nextAll('.o_data_row').get().reverse().forEach(function (row) { $(row).insertAfter($editedRow); }); if (self.currentRow !== null) { var newRowIndex = $editedRow.prop('rowIndex') - 1; self.currentRow = newRowIndex; return self._selectCell(newRowIndex, self.currentFieldIndex, {force: true}) .then(function () { // restore the selection range currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex]; if (currentWidget) { focusedElement = currentWidget.getFocusableElement().get(0); if (selectionRange) { dom.setSelectionRange(focusedElement, selectionRange); } } }); } });
return dialog.open().opened(function () { dom.append(dialog.$el, widget.$el, { in_DOM: true, callbacks: [{widget: dialog}], }); // AAB: renderButtons will be a function of AbstractAction, so this // test won't be necessary anymore if (widget.renderButtons) { widget.renderButtons(dialog.$footer); } self.currentDialogController = controller; return action; });