diff options
Diffstat (limited to 'addons/web/static/src/js/fields/basic_fields.js')
| -rw-r--r-- | addons/web/static/src/js/fields/basic_fields.js | 586 |
1 files changed, 445 insertions, 141 deletions
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index b51fa49ed9a..aac91081c3c 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -114,7 +114,7 @@ var DebouncedField = AbstractField.extend({ */ commitChanges: function () { if (this._isDirty && this.mode === 'edit') { - this._doAction(); + return this._doAction(); } }, @@ -135,7 +135,7 @@ var DebouncedField = AbstractField.extend({ // do anything (commitChanges ensures that if it has local changes, they // are triggered up before the widget is destroyed, if necessary). if (!this.isDestroyed()) { - this._setValue(this._getValue()); + return this._setValue(this._getValue()); } }, /** @@ -206,6 +206,13 @@ var InputField = DebouncedField.extend({ this.isDirty = false; } if (this.isDirty || (event && event.target === this && event.data.changes[this.name] === this.value)) { + if (this.attrs.decorations) { + // if a field is modified, then it could have triggered an onchange + // which changed some of its decorations. Since we bypass the + // render function, we need to apply decorations here to make + // sure they are recomputed. + this._applyDecorations(); + } return $.when(); } else { return this._render(); @@ -236,14 +243,20 @@ var InputField = DebouncedField.extend({ _prepareInput: function ($input) { this.$input = $input || $("<input/>"); this.$input.addClass('o_input'); - this.$input.attr({ - type: this.nodeOptions.isPassword ? 'password' : 'text', - placeholder: this.attrs.placeholder || "", - autocomplete: this.nodeOptions.isPassword ? - 'new-password' : - this.attrs.autocomplete, - }); - this.$input.val(this._formatValue(this.value)); + + var inputAttrs = { placeholder: this.attrs.placeholder || "" }; + var inputVal; + if (this.nodeOptions.isPassword) { + inputAttrs = _.extend(inputAttrs, { type: 'password', autocomplete: 'new-password' }); + inputVal = this.value || ''; + } else { + inputAttrs = _.extend(inputAttrs, { type: 'text', autocomplete: this.attrs.autocomplete }); + inputVal = this._formatValue(this.value); + } + + this.$input.attr(inputAttrs); + this.$input.val(inputVal); + return this.$input; }, /** @@ -321,7 +334,8 @@ var InputField = DebouncedField.extend({ } if (ev.data.direction ==='next' && this.attrs.modifiersValue && - this.attrs.modifiersValue.required) { + this.attrs.modifiersValue.required && + this.viewType !== 'list') { if (!this.$input.val()){ this.setInvalidClass(); ev.stopPropagation(); @@ -333,6 +347,64 @@ var InputField = DebouncedField.extend({ }, }); +var NumericField = InputField.extend({ + tagName: 'span', + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * For numeric fields, 0 is a valid value. + * + * @override + */ + isSet: function () { + return this.value === 0 || this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Format numerical value (integer or float) + * + * Note: We have to overwrite this method to skip the format if we are into + * edit mode on a input type number. + * + * @override + * @private + */ + _formatValue: function (value) { + if (this.mode === 'edit' && this.nodeOptions.type === 'number') { + return value; + } + return this._super.apply(this, arguments); + }, + + /** + * Formats an input element for edit mode. This is in a separate function so + * extending widgets can use it on their input without having input as tagName. + * + * Note: We have to overwrite this method to set the input's type to number if + * option setted into the field. + * + * @override + * @private + */ + _prepareInput: function ($input) { + var result = this._super.apply(this, arguments); + if (this.nodeOptions.type === 'number') { + this.$input.attr({type: 'number'}); + } + if (this.nodeOptions.step) { + this.$input.attr({step: this.nodeOptions.step}); + } + return result; + } +}); + var FieldChar = InputField.extend(TranslatableFieldMixin, { className: 'o_field_char', tagName: 'span', @@ -390,9 +462,10 @@ var LinkButton = AbstractField.extend({ if (this.value) { var className = this.attrs.icon || 'fa-globe'; - this.$el.html("<span />"); + this.$el.html("<span role='img'/>"); this.$el.addClass("fa "+ className); this.$el.attr('title', this.value); + this.$el.attr('aria-label', this.value); } }, @@ -428,6 +501,7 @@ var FieldDate = InputField.extend({ // use the session timezone when formatting dates this.formatOptions.timezone = true; this.datepickerOptions = _.defaults( + {}, this.nodeOptions.datepicker || {}, {defaultDate: this.value} ); @@ -458,6 +532,25 @@ var FieldDate = InputField.extend({ }, //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Asks the datepicker widget to activate the input, instead of doing it + * ourself, such that 'input' events triggered by the lib are correctly + * intercepted, and don't produce unwanted 'field_changed' events. + * + * @override + */ + activate: function () { + if (this.isFocusable() && this.datewidget) { + this.datewidget.focus(); + return true; + } + return false; + }, + + //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -700,8 +793,8 @@ var FieldBoolean = AbstractField.extend({ // The formatValue of boolean fields renders HTML elements similar to // the one rendered by the widget itself. Even though the event might // have been fired on the non-widget version of this field, we can still - // test the presence of its o_checkbox class. - if (activated && options && options.event && $(options.event.target).parents('.o_checkbox').length) { + // test the presence of its custom class. + if (activated && options && options.event && $(options.event.target).closest('.custom-control.custom-checkbox').length) { this._setValue(!this.value); // Toggle the checkbox } return activated; @@ -736,6 +829,15 @@ var FieldBoolean = AbstractField.extend({ } return rendered; }, + /** + * Associates the 'for' attribute of the internal label. + * + * @override + */ + setIDForLabel: function (id) { + this._super.apply(this, arguments); + this.$('.custom-control-label').attr('for', id); + }, //-------------------------------------------------------------------------- // Private @@ -795,25 +897,11 @@ var FieldBoolean = AbstractField.extend({ }, }); -var FieldInteger = InputField.extend({ +var FieldInteger = NumericField.extend({ className: 'o_field_integer o_field_number', - tagName: 'span', supportedFieldTypes: ['integer'], //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * For integer fields, 0 is a valid value. - * - * @override - */ - isSet: function () { - return this.value === 0 || this._super.apply(this, arguments); - }, - - //-------------------------------------------------------------------------- // Private //-------------------------------------------------------------------------- @@ -842,9 +930,8 @@ var FieldInteger = InputField.extend({ }, }); -var FieldFloat = InputField.extend({ +var FieldFloat = NumericField.extend({ className: 'o_field_float o_field_number', - tagName: 'span', supportedFieldTypes: ['float'], /** @@ -859,27 +946,159 @@ var FieldFloat = InputField.extend({ this.nodeOptions.digits = JSON.parse(this.attrs.digits); } }, +}); + +var FieldFloatTime = FieldFloat.extend({ + // this is not strictly necessary, as for this widget to be used, the 'widget' + // attrs must be set to 'float_time', so the formatType is automatically + // 'float_time', but for the sake of clarity, we explicitely define a + // FieldFloatTime widget with formatType = 'float_time'. + formatType: 'float_time', + + init: function () { + this._super.apply(this, arguments); + this.formatType = 'float_time'; + } +}); + +var FieldFloatFactor = FieldFloat.extend({ + supportedFieldTypes: ['float'], + className: 'o_field_float_factor', + formatType: 'float_factor', + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + // default values + if (!this.nodeOptions.factor){ + this.nodeOptions.factor = 1; + } + // use as format and parse options + this.parseOptions = this.nodeOptions; + } +}); + +/** + * The goal of this widget is to replace the input field by a button containing a + * range of possible values (given in the options). Each click allows the user to loop + * in the range. The purpose here is to restrict the field value to a predefined selection. + * Also, the widget support the factor conversion as the *float_factor* widget (Range values + * should be the result of the conversion). + **/ +var FieldFloatToggle = AbstractField.extend({ + supportedFieldTypes: ['float'], + formatType: 'float_factor', + className: 'o_field_float_toggle', + tagName: 'span', + events: { + click: '_onClick' + }, + + /** + * @constructor + */ + init: function () { + this._super.apply(this, arguments); + + this.formatType = 'float_factor'; + + if (this.mode === 'edit') { + this.tagName = 'button'; + } + + // we don't inherit Float Field + if (this.attrs.digits) { + this.nodeOptions.digits = JSON.parse(this.attrs.digits); + } + // default values + if (!this.nodeOptions.factor){ + this.nodeOptions.factor = 1; + } + if (!this.nodeOptions.range){ + this.nodeOptions.range = [0.0, 0.5, 1.0]; + } + + // use as format and parse options + this.parseOptions = this.nodeOptions; + }, //-------------------------------------------------------------------------- - // Public + // Private //-------------------------------------------------------------------------- /** - * For float fields, 0 is a valid value. + * Get the display value but in real type to use it in calculations + * + * @private + * @returns {float} The current formatted value + */ + _getDisplayedValue: function () { + return parseFloat(this._formatValue(this.value)); + }, + /** + * Formats the HTML input tag for edit mode and stores selection status. * * @override + * @private */ - isSet: function () { - return this.value === 0 || this._super.apply(this, arguments); + _renderEdit: function () { + // Keep a reference to the input so $el can become something else + // without losing track of the actual input. + this.$el.text(this._formatValue(this.value)); + }, + /** + * Resets the content to the formated value in readonly mode. + * + * @override + * @private + */ + _renderReadonly: function () { + this.$el.text(this._formatValue(this.value)); + }, + /** + * Get the next value in the range, from the current one. If the current + * one is not in the range, the next value of the closest one will be chosen. + * + * @private + * @returns {number} The next formatted value in the range + */ + _nextValue: function () { + var range = this.nodeOptions.range; + var val = utils.closestNumber(this._getDisplayedValue(), range); + var index = _.indexOf(range, val); + if (index !== -1) { + if (index + 1 < range.length) { + return range[index + 1]; + } + } + return range[0]; }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Clicking on the button triggers the change of value; the next one of + * the range will be displayed. + * + * @private + * @param {OdooEvent} ev + */ + _onClick: function(ev) { + if (this.mode === 'edit') { + ev.stopPropagation(); // only stop propagation in edit mode + var next_val = this._nextValue(); + this._setValue(next_val.toString()); // will be parsed in _setValue + } + }, + }); -var FieldFloatTime = FieldFloat.extend({ - // this is not strictly necessary, as for this widget to be used, the 'widget' - // attrs must be set to 'float_time', so the formatType is automatically - // 'float_time', but for the sake of clarity, we explicitely define a - // FieldFloatTime widget with formatType = 'float_time'. - formatType: 'float_time', +var FieldPercentage = FieldFloat.extend({ + formatType:'percentage', }); var FieldText = InputField.extend(TranslatableFieldMixin, { @@ -910,7 +1129,19 @@ var FieldText = InputField.extend(TranslatableFieldMixin, { } return this._super(); }, - + /** + * Override to force a resize of the textarea when its value has changed + * + * @override + */ + reset: function () { + var self = this; + return $.when(this._super.apply(this, arguments)).then(function () { + if (self.mode === 'edit') { + self.$input.trigger('change'); + } + }); + }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -1093,13 +1324,88 @@ var UrlWidget = InputField.extend({ }, }); +var CopyClipboard = { + + /** + * @override + */ + destroy: function () { + this._super.apply(this, arguments); + this.clipboard.destroy(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Instatiates the Clipboad lib. + */ + _initClipboard: function () { + var self = this; + var $clipboardBtn = this.$('.o_clipboard_button'); + $clipboardBtn.tooltip({title: _t('Copied !'), trigger: 'manual', placement: 'right'}); + this.clipboard = new ClipboardJS($clipboardBtn[0], { + text: function (_) { + return self.value.trim(); + }, + // Container added because of Bootstrap modal that give the focus to another element. + // We need to give to correct focus to ClipboardJS (see in ClipboardJS doc) + // https://github.com/zenorocha/clipboard.js/issues/155 + container: self.$el[0] + }); + this.clipboard.on('success', function () { + _.defer(function () { + $clipboardBtn.tooltip('show'); + _.delay(function () { + $clipboardBtn.tooltip('hide'); + }, 800); + }); + }); + }, +}; + +var TextCopyClipboard = FieldText.extend(CopyClipboard, { + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _render: function() { + this._super.apply(this, arguments); + this.$el.addClass('o_field_copy'); + this.$el.append($(qweb.render('CopyClipboardText'))); + this._initClipboard(); + } +}); + +var CharCopyClipboard = FieldChar.extend(CopyClipboard, { + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _render: function() { + this._super.apply(this, arguments); + this.$el.addClass('o_field_copy'); + this.$el.append($(qweb.render('CopyClipboardChar'))); + this._initClipboard(); + } +}); + var AbstractFieldBinary = AbstractField.extend({ events: _.extend({}, AbstractField.prototype.events, { 'change .o_input_file': 'on_file_change', 'click .o_select_file_button': function () { this.$('.o_input_file').click(); }, - 'click .o_clear_file_button': 'on_clear', + 'click .o_clear_file_button': '_onClearClick', }), init: function (parent, name, record) { this._super.apply(this, arguments); @@ -1185,11 +1491,33 @@ var AbstractFieldBinary = AbstractField.extend({ }); } }, - on_clear: function () { + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + /** + * Clear the file input + * + * @private + */ + _clearFile: function (){ this.set_filename(''); this._setValue(false); this._render(); }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** + * On "clear file" button click + * + * @param {MouseEvent} ev + * @private + */ + _onClearClick: function (ev) { + this._clearFile(); + }, }); var FieldBinaryImage = AbstractFieldBinary.extend({ @@ -1207,12 +1535,19 @@ var FieldBinaryImage = AbstractFieldBinary.extend({ }, }), supportedFieldTypes: ['binary'], + file_type_magic_word: { + '/': 'jpg', + 'R': 'gif', + 'i': 'png', + 'P': 'svg+xml', + }, _render: function () { var self = this; var url = this.placeholder; if (this.value) { if (!utils.is_bin_size(this.value)) { - url = 'data:image/png;base64,' + this.value; + // Use magic-word technique for detecting image type + url = 'data:image/' + (this.file_type_magic_word[this.value[0]] || 'png') + ';base64,' + this.value; } else { url = session.url('/web/image', { model: this.model, @@ -1239,7 +1574,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({ this.$('> img').remove(); this.$el.prepend($img); $img.on('error', function () { - self.on_clear(); + self._clearFile(); $img.attr('src', self.placeholder); self.do_warn(_t("Image"), _t("Could not display the selected image.")); }); @@ -1348,9 +1683,6 @@ var FieldPdfViewer = FieldBinaryFile.extend({ * @param {DOMElement} iframe */ _disableButtons: function (iframe) { - if (this.mode === 'readonly') { - $(iframe).contents().find('button#download').hide(); - } $(iframe).contents().find('button#openFile').hide(); }, /** @@ -1499,6 +1831,7 @@ var PriorityWidget = AbstractField.extend({ _renderStar: function (tag, isFull, index, tip) { return $(tag) .attr('title', tip) + .attr('aria-label', tip) .attr('data-index', index) .addClass('o_priority_star fa') .toggleClass('fa-star', isFull) @@ -1568,7 +1901,8 @@ var AttachmentImage = AbstractField.extend({ if (this.value) { this.$el.empty().append($('<img>/', { src: "/web/image/" + this.value.data.id + "?unique=1", - title: this.value.data.display_name + title: this.value.data.display_name, + alt: _t("Image") })); } } @@ -1577,10 +1911,7 @@ var AttachmentImage = AbstractField.extend({ var StateSelectionWidget = AbstractField.extend({ template: 'FormSelection', events: { - 'click a': function (e) { - e.preventDefault(); - }, - 'click li': '_setSelection' + 'click .dropdown-item': '_setSelection', }, supportedFieldTypes: ['selection'], @@ -1637,7 +1968,9 @@ var StateSelectionWidget = AbstractField.extend({ this.$('.o_status') .removeClass('o_status_red o_status_green') .addClass(currentState.state_class) - .prop('special_click', true); + .prop('special_click', true) + .parent().attr('title', currentState.state_name) + .attr('aria-label', currentState.state_name); // Render "FormSelection.Items" and move it into "FormSelection" var $items = $(qweb.render('FormSelection.items', { @@ -1659,13 +1992,12 @@ var StateSelectionWidget = AbstractField.extend({ * @param {MouseEvent} ev */ _setSelection: function (ev) { - var li = $(ev.target).closest('li'); - if (li.length) { - var value = String(li.data('value')); - this._setValue(value); - if (this.mode === 'edit') { - this._render(); - } + ev.preventDefault(); + var $item = $(ev.currentTarget); + var value = String($item.data('value')); + this._setValue(value); + if (this.mode === 'edit') { + this._render(); } }, }); @@ -1701,8 +2033,9 @@ var FavoriteWidget = AbstractField.extend({ * @private */ _render: function () { - var template = this.attrs.nolabel ? '<a href="#"><i class="fa %s" title="%s"></i></a>' : '<a href="#"><i class="fa %s"></i> %s</a>'; - this.$el.empty().append(_.str.sprintf(template, this.value ? 'fa-star' : 'fa-star-o', this.value ? _t('Remove from Favorites') : _t('Add to Favorites'))); + var tip = this.value ? _t('Remove from Favorites') : _t('Add to Favorites'); + var template = this.attrs.nolabel ? '<a href="#"><i class="fa %s" title="%s" aria-label="%s" role="img"></i></a>' : '<a href="#"><i class="fa %s" role="img" aria-label="%s"></i> %s</a>'; + this.$el.empty().append(_.str.sprintf(template, this.value ? 'fa-star' : 'fa-star-o', tip, tip)); }, //-------------------------------------------------------------------------- @@ -1740,7 +2073,7 @@ var LabelSelection = AbstractField.extend({ _render: function () { this.classes = this.nodeOptions && this.nodeOptions.classes || {}; var labelClass = this.classes[this.value] || 'primary'; - this.$el.addClass('label label-' + labelClass).text(this._formatValue(this.value)); + this.$el.addClass('badge badge-' + labelClass).text(this._formatValue(this.value)); }, }); @@ -1807,32 +2140,10 @@ var FieldBooleanButton = AbstractField.extend({ }); var BooleanToggle = FieldBoolean.extend({ + className: FieldBoolean.prototype.className + ' o_boolean_toggle', events: { 'click': '_onClick' }, - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - */ - _render: function () { - this._super.apply(this, arguments); - this._renderToggleSwitch(); - }, - - /** - * Display toggle switch - * - * @private - */ - _renderToggleSwitch: function () { - this.$el.addClass("o_boolean_toggle"); - var $div = $('<div class="slider"></div>'); - $div.insertAfter(this.$("input[type=checkbox]")); - }, //-------------------------------------------------------------------------- // Handlers @@ -1849,7 +2160,6 @@ var BooleanToggle = FieldBoolean.extend({ this._setValue(!this.value); this.$el.closest(".o_data_row").toggleClass('text-muted', this.value); }, - }); var StatInfo = AbstractField.extend({ @@ -2082,7 +2392,10 @@ var FieldProgressBar = AbstractField.extend({ widthComplete = 100; } - this.$('.o_progress').toggleClass('o_progress_overflow', value > max_value); + this.$('.o_progress').toggleClass('o_progress_overflow', value > max_value) + .attr('aria-valuemin', '0') + .attr('aria-valuemax', max_value) + .attr('aria-valuenow', value); this.$('.o_progressbar_complete').css('width', widthComplete + '%'); if (!this.write_mode) { @@ -2151,6 +2464,7 @@ var FieldToggleBoolean = AbstractField.extend({ .toggleClass('text-muted', !this.value); var title = this.value ? this.attrs.options.active : this.attrs.options.inactive; this.$el.attr('title', title); + this.$el.attr('aria-pressed', this.value); }, //-------------------------------------------------------------------------- @@ -2198,6 +2512,25 @@ var JournalDashboardGraph = AbstractField.extend({ } this._super.apply(this, arguments); }, + /** + * The widget view uses the nv(d3) lib to render the graph. This lib + * requires that the rendering is done directly into the DOM (so that it can + * correctly compute positions). However, the views are always rendered in + * fragments, and appended to the DOM once ready (to prevent them from + * flickering). We here use the on_attach_callback hook, called when the + * widget is attached to the DOM, to perform the rendering. This ensures + * that the rendering is always done in the DOM. + */ + on_attach_callback: function () { + this._isInDOM = true; + this._renderInDOM(); + }, + /** + * Called when the field is detached from the DOM. + */ + on_detach_callback: function () { + this._isInDOM = false; + }, //-------------------------------------------------------------------------- // Private @@ -2218,9 +2551,25 @@ var JournalDashboardGraph = AbstractField.extend({ } }, /** + * Render the widget only when it is in the DOM. This is because nvd3 + * renders graph only when it is in DOM, apparently to compute available + * height and width for instance. + * + * @override * @private */ _render: function () { + if (this._isInDOM) { + return this._renderInDOM(); + } + return $.when(); + }, + /** + * Render the widget. This function assumes that it is attached to the DOM. + * + * @private + */ + _renderInDOM: function () { // note: the rendering of this widget is aynchronous as nvd3 does a // setTimeout(0) before executing the callback given to addGraph var self = this; @@ -2644,56 +2993,6 @@ var AceEditor = DebouncedField.extend({ }, }); -var ImageSelection = AbstractField.extend({ - supportedFieldTypes: ['selection'], - events: _.extend({}, AbstractField.prototype.events, { - 'click img': '_onImgClicked', - }), - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - */ - _render: function () { - var self = this; - this.$el.empty(); - _.each(this.nodeOptions, function (val, key) { - var $container = $('<div>').addClass('col-xs-3 text-center'); - var $img = $('<img>') - .addClass('img img-responsive img-thumbnail ml16') - .toggleClass('btn-info', key === self.value) - .attr('src', val.image_link) - .data('key', key); - $container.append($img); - if (val.preview_link) { - var $previewLink = $('<a>') - .text('Preview') - .attr('href', val.preview_link) - .attr('target', '_blank'); - $container.append($previewLink); - } - self.$el.append($container); - }); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * @override - * @private - * @param {MouseEvent} event - */ - _onImgClicked: function (event) { - this._setValue($(event.currentTarget).data('key')); - }, -}); - return { TranslatableFieldMixin: TranslatableFieldMixin, DebouncedField: DebouncedField, @@ -2711,6 +3010,9 @@ return { FieldDomain: FieldDomain, FieldFloat: FieldFloat, FieldFloatTime: FieldFloatTime, + FieldFloatFactor: FieldFloatFactor, + FieldFloatToggle: FieldFloatToggle, + FieldPercentage : FieldPercentage, FieldInteger: FieldInteger, FieldMonetary: FieldMonetary, FieldPercentPie: FieldPercentPie, @@ -2720,14 +3022,16 @@ return { FieldToggleBoolean: FieldToggleBoolean, HandleWidget: HandleWidget, InputField: InputField, + NumericField: NumericField, AttachmentImage: AttachmentImage, - ImageSelection: ImageSelection, LabelSelection: LabelSelection, StateSelectionWidget: StateSelectionWidget, FavoriteWidget: FavoriteWidget, PriorityWidget: PriorityWidget, StatInfo: StatInfo, UrlWidget: UrlWidget, + TextCopyClipboard: TextCopyClipboard, + CharCopyClipboard: CharCopyClipboard, JournalDashboardGraph: JournalDashboardGraph, AceEditor: AceEditor, }; |
