/** * EGroupware eTemplate2 - JS Selectbox object * * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package etemplate * @subpackage api * @link https://www.egroupware.org * @author Nathan Gray * @author Andreas Stöckel * @copyright Nathan Gray 2011 */ /*egw:uses /vendor/bower-asset/jquery/dist/jquery.js; /api/js/jquery/chosen/chosen.jquery.js; et2_core_xml; et2_core_DOMWidget; et2_core_inputWidget; */ import "../../../vendor/bower-asset/jquery/dist/jquery.min.js"; import "../jquery/chosen/chosen.jquery.js"; import { et2_no_init } from "./et2_core_common"; import { ClassWithAttributes } from "./et2_core_inheritance"; import { et2_register_widget } from "./et2_core_widget"; import { et2_inputWidget } from './et2_core_inputWidget'; import { et2_DOMWidget } from "./et2_core_DOMWidget"; import { et2_directChildrenByTagName, et2_readAttrWithDefault } from "./et2_core_xml"; import { egw } from "../jsapi/egw_global"; import { sprintf } from "../egw_action/egw_action_common.js"; // all calls to Chosen jQuery plugin as jQuery.(un)chosen() give errors which are currently suppressed with @ts-ignore // adding npm package @types/chosen-js did NOT help :( /** * et2 select(box) widget */ export class et2_selectbox extends et2_inputWidget { /** * Constructor */ constructor(_parent, _attrs, _child) { // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_selectbox._attributes, _child || {})); this.input = null; this.value = ''; this.selected_first = true; /** * Regular expression, to check string-value contains multiple comma-separated values */ this._is_multiple_regexp = /^[,0-9A-Za-z/_ -]+$/; /** * Regular expression and replace value for escaping values in jQuery selectors used to find options */ this._escape_value_replace = /\\/g; this._escape_value_with = '\\\\'; this.input = null; // Start at '' to avoid infinite loops while setting value/select options this.value = ''; // Allow no other widgets inside this one this.supportedWidgetClasses = []; // Legacy options could have row count or empty label in first slot if (typeof this.options.rows == "string") { if (isNaN(this.options.rows)) { this.options.empty_label = this.egw().lang(this.options.rows); this.options.rows = 1; } else { this.options.rows = parseInt(this.options.rows); } } if (this.options.rows > 1) { this.options.multiple = true; if (this.options.tags) { this.createInputWidget(); } else { this.createMultiSelect(); } } else { this.createInputWidget(); } if (!this.options.empty_label && !this.options.readonly && this.options.multiple) { this.options.empty_label = this.egw().lang('Select some options'); } } destroy() { if (this.input != null) { // @ts-ignore this.input.unchosen(); } if (this.expand_button) { this.expand_button.off(); this.expand_button.remove(); this.expand_button = null; } super.destroy(); this.input = null; } transformAttributes(_attrs) { super.transformAttributes(_attrs); // If select_options are already known, skip the rest if (this.options && this.options.select_options && !jQuery.isEmptyObject(this.options.select_options) || _attrs.select_options && !jQuery.isEmptyObject(_attrs.select_options) || // Allow children to skip select_options - check to make sure default got set to something (should be {}) typeof _attrs.select_options == 'undefined' || _attrs.select_options === null) { // do not return inside nextmatch, as get_rows data might have changed select_options // for performance reasons we only do it for first row, which should have id "0[...]" if (this.getParent() && this.getParent().getType() != 'rowWidget' || !_attrs.id || _attrs.id[0] != '0') return; } var sel_options = et2_selectbox.find_select_options(this, _attrs['select_options'], _attrs); if (!jQuery.isEmptyObject(sel_options)) { _attrs['select_options'] = sel_options; } } /** * Switch instanciated widget to multi-selection and back, optionally enabeling tags too * * If you want to switch tags on too, you need to do so after switching to multiple! * * @param {boolean} _multiple * @param {integer} _size default=3 */ set_multiple(_multiple, _size) { this.options.multiple = _multiple; if (this.input) { if (_multiple) { this.input.attr('size', _size || 3); this.input.prop('multiple', true); this.input.attr('name', this.id + '[]'); if (this.input[0].options.length && this.input[0].options[0].value === '') { this.input[0].options[0] = null; } } else { this.input.prop('multiple', false); this.input.removeAttr('size'); this.input.attr('name', this.id); if (this.options.empty_label && this.input[0].options[0].value !== '') { this._appendOptionElement('', this.options.empty_label); } } if (this.expand_button) { if (_multiple) { this.expand_button.addClass('ui-icon-minus').removeClass('ui-icon-plus'); } else { this.expand_button.removeClass('ui-icon-minus').addClass('ui-icon-plus'); } } } } change(_node, _widget, _value) { var valid = super.change.apply(this, arguments); if (!this.input) return valid; var selected = this.input.siblings().find('a.chzn-single'); var val = _value && _value.selected ? _value.selected : this.input.val(); switch (this.getType()) { case 'select-country': if (selected && selected.length == 1 && val) { selected.removeClass(function (index, className) { return (className.match(/(^|\s)flag-\S+/g) || []).join(' '); }); selected.find('span.img').remove(); selected.prepend('<span class="img"></span>'); selected.addClass('et2_country-select flag-' + val.toLowerCase()); } else if (selected) { selected.removeClass('et2_country-select'); } break; } return valid; } /** * Overridden from parent to make sure tooltip handler is bound to the correct element * if tags is on. */ getTooltipElement() { if (this.input && (this.options.tags || this.options.search)) { return jQuery(this.input.siblings()).get(0); } return this.getDOMNode(this); } /** * Add an option to regular drop-down select * * @param {string} _value value attribute of option * @param {string} _label label of option * @param {string} _title title attribute of option * @param {node} dom_element parent of new option * @param {string} _class specify classes of option */ _appendOptionElement(_value, _label, _title, dom_element, _class) { if (_value == "" && (_label == null || _label == "")) { return; // empty_label is added in set_select_options anyway, ignoring it here to not add it twice } if (this.input == null) { return this._appendMultiOption(_value, _label, _title, dom_element); } var option = jQuery(document.createElement("option")) .attr("value", _value) .text(_label + ""); option.addClass(_class); if (this.options.tags) { switch (this.getType()) { case 'select-cat': option.addClass('cat_' + _value); break; case 'select-country': // jQuery(document.createElement("span")).addClass('et2_country-select').appenTo(option); option.addClass('et2_country-select flag-' + _value.toLowerCase()); break; } if (this.options.value_class != '') option.addClass(this.options.value_class + _value); } if (typeof _title != "undefined" && _title) { option.attr("title", _title); } if (_label == this.options.empty_label || this.options.empty_label == "" && _value === "") { // Make sure empty / all option is first option.prependTo(this.input); } else { option.appendTo(dom_element || this.input); } } /** * Append a value to multi-select * * @param {string} _value value attribute of option * @param {string} _label label of option * @param {string} _title title attribute of option * @param {node} dom_element parent of new option */ _appendMultiOption(_value, _label, _title, dom_element) { var option_data = null; if (typeof _label == "object") { option_data = _label; _label = option_data.label; } // Already in header if (_label == this.options.empty_label) return; var opt_id = this.dom_id + "_opt_" + _value; var label = jQuery(document.createElement("label")) .attr("for", opt_id) .hover(function () { jQuery(this).addClass("ui-state-hover"); }, function () { jQuery(this).removeClass("ui-state-hover"); }); var option = jQuery(document.createElement("input")) .attr("type", "checkbox") .attr("id", opt_id) .attr("value", _value) .appendTo(label); if (typeof _title !== "undefined") { option.attr("title", _title); } // Some special stuff for categories if (option_data) { if (option_data.icon) { var img = this.egw().image(option_data.icon); jQuery(document.createElement(img ? "img" : "div")) .attr("src", img) .addClass('cat_icon cat_' + _value) .appendTo(label); } if (option_data.color) { label.css("background-color", option_data.color) .addClass('cat_' + _value); } } //added tooltip to multiselect if (typeof _title == "undefined") { _title = _label; } label.append(jQuery("<span title='" + _title + "'>" + _label + "</span>")); var li = jQuery(document.createElement("li")).append(label); if (this.options.value_class != '') li.addClass(this.options.value_class + _value); li.appendTo(dom_element || this.multiOptions); } /** * Create a regular drop-down select box */ createInputWidget() { // Create the base input widget this.input = jQuery(document.createElement("select")) .addClass("et2_selectbox") .attr("size", this.options.rows); this.setDOMNode(this.input[0]); // Add the empty label if (this.options.empty_label) { this._appendOptionElement("", this.options.empty_label); } // Set multiple if (this.options.multiple) { this.input.attr("multiple", "multiple"); } } /** * Create a list of checkboxes */ createMultiSelect() { var node = jQuery(document.createElement("div")) .addClass("et2_selectbox"); var header = jQuery(document.createElement("div")) .addClass("ui-widget-header ui-helper-clearfix") .appendTo(node); var controls = jQuery(document.createElement("ul")) .addClass('ui-helper-reset') .appendTo(header); jQuery(document.createElement("span")) .text(this.options.empty_label) .addClass("ui-multiselect-header") .appendTo(header); // Set up for options to be added later var options = this.multiOptions = jQuery(document.createElement("ul")); this.multiOptions.addClass("ui-multiselect-checkboxes ui-helper-reset") .css("height", 1.9 * this.options.rows + "em") .appendTo(node); if (this.options.rows >= 5) { // Check / uncheck all var header_controls = { check: { icon_class: 'ui-icon-check', label: this.egw().lang('Check all'), click(e) { var all_off = false; jQuery("input[type='checkbox']", e.data).each(function () { if (!jQuery(this).prop("checked")) all_off = true; }); jQuery("input[type='checkbox']", e.data).prop("checked", all_off); } } }; for (var key in header_controls) { jQuery(document.createElement("li")) .addClass("et2_clickable") .click(options, header_controls[key].click) .attr("title", header_controls[key].label) .append('<span class="ui-icon ' + header_controls[key].icon_class + '"/>') .appendTo(controls); } } this.setDOMNode(node[0]); } doLoadingFinished() { super.doLoadingFinished(); this.set_tags(this.options.tags, this.options.width); // Reset dirty again here. super.doLoadingFinished() does it too, but set_tags() & others // change things. Moving set_tags() before super.doLoadingFinished() breaks tag widgets this.resetDirty(); return true; } loadFromXML(_node) { // Handle special case where legacy option for empty label is used (conflicts with rows), and rows is set as an attribute var legacy = _node.getAttribute("options"); if (legacy) { var legacy = legacy.split(","); if (legacy.length && isNaN(legacy[0])) { this.options.empty_label = legacy[0]; } } // Read the option-tags var options = et2_directChildrenByTagName(_node, "option"); if (options.length) { // Break reference to content manager, we don't want to add to it this.options.select_options = jQuery.extend([], this.options.select_options); } var egw = this.egw(); for (var i = 0; i < options.length; i++) { this.options.select_options.push({ value: et2_readAttrWithDefault(options[i], "value", options[i].textContent), // allow options to contain multiple translated sub-strings eg: {Firstname}.{Lastname} "label": options[i].textContent.replace(/{([^}]+)}/g, function (str, p1) { return egw.lang(p1); }), "title": et2_readAttrWithDefault(options[i], "title", "") }); } this.set_select_options(this.options.select_options); } /** * Find an option by it's value * * Taking care of escaping values correctly eg. EGroupware\Api\Mail\Smtp using above regular expression * * @param {string} _value * @return {array} */ find_option(_value) { return jQuery("option[value='" + (typeof _value === 'string' ? _value.replace(this._escape_value_replace, this._escape_value_with) : _value) + "']", this.input); } /** * Set value * * @param {string|number} _value * @param {boolean} _dont_try_set_options true: if _value is not in options, use "" instead of calling set_select_options * (which would go into an infinit loop) */ // @ts-ignore for 2nd parameter set_value(_value, _dont_try_set_options) { if (typeof _value == "number") _value = "" + _value; // convert to string for consitent matching if (typeof _value == "string" && (this.options.multiple || this.options.expand_multiple_rows) && _value.match(this._is_multiple_regexp) !== null) { _value = _value.split(','); } if (this.input !== null && this.options.select_options && (!jQuery.isEmptyObject(this.options.select_options) || this.options.select_options.length > 0) && this.input.children().length == 0) { // No options set yet this.set_select_options(this.options.select_options); } // select-cat set/unset right cat_ color for selected value if ((this.getType() == 'select-cat' || this.options.value_class) && this.options.tags) { var chosen = this.input.next(); var prefix_c = this.options.value_class ? this.options.value_class : 'cat_'; this.input.removeClass(prefix_c + this._oldValue); this.input.addClass(prefix_c + this.value); if (chosen.length > 0) { chosen.removeClass(prefix_c + this._oldValue); chosen.addClass(prefix_c + this.value); } } if (this.getType() == 'select-country' && this.options.tags) { var selected = this.input.siblings().find('a.chzn-single'); if (selected && selected.length == 1 && _value) { selected.removeClass(function (index, className) { return (className.match(/(^|\s)flag-\S+/g) || []).join(' '); }); selected.find('span.img').remove(); selected.prepend('<span class="img"></span>'); selected.addClass('et2_country-select flag-' + _value.toLowerCase()); } } if (this.getType() == "select-bitwise" && _value && !isNaN(_value) && this.options.select_options) { var new_value = []; for (var index in this.options.select_options) { var right = this.options.select_options[index].value; if (!!(_value & right)) { new_value.push(right); } } _value = new_value; } this._oldValue = this.value; if (this.input !== null && (this.options.tags || this.options.search)) { // Value must be a real Array, not an object this.input.val(typeof _value == 'object' && _value != null ? jQuery.map(_value, function (value, index) { return [value]; }) : _value); this.input.trigger("liszt:updated"); var self = this; if (this.getType() == 'listbox' && this.options.value_class != '') { var chosen = this.input.next(); chosen.find('.search-choice-close').each(function (i, v) { // @ts-ignore jQuery(v).parent().addClass(self.options.value_class + self.options.select_options[v.rel]['value']); }); } this.value = _value; return; } if (this.input == null) { return this.set_multi_value(_value); } // Auto-expand multiple if not yet turned on, and value has multiple if (this.options.expand_multiple_rows && !this.options.multiple && jQuery.isArray(_value) && _value.length > 1) { this.set_multiple(true, this.options.expand_multiple_rows); } jQuery("option", this.input).prop("selected", false); if (typeof _value == "object") { for (var i in _value) { this.find_option(_value[i]).prop("selected", true); } } else { if (_value && this.find_option(_value).prop("selected", true).length == 0) { if (this.options.select_options[_value] || this.options.select_options.filter && this.options.select_options.filter(function (value) { return value == _value; }) && !_dont_try_set_options) { // Options not set yet? Do that now, which will try again. return this.set_select_options(this.options.select_options); } else if (_dont_try_set_options) { this.value = ""; } else if (jQuery.isEmptyObject(this.options.select_options)) { this.egw().debug("warn", "Can't set value to '%s', widget has no options set", _value, this); this.value = null; } else { var debug_value = _value; if (debug_value === null) debug_value == 'NULL'; this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this); } return; } } this.value = _value; if (this.isAttached() && this._oldValue !== et2_no_init && this._oldValue !== _value) { this.input.change(); } } /** * Find an option by it's value * * Taking care of escaping values correctly eg. EGroupware\Api\Mail\Smtp * * @param {string} _value * @return {array} */ find_multi_option(_value) { return jQuery("input[value='" + (typeof _value === 'string' ? _value.replace(this._escape_value_replace, this._escape_value_with) : _value) + "']", this.multiOptions); } set_multi_value(_value) { jQuery("input", this.multiOptions).prop("checked", false); if (typeof _value == "object") { for (var i in _value) { this.find_multi_option(_value[i]).prop("checked", true); } } else { if (this.find_multi_option(_value).prop("checked", true).length == 0) { var debug_value = _value; if (debug_value === null) debug_value == 'NULL'; this.egw().debug("warn", "Tried to set value '%s' that isn't an option", debug_value, this); } } // Sort selected to the top if (this.selected_first) { this.multiOptions.find("li:has(input:checked)").prependTo(this.multiOptions); } this.value = _value; } /** * Method to check all options of a multi-select, if not all are selected, or none if all where selected * * @todo: add an attribute to automatic add a button calling this method */ select_all_toggle() { var all = jQuery("input", this.multiOptions); all.prop("checked", jQuery("input:checked", this.multiOptions).length == all.length ? false : true); } /** * Add a button to toggle between single select and multi select. * * @param {number} _rows How many rows for multi-select */ set_expand_multiple_rows(_rows) { this.options.expand_multiple_rows = _rows; var surroundings = this.getSurroundings(); if (_rows <= 1 && this.expand_button) { // Remove surroundings.removeDOMNode(this.expand_button.get(0)); } else { if (!this.expand_button) { var button_id = this.getInstanceManager().uniqueId + '_' + this.id.replace(/\./g, '-') + "_expand"; this.expand_button = jQuery("<button class='et2_button et2_button_icon et2_selectbox_expand ui-icon' id='" + button_id + "'/>") .addClass(this.options.multiple ? 'ui-icon-minus' : 'ui-icon-plus') .on("click", jQuery.proxy(function (e) { if (typeof this.input.attr('size') !== 'undefined' && this.input.attr('size') != 1) { this.set_multiple(false, 1); this.expand_button.removeClass('ui-icon-minus').addClass('ui-icon-plus'); } else { this.set_multiple(true, this.options.expand_multiple_rows); this.expand_button.addClass('ui-icon-minus').removeClass('ui-icon-plus'); } }, this)); } surroundings.appendDOMNode(this.expand_button.get(0)); } surroundings.update(); } /** * Turn tag style on and off * * If you want to switch multiple on too, you need to do so before switching tags on! * * @param {boolean} _tags * @param {string} _width width to use, default width of selectbox */ set_tags(_tags, _width) { this.options.tags = _tags; // Can't actually do chosen until attached, loadingFinished should call again if (!this.isAttached()) return; if (this.input != null && !this.options.tags && !this.options.search) { // @ts-ignore this.input.unchosen().css('width', ''); return; } // Turn on tags, if desired if (this.input != null && (this.options.search || this.options.tags) && !this.options.disabled) { if (this.options.empty_label) { this.input.attr("data-placeholder", this.options.empty_label); // Remove from list of options, if multiple if (this.options.multiple) { this.input.children('[value=""]').remove(); } } // Don't do it again if already done if (this.input.hasClass("chzn-done")) return; // Properly size chosen, even if on a hidden tab var size = egw.getHiddenDimensions(this.input); if (this.getType() == 'listbox' && this.options.value_class != '') { var self = this; this.input.find('option').each(function (i, v) { // @ts-ignore jQuery(v).addClass(self.options.value_class + v.value); }); } this.input.on('liszt:ready', function (evt, params) { this.set_statustext(this.statustext); }.bind(this)); this.input.chosen({ inherit_select_classes: true, search_contains: true, width: _width || size.w + "px", allow_single_deselect: this.options.allow_single_deselect, no_results_text: this.egw().lang('No results match') }); this.input.siblings().filter(".chzn-container") .attr("role", "combobox") .attr("tabindex", this.options.tabindex || "0"); // set autocomplete for search input field to an arbitary value in order to stop it. this.input.siblings().find('.chzn-search input').attr('autocomplete', 'new-password'); if (this.getType() == 'select-country') { var selected = this.input.siblings().find('a.chzn-single'); if (selected && selected.length == 1 && this.getValue()) { selected.removeClass(function (index, className) { return (className.match(/(^|\s)flag-\S+/g) || []).join(' '); }); selected.find('span.img').remove(); selected.prepend('<span class="img"></span>'); selected.addClass('et2_country-select flag-' + this.getValue().toLowerCase()); } } if (this.options.onchange) { // Unbind change handler of widget's ancestor to stop it from bubbling // chosen has its own onchange jQuery(this.input).unbind('change.et2_inputWidget'); var self = this; this.input.chosen().change(function (_ev, _change) { // enhance signature of regular et2_selectbox.onchange with 3. parameter from chosen self.change.call(self, self.input, self, _change); }); } // multi selection with limited show line of single row if (this.options.multiple && this.options.rows == 1 && this.options.height) { var $chosen_div = jQuery(this.input.siblings()); var self = this; /** * A function to set counter for multi tags limited for single row * @returns {undefined} */ var _update_item_counter = function () { $chosen_div.find('ul.chzn-choices').attr('data-after', self.getValue().length); }; // Update the item counter _update_item_counter(); // Initialize the single row class $chosen_div.toggleClass('et2_selectbox_single_row', true); // bind mouse handlers $chosen_div.on('mouseleave mouseup', function (e) { jQuery(this).toggleClass('et2_selectbox_multi_row', false); jQuery(this).toggleClass('et2_selectbox_single_row', true); _update_item_counter(); }); $chosen_div.on('mouseenter', function (e) { jQuery(this).toggleClass('et2_selectbox_multi_row', true); jQuery(this).toggleClass('et2_selectbox_single_row', false); }); } } } /** * The set_select_options function is added, as the select options have to be * added after the "option"-widgets were added to selectbox. * * @param {(array|object)} _options array or object with options */ set_select_options(_options) { // Empty current options if (this.input) { this.input.empty(); } else if (this.multiOptions) { this.multiOptions.empty(); } // Re-add empty, it's usually not there (empty_label get's allways translated, independent of no_lang!) // Empty label should not be added as an option for chosen, it conflicts if (this.options.empty_label && !(this.options.tags || this.options.search)) { this._appendOptionElement('', this.egw().lang(this.options.empty_label)); } // Add the select_options for (var key in _options) { // Translate the options if (!this.options.no_lang) { if (typeof _options[key] === 'object' && _options[key] !== null) { if (_options[key]["label"]) _options[key]["label"] = this.egw().lang(_options[key]["label"]); if (_options[key]["title"]) _options[key]["title"] = this.egw().lang(_options[key]["title"]); } else if (Array.isArray(_options) && _options.length > 0 || typeof _options === 'object') { _options[key] = this.egw().lang(_options[key]); } } if (typeof _options[key] === 'object' && _options[key] !== null) { // Optgroup if (typeof _options[key]["label"] == 'undefined' && typeof _options[key]["title"] == "undefined") { var label = isNaN(key) ? key : _options[key].value; var group = jQuery(document.createElement("optgroup")) .attr("label", this.options.no_lang ? label : this.egw().lang(label)) .appendTo(this.input); if (this.input == null) { group = jQuery(document.createElement("ul")) .append('<li class="ui-widget-header"><span>' + key + '</span></li>') .appendTo(this.multiOptions); } for (var sub in _options[key]) { if (sub == 'value') continue; if (typeof _options[key][sub] === 'object' && _options[key][sub] !== null) { this._appendOptionElement(sub, _options[key][sub]["label"] ? _options[key][sub]["label"] : "", _options[key][sub]["title"] ? _options[key][sub]["title"] : "", group); } else { this._appendOptionElement(sub, _options[key][sub], undefined, group); } } } else if (this.input == null) { // Allow some special extras for objects by passing the whole thing _options[key]["label"] = _options[key]["label"] ? _options[key]["label"] : ""; this._appendMultiOption(typeof _options[key].value != 'undefined' ? _options[key].value : key, _options[key], _options[key]["title"]); } else { this._appendOptionElement(typeof _options[key].value != 'undefined' ? _options[key].value : key, _options[key]["label"] ? _options[key]["label"] : "", _options[key]["title"] ? _options[key]["title"] : "", '', _options[key]["class"] ? _options[key]["class"] : ""); } } else { this._appendOptionElement(key, _options[key]); } } // add an empty option for single select tags with empty_label in order // to make allow_single_deselect option to work. if (this.options.empty_label && this.options.tags && this.options.allow_single_deselect && !this.options.multiple) { var empty_option = jQuery(document.createElement("option")) .attr("value", ''); empty_option.prependTo(this.input); } this.options.select_options = _options; if (this.options.tags || this.options.search) { this.input.trigger("liszt:updated"); } // Sometimes value gets set before options if (this.value === null && this.options.value) { // Null means it tried to set the value, and it got stripped by missing options this.set_value(this.options.value, true); } else if (this.value || (this.options.empty_label && !this.options.multiple) || this.value === '' && this.input && this.input.children('[value=""]').length === 1) { this.set_value(this.value, true); // true = dont try to set_options, to avoid an infinit recursion } } getValue() { let value = []; if (this.input == null) { jQuery("input:checked", this.multiOptions).each(function () { value.push(this.value); }); // we need to return null for no value instead of empty array, which gets overwritten by preserved value on server-side } else { value = super.getValue(); if (value === null) value = this.options.multiple ? [] : ""; // do NOT return null, as it does not get transmitted to server } return value; } isDirty() { if (this.input == null) { var value = this.getValue(); // Array comparison return !(jQuery(this._oldValue).not(value).length == 0 && jQuery(value).not(this._oldValue).length == 0); } return super.isDirty(); } /** * override set disabled for tags as the tags using * chosen dom and need to be treated different * * @param {type} _disable * @returns {undefined} */ set_disabled(_disable) { super.set_disabled(_disable); if (this.options.tags) { // Always hide input options if (this.input) { this.input.hide(); } if (_disable) { jQuery(this.node.nextElementSibling).hide(); } else { jQuery(this.node.nextElementSibling).show(); } } } /** * Find the select options for a widget, out of the many places they could be. * @param {et2_widget} widget to check for. Should be some sort of select widget. * @param {object} attr_options Select options in attributes array * @param {object} attrs Widget attributes * @return {object} Select options, or empty object */ static find_select_options(widget, attr_options, attrs) { var name_parts = widget.id.replace(/[/g, '[').replace(/]|]/g, '').split('['); var type_options = {}; var content_options = {}; // First check type, there may be static options. There's some special handling // for filterheaders, which have the wrong type. // TODO: filterheader should always be defined, find out why it's not var type = /* widget.instanceOf(et2_nextmatch_filterheader) ? attrs.widget_type || '' :*/ widget._type; var type_function = type.replace('select-', '').replace('taglist-', '').replace('_ro', '') + '_options'; if (typeof this[type_function] == 'function') { var old_type = widget._type; widget._type = type.replace('taglist-', 'select-'); if (typeof attrs.other == 'string') { attrs.other = attrs.other.split(','); } // Copy, to avoid accidental modification // // type options used to use jQuery.extend deep copy to get a clone object of options // but as jQuery.extend deep copy is very expensive operation in MSIE (in this case almost 400ms) // we use JSON parsing instead to copy the options object type_options = this[type_function].call(this, widget, attrs); try { type_options = JSON.parse(JSON.stringify(type_options)); } catch (e) { egw.debug(e); } widget._type = old_type; } // Try to find the options inside the "sel-options" if (widget.getArrayMgr("sel_options")) { // Try first according to ID content_options = widget.getArrayMgr("sel_options").getEntry(widget.id); // ID can get set to an array with 0 => ' ' - not useful if (content_options && (content_options.length == 1 && typeof content_options[0] == 'string' && content_options[0].trim() == '' || // eg. autorepeated id "cat[3]" would pick array element 3 from cat typeof content_options.value != 'undefined' && typeof content_options.label != 'undefined' && widget.id.match(/\[\d+\]$/))) { content_options = null; } // We could wind up too far inside options if label,title are defined if (content_options && !isNaN(name_parts[name_parts.length - 1]) && content_options.label && content_options.title) { name_parts.pop(); content_options = widget.getArrayMgr("sel_options").getEntry(name_parts.join('[')); delete content_options["$row"]; } // Select options tend to be defined once, at the top level, so try that if (!content_options || content_options.length == 0) { content_options = widget.getArrayMgr("sel_options").getRoot().getEntry(name_parts[name_parts.length - 1]); } // Try in correct namespace (inside a grid or something) if (!content_options || content_options.length == 0) { content_options = widget.getArrayMgr("sel_options").getEntry(name_parts[name_parts.length - 1]); } // Try name like widget[$row] if (name_parts.length > 1 && (!content_options || content_options.length == 0)) { var pop_that = jQuery.extend([], name_parts); while (pop_that.length > 1 && (!content_options || content_options.length == 0)) { var last = pop_that.pop(); content_options = widget.getArrayMgr('sel_options').getEntry(pop_that.join('[')); // Double check, might have found a normal parent namespace ( eg subgrid in subgrid[selectbox] ) // with an empty entry for the selecbox. If there were valid options here, // we would have found them already, and keeping this would result in the ID as an option if (content_options && !jQuery.isArray(content_options) && typeof content_options[last] != 'undefined' && content_options[last]) { content_options = content_options[last]; } else if (content_options) { // Check for real values for (var key in content_options) { if (!(isNaN(key) && typeof content_options[key] === 'string' || !isNaN(key) && typeof content_options[key] === 'object' && typeof content_options[key]['value'] !== 'undefined')) { // Found a parent of some other namespace content_options = undefined; break; } } } } } // Maybe in a row, and options got stuck in ${row} instead of top level // not sure this code is still needed, as server-side no longer creates ${row} or {$row} for select-options var row_stuck = ['${row}', '{$row}']; for (let i = 0; i < row_stuck.length && (!content_options || content_options.length == 0); i++) { // perspectiveData.row in nm, data["${row}"] in an auto-repeat grid if (widget.getArrayMgr("sel_options").perspectiveData.row || widget.getArrayMgr("sel_options").data[row_stuck[i]]) { var row_id = widget.id.replace(/[0-9]+/, row_stuck[i]); content_options = widget.getArrayMgr("sel_options").getEntry(row_id); if (!content_options || content_options.length == 0) { content_options = widget.getArrayMgr("sel_options").getEntry(row_stuck[i] + '[' + widget.id + ']'); } } } if (attr_options && !jQuery.isEmptyObject(attr_options) && content_options) { content_options = jQuery.extend(true, {}, attr_options, content_options); } } // Check whether the options entry was found, if not read it from the // content array. if (jQuery.isEmptyObject(content_options) && widget.getArrayMgr('content') != null) { if (content_options) attr_options = content_options; var content_mgr = widget.getArrayMgr('content'); if (content_mgr) { // If that didn't work, check according to ID if (!content_options) content_options = content_mgr.getEntry("options-" + widget.id); // Again, try last name part at top level if (!content_options) content_options = content_mgr.getRoot().getEntry("options-" + name_parts[name_parts.length - 1]); } } // Default to an empty object if (content_options == null) { content_options = {}; } // Include type options, preferring any content options if (type_options.length || !jQuery.isEmptyObject(type_options)) { for (let i in content_options) { var value = typeof content_options[i] == 'object' && typeof content_options[i].value !== 'undefined' ? content_options[i].value : i; var added = false; // Override any existing for (var j in type_options) { if ('' + type_options[j].value === '' + value) { added = true; type_options[j] = content_options[i]; break; } } if (!added) { type_options.splice(i, 0, content_options[i]); } } content_options = type_options; } return content_options; } /** * Some static options, no need to transfer them over and over. * We still need the same thing on the server side to validate, so they * have to match. See Etemplate\Widget\Select::typeOptions() * The type specific legacy options wind up in attrs.other. * * @param {type} widget */ static priority_options(widget) { return [ { value: 1, label: 'low' }, { value: 2, label: 'normal' }, { value: 3, label: 'high' }, { value: 0, label: 'undefined' } ]; } static bool_options(widget) { return [ { value: 0, label: 'no' }, { value: 1, label: 'yes' } ]; } static month_options(widget) { return [ { value: 1, label: 'January' }, { value: 2, label: 'February' }, { value: 3, label: 'March' }, { value: 4, label: 'April' }, { value: 5, label: 'May' }, { value: 6, label: 'June' }, { value: 7, label: 'July' }, { value: 8, label: 'August' }, { value: 9, label: 'September' }, { value: 10, label: 'October' }, { value: 11, label: 'November' }, { value: 12, label: 'December' } ]; } static number_options(widget, attrs) { if (typeof attrs.other != 'object') { attrs.other = []; } var options = []; var min = typeof (attrs.other[0]) == 'undefined' ? 1 : parseInt(attrs.other[0]); var max = typeof (attrs.other[1]) == 'undefined' ? 10 : parseInt(attrs.other[1]); var interval = typeof (attrs.other[2]) == 'undefined' ? 1 : parseInt(attrs.other[2]); var format = '%d'; // leading zero specified in interval if (attrs.other[2] && attrs.other[2][0] == '0') { format = '%0' + ('' + interval).length + 'd'; } // Suffix if (attrs.other[3]) { format += widget.egw().lang(attrs.other[3]); } // Avoid infinite loop if interval is the wrong direction if ((min <= max) != (interval > 0)) { interval = -interval; } for (var i = 0, n = min; n <= max && i <= 100; n += interval, ++i) { options.push({ value: n, label: sprintf(format, n) }); } return options; } static percent_options(widget, attrs) { if (typeof attrs.other != 'object') { attrs.other = []; } attrs.other[0] = 0; attrs.other[1] = 100; attrs.other[2] = typeof (attrs.other[2]) == 'undefined' ? 10 : parseInt(attrs.other[2]); attrs.other[3] = '%%'; return this.number_options(widget, attrs); } static year_options(widget, attrs) { if (typeof attrs.other != 'object') { attrs.other = []; } var t = new Date(); attrs.other[0] = t.getFullYear() - (typeof (attrs.other[0]) == 'undefined' ? 3 : parseInt(attrs.other[0])); attrs.other[1] = t.getFullYear() + (typeof (attrs.other[1]) == 'undefined' ? 2 : parseInt(attrs.other[1])); attrs.other[2] = typeof (attrs.other[2]) == 'undefined' ? 1 : parseInt(attrs.other[2]); return this.number_options(widget, attrs); } static day_options(widget, attrs) { attrs.other = [1, 31, 1]; return this.number_options(widget, attrs); } static hour_options(widget, attrs) { var options = []; var timeformat = egw.preference('common', 'timeformat'); for (var h = 0; h <= 23; ++h) { options.push({ value: h, label: timeformat == 12 ? ((12 ? h % 12 : 12) + ' ' + (h < 12 ? egw.lang('am') : egw.lang('pm'))) : sprintf('%02d', h) }); } return options; } static app_options(widget, attrs) { var options = ',' + (attrs.other || []).join(','); return this.cached_server_side_options(widget, options, attrs); } static cat_options(widget, attrs) { // Add in application, if not there if (typeof attrs.other == 'undefined') { attrs.other = new Array(4); } if (typeof attrs.other[3] == 'undefined') { attrs.other[3] = attrs.application || widget.getInstanceManager().app; } var options = (attrs.other || []).join(','); return this.cached_server_side_options(widget, options, attrs); } static country_options(widget, attrs) { var options = ','; return this.cached_server_side_options(widget, options, attrs); } static state_options(widget, attrs) { var options = attrs.country_code ? attrs.country_code : 'de'; return this.cached_server_side_options(widget, options, attrs); } static dow_options(widget, attrs) { var options = ',' + (attrs.other || []).join(','); return this.cached_server_side_options(widget, options, attrs); } static lang_options(widget, attrs) { var options = ',' + (attrs.other || []).join(','); return this.cached_server_side_options(widget, options, attrs); } static timezone_options(widget, attrs) { var options = ',' + (attrs.other || []).join(','); return this.cached_server_side_options(widget, options, attrs); } /** * Some options change, or are too complicated to have twice, so we get the * options from the server once, then keep them to use if they're needed again. * We use the options string to keep the different possibilites (eg. categories * for different apps) seperate. * * @param {et2_selectbox} widget Selectbox we're looking at * @param {string} options_string * @param {Object} attrs Widget attributes (not yet fully set) * @returns {Object} Array of options, or empty and they'll get filled in later */ static cached_server_side_options(widget, options_string, attrs) { // normalize options by removing trailing commas options_string = options_string.replace(/,+$/, ''); var cache_id = widget._type + '_' + options_string; var cache_owner = ( // Todo: @new-js-loader et2_selectbox is no longer instanciated globaly --> caching needs to be fixed et2_selectbox /*egw.window.et2_selectbox ? egw.window.et2_selectbox : egw(window).window.et2_selectbox*/ ).type_cache; var cache = cache_owner[cache_id]; // Options for a selectbox in a nextmatch must be returned now, as the // widget we have is not enough to set the options later. var in_nextmatch = false; if (typeof cache === 'undefined' || typeof cache.length === 'undefined') { var parent = widget._parent; while (parent && !in_nextmatch) { in_nextmatch = parent && parent._type && parent._type === 'nextmatch'; parent = parent._parent; } } if (typeof cache == 'undefined' || in_nextmatch) { // Fetch with json instead of jsonq because there may be more than // one widget listening for the response by the time it gets back, // and we can't do that when it's queued. var req = egw.json('EGroupware\\Api\\Etemplate\\Widget\\Select::ajax_get_options', [widget._type, options_string, attrs.value]).sendRequest(!in_nextmatch); if (typeof cache === 'undefined') { cache_owner[cache_id] = req; } cache = req; } if (typeof cache.done == 'function') { // pending, wait for it cache.done(jQuery.proxy(function (response) { cache = cache_owner[cache_id] = response.response[0].data || undefined; // Set select_options in attributes in case we get a resonse before // the widget is finished loading (otherwise it will re-set to {}) attrs.select_options = cache; egw.window.setTimeout(jQuery.proxy(function () { // Avoid errors if widget is destroyed before the timeout if (this.widget && typeof this.widget.id !== 'undefined') { this.widget.set_select_options(et2_selectbox.find_select_options(this.widget, {}, this.widget.options)); } }, this), 1); }, { widget: widget, cache_id: cache_id })); return []; } else { // Check that the value is in there // Make sure we are not requesting server for an empty value option or // other widgets but select-timezone as server won't find anything and // it will fall into an infinitive loop, e.g. select-cat widget. if (attrs.value && attrs.value != "" && attrs.value != "0" && attrs.type == "select-timezone") { var missing_option = true; for (var i = 0; i < cache.length && missing_option; i++) { if (cache[i].value == attrs.value) { missing_option = false; } } // Try again - ask the server with the current value this time if (missing_option) { delete cache_owner[cache_id]; return this.cached_server_side_options(widget, options_string, attrs); } else { if (attrs.value && widget && widget.get_value() !== attrs.value) { egw.window.setTimeout(jQuery.proxy(function () { // Avoid errors if widget is destroyed before the timeout if (this.widget && typeof this.widget.id !== 'undefined') { this.widget.set_value(this.widget.options.value); } }, { widget: widget }), 1); } } } return cache; } } } et2_selectbox._attributes = { // todo fully implement attr[multiple] === "dynamic" to render widget with a button to switch to multiple // as it is used in account_id selection in admin >> mailaccount (app.admin.edit_multiple method client-side) "multiple": { "name": "multiple", "type": "boolean", "default": false, "description": "Allow selecting multiple options" }, "expand_multiple_rows": { "name": "Expand multiple", "type": "integer", "default": et2_no_init, "description": "Shows single select widget, with a button. If the " + "user clicks the button, the input will toggle to a multiselect," + "with this many rows. " }, "rows": { "name": "Rows", "type": "any", "default": 1, "description": "Number of rows to display" }, "empty_label": { "name": "Empty label", "type": "string", "default": "", "description": "Textual label for first row, eg: 'All' or 'None'. ID will be ''", translate: true }, "select_options": { "type": "any", "name": "Select options", "default": {}, "description": "Internaly used to hold the select options." }, "selected_first": { "name": "Selected options first", "type": "boolean", "default": true, "description": "For multi-selects, put the selected options at the top of the list when first loaded" }, // Chosen options "search": { "name": "Search", "type": "boolean", "default": false, "description": "For single selects, add a search box to the drop-down list" }, "tags": { "name": "Tag style", "type": "boolean", "default": false, "description": "For multi-selects, displays selected as a list of tags instead of a big list" }, "allow_single_deselect": { "name": "Allow Single Deselect", "type": "boolean", "default": true, "description": "Allow user to unset current selected value" }, // Value can be string or integer "value": { "type": "any" }, // Type specific legacy options. Avoid using. "other": { "ignore": true, "type": "any" }, value_class: { name: "Value class", type: "string", default: "", description: "Allow to set a custom css class combined with selected value. (e.g. cat_23)" } }; et2_selectbox.legacyOptions = ["rows", "other"]; // Other is sub-type specific et2_selectbox.type_cache = {}; et2_register_widget(et2_selectbox, ["menupopup", "listbox", "select", "select-cat", "select-percent", 'select-priority', 'select-country', 'select-state', 'select-year', 'select-month', 'select-day', 'select-dow', 'select-hour', 'select-number', 'select-app', 'select-lang', 'select-bool', 'select-timezone', 'select-bitwise']); /** * et2_selectbox_ro is the readonly implementation of the selectbox. */ export class et2_selectbox_ro extends et2_selectbox { /** * Constructor */ constructor(_parent, _attrs, _child) { // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_selectbox_ro._attributes, _child || {})); this.optionValues = {}; this.supportedWidgetClasses = []; this.optionValues = {}; if (this.options.select_options) this.set_select_options(this.options.select_options); } createInputWidget() { this.span = jQuery(document.createElement("span")) .addClass("et2_selectbox readonly") .text(this.options.empty_label); this.setDOMNode(this.span[0]); } // Handle read-only multiselects in the same way createMultiSelect() { this.span = jQuery(document.createElement("ul")) .addClass("et2_selectbox readonly"); this.setDOMNode(this.span[0]); } loadFromXML(_node) { // Read the option-tags var options = et2_directChildrenByTagName(_node, "options"); for (var i = 0; i < options.length; i++) { this.optionValues[et2_readAttrWithDefault(options[i], "value", 0)] = { "label": options[i].textContent, "title": et2_readAttrWithDefault(options[i], "title", "") }; } } set_select_options(_options) { for (var key in _options) { // Translate the options if (!this.options.no_lang) { if (typeof _options[key] === 'object' && _options[key] != null) { if (_options[key]["label"]) _options[key]["label"] = this.egw().lang(_options[key]["label"]); if (_options[key]["title"]) _options[key]["title"] = this.egw().lang(_options[key]["title"]); } else { _options[key] = this.egw().lang(_options[key]); } } } this.optionValues = _options; } set_value(_value) { this.value = _value; if (this.getType() == "select-bitwise" && _value && !isNaN(_value) && this.options.select_options) { var new_value = []; for (var index in this.options.select_options) { var option = this.options.select_options[index]; var right = option && option.value ? option.value : index; if (!!(_value & right)) { new_value.push(right); } } _value = new_value; } if (typeof _value == "string") { _value = _value.match(this._is_multiple_regexp) !== null ? _value.split(',') : [_value]; } // need to handle numerical values too else if (typeof _value == 'number') { _value = [_value]; } this.span.empty(); if (_value) { for (var i = 0; i < _value.length; i++) { for (var o in this.optionValues) { var option = this.optionValues[o]; var key = typeof option == 'object' && option != null && typeof option.value != 'undefined' ? option.value : o; if (key != _value[i]) continue; var label = typeof option == 'object' ? option.label : option; if (_value.length == 1) { this.span.text(label); if (typeof option == 'object' && option.title) this.set_statustext(option.title); break; } else { jQuery('<li>') .text(label) .attr('data-value', _value[i]) .appendTo(this.span); break; } } } } else if (this.options.empty_label) { this.span.text(this.options.empty_label); } } /** * Override parent to return null - no value, not node value */ getValue() { return null; } /** * Readonly selectbox can't be dirty */ isDirty() { return false; } /** * Functions for et2_IDetachedDOM */ /** * Creates a list of attributes which can be set when working in the * "detached" mode. The result is stored in the _attrs array which is provided * by the calling code. * * @param {array} _attrs array to add further attributes to */ getDetachedAttributes(_attrs) { _attrs.push("value", 'select_options'); } /** * Returns an array of DOM nodes. The (relatively) same DOM-Nodes have to be * passed to the "setDetachedAttributes" function in the same order. * * @return {array} */ getDetachedNodes() { return [this.span[0]]; } /** * Sets the given associative attribute->value array and applies the * attributes to the given DOM-Node. * * @param _nodes is an array of nodes which have to be in the same order as * the nodes returned by "getDetachedNodes" * @param _values is an associative array which contains a subset of attributes * returned by the "getDetachedAttributes" function and sets them to the * given values. */ setDetachedAttributes(_nodes, _values) { this.span = jQuery(_nodes[0]); if (typeof _values.select_options != 'undefined') { this.set_select_options(_values.select_options); } this.set_value(_values["value"]); } } et2_register_widget(et2_selectbox_ro, ["menupopup_ro", "listbox_ro", "select_ro", "select-cat_ro", "select-percent_ro", 'select-priority_ro', 'select-access_ro', 'select-country_ro', 'select-state_ro', 'select-year_ro', 'select-month_ro', 'select-day_ro', 'select-dow_ro', 'select-hour_ro', 'select-number_ro', 'select-app_ro', 'select-lang_ro', 'select-bool_ro', 'select-timezone_ro', 'select-bitwise_ro']); /** * Widget class which represents a single option inside a selectbox */ /*var et2_option = et2_baseWidget.extend({ static readonly _attributes : any = { "value": { "name": "Value", "type": "string", "description": "Value which is sent back to the server when this entry is selected." }, "label": { "name": "Label", "type": "string", "description": "Caption of the option element" }, "width": { "ignore": true }, "height": { "ignore": true }, "align": { "ignore": true } } init() { this._super.apply(this, arguments); // Only allow other options inside of this element this.supportedWidgetClasses = [et2_option]; this.option = jQuery(document.createElement("option")) .attr("value", this.options.value) .attr("selected", this._parent.options.value == this.options.value ? "selected" : ""); if (this.options.label) { this.option.text(this.options.label); } this.setDOMNode(this.option[0]); } destroy() { this._super.apply(this, arguments); this.option = null; } loadContent(_data) { this.option.text(_data); } /* Doesn't work either with selectboxes set_statustext(_value) { this.statustext = _value; this.option.attr("title", _value); }*/ //});*/ //et2_register_widget(et2_option, ["option"]); /** * Class which just implements the menulist container */ export class et2_menulist extends et2_DOMWidget { /** * Constructor */ constructor(_parent, _attrs, _child) { // Call the inherited constructor super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_menulist._attributes, _child || {})); this.supportedWidgetClasses = [et2_selectbox, et2_selectbox_ro]; } // Just pass the parent DOM node through getDOMNode(_sender) { if (_sender != this) { return this.getParent().getDOMNode(this); } return null; } // Also need to pass through parent's children getChildren() { return this.getParent().getChildren(); } } et2_menulist._attributes = {}; et2_register_widget(et2_menulist, ["menulist"]); //# sourceMappingURL=et2_widget_selectbox.js.map