From ed16ce52a20a7e040e5d6c4ed0ee8f109e80d70e Mon Sep 17 00:00:00 2001 From: nathan Date: Thu, 7 Jul 2022 13:18:42 -0600 Subject: [PATCH] Change nextmatch headers to use web components --- addressbook/templates/default/app.css | 8 +- addressbook/templates/pixelegg/app.css | 4 +- api/etemplate.php | 63 ++- .../Headers/AccountFilterHeader.ts | 19 + .../Headers/CustomFilterHeader.ts | 92 ++++ .../Et2Nextmatch/Headers/EntryHeader.ts | 43 ++ .../Et2Nextmatch/Headers/FilterHeader.ts | 18 + .../Et2Nextmatch/Headers/FilterMixin.ts | 76 +++ api/js/etemplate/Et2Select/Et2Select.ts | 1 + api/js/etemplate/et2_extension_nextmatch.ts | 483 ++---------------- api/js/etemplate/etemplate2.ts | 4 + .../Widget/Nextmatch/Customfilter.php | 4 +- api/templates/default/etemplate2.css | 4 +- 13 files changed, 358 insertions(+), 461 deletions(-) create mode 100644 api/js/etemplate/Et2Nextmatch/Headers/AccountFilterHeader.ts create mode 100644 api/js/etemplate/Et2Nextmatch/Headers/CustomFilterHeader.ts create mode 100644 api/js/etemplate/Et2Nextmatch/Headers/EntryHeader.ts create mode 100644 api/js/etemplate/Et2Nextmatch/Headers/FilterHeader.ts create mode 100644 api/js/etemplate/Et2Nextmatch/Headers/FilterMixin.ts diff --git a/addressbook/templates/default/app.css b/addressbook/templates/default/app.css index 9c691fa4d9..3553e7a4f6 100644 --- a/addressbook/templates/default/app.css +++ b/addressbook/templates/default/app.css @@ -108,10 +108,10 @@ div.city_state_postcode #addressbook-edit_adr_one_postalcode {margin-right: 5px * adjust width of select-boxes in nextmatch */ #addressbook-index .filtersContainer { - position: absolute; - top: 0; /* Required for Chrome 76+ on Windows */ - left: 342px; - right: 215px; + position: absolute; + top: 0; /* Required for Chrome 76+ on Windows */ + left: 350px; + right: 233px; } #addressbook-index .filtersContainer select { width: 31.5%; diff --git a/addressbook/templates/pixelegg/app.css b/addressbook/templates/pixelegg/app.css index 76c247c440..d564748fb1 100755 --- a/addressbook/templates/pixelegg/app.css +++ b/addressbook/templates/pixelegg/app.css @@ -126,8 +126,8 @@ div.city_state_postcode #addressbook-edit_adr_one_postalcode { position: absolute; top: 0; /* Required for Chrome 76+ on Windows */ - left: 342px; - right: 215px; + left: 350px; + right: 233px; } #addressbook-index .filtersContainer select { width: 31.5%; diff --git a/api/etemplate.php b/api/etemplate.php index 6753f04adc..861c1243f6 100644 --- a/api/etemplate.php +++ b/api/etemplate.php @@ -82,23 +82,27 @@ function send_template() list($matches[1], $matches[2]) = explode('-', $type[1], 2); if (!empty($matches[2])) $matches[2] = '-'.$matches[2]; } - static $legacy_options = array( // use "ignore" to ignore further comma-sep. values, otherwise they are all in last attribute - 'select' => 'empty_label,ignore', - 'select-account' => 'empty_label,account_type,ignore', - 'select-number' => 'empty_label,min,max,interval,suffix', - 'box' => ',cellpadding,cellspacing,keep', - 'hbox' => 'cellpadding,cellspacing,keep', - 'vbox' => 'cellpadding,cellspacing,keep', - 'groupbox' => 'cellpadding,cellspacing,keep', - 'checkbox' => 'selected_value,unselected_value,ro_true,ro_false', - 'radio' => 'set_value,ro_true,ro_false', - 'customfields' => 'sub-type,use-private,field-names', - 'date' => 'data_format,ignore', + static $legacy_options = array( + // use "ignore" to ignore further comma-sep. values, otherwise they are all in last attribute + 'select' => 'empty_label,ignore', + 'select-account' => 'empty_label,account_type,ignore', + 'select-number' => 'empty_label,min,max,interval,suffix', + 'box' => ',cellpadding,cellspacing,keep', + 'hbox' => 'cellpadding,cellspacing,keep', + 'vbox' => 'cellpadding,cellspacing,keep', + 'groupbox' => 'cellpadding,cellspacing,keep', + 'checkbox' => 'selected_value,unselected_value,ro_true,ro_false', + 'radio' => 'set_value,ro_true,ro_false', + 'customfields' => 'sub-type,use-private,field-names', + 'date' => 'data_format,ignore', // Legacy option "mode" was never implemented in et2 - 'description' => 'bold-italic,link,activate_links,label_for,link_target,link_popup_size,link_title', - 'button' => 'image,ro_image', - 'buttononly' => 'image,ro_image', - 'link-entry' => 'only_app,application_list', + 'description' => 'bold-italic,link,activate_links,label_for,link_target,link_popup_size,link_title', + 'button' => 'image,ro_image', + 'buttononly' => 'image,ro_image', + 'link-entry' => 'only_app,application_list', + 'nextmatch-filterheader' => 'empty_label', + 'nextmatch-customfilter' => 'widget_type,widget_options', + 'nextmatch-accountfilter' => 'empty_label,account_type,ignore', ); // prefer more specific type-subtype over just type $names = $legacy_options[$matches[1] . $matches[2]] ?? $legacy_options[$matches[1]] ?? null; @@ -229,6 +233,33 @@ function send_template() return $replace; }, $str); + // nextmatch headers + $str = preg_replace_callback('#<(nextmatch-)([^ ]+)(header|filter) ([^>]+?)/>#s', static function (array $matches) + { + preg_match_all('/(^|\s)([a-z0-9_-]+)="([^"]*)"/i', $matches[4], $attrs, PREG_PATTERN_ORDER); + $attrs = array_combine($attrs[2], $attrs[3]); + + if(!$matches[2] || in_array($matches[2], ['sort'])) + { + return $matches[0]; + } + // No longer needed & type causes problems + unset($attrs['type'], $attrs['tags']); + + if($matches[2] == 'taglist') + { + $matches[2] = "filter"; + } + + $replace = ''; + return $replace; + }, $str); + // ^^^^^^^^^^^^^^^^ above widgets get transformed independent of legacy="true" set in overlay ^^^^^^^^^^^^^^^^^^ // eTemplate marked as legacy --> replace only some widgets (eg. requiring jQueryUI) with web-components diff --git a/api/js/etemplate/Et2Nextmatch/Headers/AccountFilterHeader.ts b/api/js/etemplate/Et2Nextmatch/Headers/AccountFilterHeader.ts new file mode 100644 index 0000000000..3f6bfc3080 --- /dev/null +++ b/api/js/etemplate/Et2Nextmatch/Headers/AccountFilterHeader.ts @@ -0,0 +1,19 @@ +import {Et2SelectAccount} from "../../Et2Select/Et2SelectAccount"; +import {et2_INextmatchHeader} from "../../et2_extension_nextmatch"; +import {FilterMixin} from "./FilterMixin"; + +/** + * Filter by account + */ +export class Et2AccountFilterHeader extends FilterMixin(Et2SelectAccount) implements et2_INextmatchHeader +{ + constructor(...args : any[]) + { + super(); + this.hoist = true; + this.clearable = true; + } + +} + +customElements.define("et2-nextmatch-header-account", Et2AccountFilterHeader); \ No newline at end of file diff --git a/api/js/etemplate/Et2Nextmatch/Headers/CustomFilterHeader.ts b/api/js/etemplate/Et2Nextmatch/Headers/CustomFilterHeader.ts new file mode 100644 index 0000000000..0c5cb8cf54 --- /dev/null +++ b/api/js/etemplate/Et2Nextmatch/Headers/CustomFilterHeader.ts @@ -0,0 +1,92 @@ +import {loadWebComponent} from "../../Et2Widget/Et2Widget"; +import {Et2Select} from "../../Et2Select/Et2Select"; +import {Et2InputWidget, Et2InputWidgetInterface} from "../../Et2InputWidget/Et2InputWidget"; +import {FilterMixin} from "./FilterMixin"; +import {html, LitElement} from "@lion/core"; + +/** + * Filter by some other type of widget + * Acts as a wrapper around the other widget, but handles all the nm stuff here + * Any attributes set are passed to the filter widget + */ +export class Et2CustomFilterHeader extends FilterMixin(Et2InputWidget(LitElement)) +{ + private widget_type : string; + private widget_options : {}; + private filter_node : Et2InputWidgetInterface & LitElement; + + static get properties() + { + return { + ...super.properties, + + /** + * tag of widget we want to use to filter + */ + widget_type: {type: String}, + + /** + * Attributes / properties used for the filter widget + */ + widget_options: {type: Object} + }; + } + + constructor(...args : any[]) + { + super(); + this.widget_type = "et2-description"; + this.widget_options = {}; + } + + transformAttributes(attrs) + { + super.transformAttributes(attrs); + + switch(attrs.widget_type) + { + case "link-entry": + this.widget_type = 'et2-nextmatch-header-entry'; + break; + default: + this.widget_type = attrs.widget_type; + // Prefer webcomponent, if legacy type was sent + if(window.customElements.get("et2-" + this.widget_type)) + { + this.widget_type = "et2-" + this.widget_type; + } + } + // @ts-ignore TS doesn't know about this.getParent() + this.filter_node = loadWebComponent(this.widget_type, {...attrs, ...this.widget_options}, this); + if(this.filter_node instanceof Et2Select) + { + this.filter_node.hoist = true; + this.filter_node.clearable = true; + } + } + + connectedCallback() + { + super.connectedCallback(); + if(this.filter_node) + { + this.filter_node.updateComplete.then(() => + { + this.filter_node.addEventListener("change", this.handleChange); + }) + } + } + + render() + { + return html` + `; + } + + get value() { return this.filter_node?.value || undefined;} + + set value(new_value) { this.filter_node.value = new_value;} + +} + +customElements.define("et2-nextmatch-header-custom", Et2CustomFilterHeader); \ No newline at end of file diff --git a/api/js/etemplate/Et2Nextmatch/Headers/EntryHeader.ts b/api/js/etemplate/Et2Nextmatch/Headers/EntryHeader.ts new file mode 100644 index 0000000000..b88ee067e5 --- /dev/null +++ b/api/js/etemplate/Et2Nextmatch/Headers/EntryHeader.ts @@ -0,0 +1,43 @@ +import {et2_INextmatchHeader} from "../../et2_extension_nextmatch"; +import {FilterMixin} from "./FilterMixin"; +import {Et2LinkEntry} from "../../Et2Link/Et2LinkEntry"; + +/** + * Filter using a selected entry + */ +export class Et2EntryFilterHeader extends FilterMixin(Et2LinkEntry) implements et2_INextmatchHeader +{ + + /** + * Override to always return a string appname:id (or just id) for simple (one real selection) + * cases, parent returns an object. If multiple are selected, or anything other than app and + * id, the original parent value is returned. + */ + get value() + { + let value = super.value; + if(typeof value == "object" && value != null) + { + if(!value.app || !value.id) + { + return null; + } + + // If simple value, format it legacy string style, otherwise + // we return full value + if(typeof value.id == 'string') + { + value = value.app + ":" + value.id; + } + } + return value; + } + + set value(new_value) + { + super.value = new_value; + } + +} + +customElements.define("et2-nextmatch-header-entry", Et2EntryFilterHeader); diff --git a/api/js/etemplate/Et2Nextmatch/Headers/FilterHeader.ts b/api/js/etemplate/Et2Nextmatch/Headers/FilterHeader.ts new file mode 100644 index 0000000000..c8bc14dee2 --- /dev/null +++ b/api/js/etemplate/Et2Nextmatch/Headers/FilterHeader.ts @@ -0,0 +1,18 @@ +import {et2_INextmatchHeader} from "../../et2_extension_nextmatch"; +import {Et2Select} from "../../Et2Select/Et2Select"; +import {FilterMixin} from "./FilterMixin"; + +/** + * Filter from a provided list of options + */ +export class Et2FilterHeader extends FilterMixin(Et2Select) implements et2_INextmatchHeader +{ + constructor(...args : any[]) + { + super(...args); + this.hoist = true; + this.clearable = true; + } +} + +customElements.define("et2-nextmatch-header-filter", Et2FilterHeader); diff --git a/api/js/etemplate/Et2Nextmatch/Headers/FilterMixin.ts b/api/js/etemplate/Et2Nextmatch/Headers/FilterMixin.ts new file mode 100644 index 0000000000..21888e5e39 --- /dev/null +++ b/api/js/etemplate/Et2Nextmatch/Headers/FilterMixin.ts @@ -0,0 +1,76 @@ +import {egw} from "../../../jsapi/egw_global"; +import {et2_INextmatchHeader, et2_nextmatch} from "../../et2_extension_nextmatch"; +import {LitElement} from "@lion/core"; + +// Export the Interface for TypeScript +type Constructor = new (...args : any[]) => T; + +/** + * Base class for things that do filter type behaviour in nextmatch header + * Separated to keep things a little simpler. + * + * Currently I assume we're extending an Et2Select, so changes may need to be made for better abstraction + */ +export const FilterMixin = (superclass : T) => class extends superclass implements et2_INextmatchHeader +{ + private nextmatch : et2_nextmatch; + + /** + * Override to add change handler + * + */ + connectedCallback() + { + super.connectedCallback(); + + // Make sure there's an option for all + if(!this.empty_label && Array.isArray(this.select_options) && !this.select_options.find(o => o.value == "")) + { + this.empty_label = this.label ? this.label : egw.lang("All"); + } + + this.handleChange = this.handleChange.bind(this); + + // Bind late, maybe that helps early change triggers? + this.updateComplete.then(() => + { + this.addEventListener("change", this.handleChange); + }); + } + + disconnectedCallback() + { + super.disconnectedCallback(); + this.removeEventListener("change", this.handleChange); + } + + handleChange(event) + { + if(typeof this.nextmatch == 'undefined') + { + // Not fully set up yet + return; + } + let col_filter = {}; + col_filter[this.id] = this.value; + + this.nextmatch.applyFilters({col_filter: col_filter}); + } + + /** + * Set nextmatch is the function which has to be implemented for the + * et2_INextmatchHeader interface. + * + * @param {et2_nextmatch} _nextmatch + */ + setNextmatch(_nextmatch : et2_nextmatch) + { + this.nextmatch = _nextmatch; + + // Set current filter value from nextmatch settings + if(this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) + { + this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); + } + } +} \ No newline at end of file diff --git a/api/js/etemplate/Et2Select/Et2Select.ts b/api/js/etemplate/Et2Select/Et2Select.ts index 9c7086445a..eb18f8def3 100644 --- a/api/js/etemplate/Et2Select/Et2Select.ts +++ b/api/js/etemplate/Et2Select/Et2Select.ts @@ -66,6 +66,7 @@ export class Et2Select extends Et2WithSearchMixin(Et2WidgetWithSelect) css` :host { display: block; + flex: 1 0 auto; --icon-width: 20px; } diff --git a/api/js/etemplate/et2_extension_nextmatch.ts b/api/js/etemplate/et2_extension_nextmatch.ts index 6305f675f5..8ba880d5c8 100644 --- a/api/js/etemplate/et2_extension_nextmatch.ts +++ b/api/js/etemplate/et2_extension_nextmatch.ts @@ -59,11 +59,9 @@ import {et2_nextmatch_controller} from "./et2_extension_nextmatch_controller"; import {et2_dataview} from "./et2_dataview"; import {et2_dataview_column} from "./et2_dataview_model_columns"; import {et2_customfields_list} from "./et2_extension_customfields"; -import {et2_link_entry, et2_link_to} from "./et2_widget_link"; +import {et2_link_to} from "./et2_widget_link"; import {et2_grid} from "./et2_widget_grid"; import {et2_dataview_grid} from "./et2_dataview_view_grid"; -import {et2_taglist} from "./et2_widget_taglist"; -import {et2_selectAccount} from "./et2_widget_selectAccount"; import {et2_dynheight} from "./et2_widget_dynheight"; import {et2_arrayMgr} from "./et2_core_arrayMgr"; import {et2_button} from "./et2_widget_button"; @@ -77,8 +75,11 @@ import {Et2Dialog} from "./Et2Dialog/Et2Dialog"; import {Et2Select} from "./Et2Select/Et2Select"; import {Et2Button} from "./Et2Button/Et2Button"; import {loadWebComponent} from "./Et2Widget/Et2Widget"; +import {Et2AccountFilterHeader} from "./Nextmatch/Headers/AccountFilterHeader"; +import {Et2SelectCategory} from "./Et2Select/Et2SelectCategory"; //import {et2_selectAccount} from "./et2_widget_SelectAccount"; +let keep_import : Et2AccountFilterHeader /** * Interface all special nextmatch header elements have to implement. @@ -1260,7 +1261,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 _widget.iterateOver(function(_widget) { - const label = self.egw().lang(_widget.options.label || _widget.options.empty_label || ''); + const label = self.egw().lang(_widget.label || _widget.empty_label || _widget.options.label || _widget.options.empty_label || ''); if(!label) return; // skip empty, undefined or null labels if(!result) { @@ -1999,18 +2000,18 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 autoRefresh = loadWebComponent("et2-select", { empty_label: "Refresh", id: "nm_autorefresh", - select_options: { - // Cause [unknown] problems with mail - 30: "30 seconds", - //60: "1 Minute", - 180: "3 Minutes", - 300: "5 Minutes", - 900: "15 Minutes", - 1800: "30 Minutes" - }, statustext: egw.lang("Automatically refresh list"), value: this._get_autorefresh() }, this); + autoRefresh.select_options = { + // Cause [unknown] problems with mail + 30: "30 seconds", + //60: "1 Minute", + 180: "3 Minutes", + 300: "5 Minutes", + 900: "15 Minutes", + 1800: "30 Minutes" + }; } const defaultCheck = loadWebComponent("et2-select", { @@ -2181,7 +2182,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 // Add autorefresh if(autoRefresh) { - $footerWrap.append(autoRefresh.getSurroundings().getDOMNode(autoRefresh.getDOMNode())); + $footerWrap.append(autoRefresh); } // Add default checkbox for admins @@ -2606,7 +2607,7 @@ export class et2_nextmatch extends et2_DOMWidget implements et2_IResizeable, et2 } else if(bool) { - filter = this.header._build_select(filter_name, 'select', + filter = this.header._build_select(filter_name, 'et2-select', this.settings[filter_name], this.settings[filter_name + '_no_lang']); } } @@ -3354,9 +3355,9 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext private action_header : JQuery; private search_box : JQuery; - private category : any; - private filter : et2_selectbox; - private filter2 : et2_selectbox; + private category : Et2Select | Et2SelectCategory; + private filter : Et2Select; + private filter2 : Et2Select; private right_div : JQuery; private count : JQuery; private count_total : JQuery; @@ -3520,7 +3521,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext { if(typeof settings.cat_id_label == 'undefined') settings.cat_id_label = ''; this.category = this._build_select('cat_id', settings.cat_is_select ? - 'select' : 'select-cat', settings.cat_id, settings.cat_is_select !== true, { + 'et2-select' : 'et2-select-cat', settings.cat_id, settings.cat_is_select !== true, { multiple: false, tags: true, class: "select-cat", @@ -3531,13 +3532,13 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Filter 1 if(!settings.no_filter) { - this.filter = this._build_select('filter', 'select', settings.filter, settings.filter_no_lang); + this.filter = this._build_select('filter', 'et2-select', settings.filter, settings.filter_no_lang); } // Filter 2 if(!settings.no_filter2) { - this.filter2 = this._build_select('filter2', 'select', settings.filter2, + this.filter2 = this._build_select('filter2', 'et2-select', settings.filter2, settings.filter2_no_lang, { multiple: false, tags: settings.filter2_tags, @@ -3704,7 +3705,7 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext * @param {string} lang * @param {object} extra */ - _build_select(name : string, type : string, value : string, lang : string | boolean, extra? : object) : et2_selectbox + _build_select(name : string, type : string, value : string, lang : string | boolean, extra? : object) : Et2Select { const widget_options = jQuery.extend({ "id": name, @@ -3716,13 +3717,9 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext // Set select options // Check in content for options- const mgr = this.nextmatch.getArrayMgr("content"); - let options = mgr.getEntry("options-" + name); - // Look in sel_options - if(!options) options = this.nextmatch.getArrayMgr("sel_options").getEntry(name); - // Check parent sel_options, because those are usually global and don't get passed down - if(!options) options = this.nextmatch.getArrayMgr("sel_options").getParentMgr()?.getEntry(name); + let options = false // Sometimes legacy stuff puts it in here - if(!options) options = mgr.getEntry('rows[sel_options][' + name + ']'); + options = mgr.getEntry('rows[sel_options][' + name + ']'); // Maybe in a row, and options got stuck in ${row} instead of top level const row_stuck = ['${row}', '{$row}']; @@ -3753,21 +3750,21 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext } // Create widget - const select = et2_createWidget(type, widget_options, this); + const select = loadWebComponent(type, widget_options, this); - if(options) select.set_select_options(options); + if(options) + { + select.select_options = options; + } // Set value select.set_value(value); // Set activeFilters to current value - this.nextmatch.activeFilters[select.id] = select.get_value(); - - // Set onChange - const input = select.input; + this.nextmatch.activeFilters[select.id] = select.value; // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - select.attributes.select_options.ignore = true; + //select.attributes.select_options.ignore = true; if(this.nextmatch.options.settings[name + "_onchange"]) { @@ -3782,18 +3779,23 @@ export class et2_nextmatch_header_bar extends et2_DOMWidget implements et2_INext } // Connect it to the onchange event of the input element - may submit - select.change = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); + select.onchange = et2_compileLegacyJS(onchange, this.nextmatch, select.getInputNode()); this._bindHeaderInput(select); } else // default request changed rows with new filters, previous this.form.submit() { - input.change(this.nextmatch, function(event) + select.addEventListener("change", () => { const set = {}; - set[name] = select.getValue(); - event.data.applyFilters(set); + set[select.id] = select.getValue(); + this.nextmatch.applyFilters(set); }); } + select.updateComplete.then(async() => + { + await select.updateComplete; + //select.syncValueFromItems(); + }) return select; } @@ -4236,8 +4238,8 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements { delete (field.values['']); } - widget = et2_createWidget( - field.type == 'select-account' ? 'nextmatch-accountfilter' : "nextmatch-filterheader", + widget = loadWebComponent( + field.type == 'select-account' ? 'et2-nextmatch-header-accountfilter' : "et2-nextmatch-header-filter", { id: cf_id, empty_label: field.label, @@ -4248,10 +4250,10 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements } else if(apps[field.type]) { - widget = et2_createWidget("nextmatch-entryheader", { + widget = loadWebComponent("et2-nextmatch-header-entry", { id: cf_id, only_app: field.type, - blur: field.label + placeholder: field.label }, this); } else @@ -4263,7 +4265,7 @@ export class et2_nextmatch_customfields extends et2_customfields_list implements } // If this is already attached, widget needs to be finished explicitly - if(this.isAttached() && !widget.isAttached()) + if(this.isAttached() && typeof widget.isAttached == "function" && !widget.isAttached()) { widget.loadingFinished(); } @@ -4435,395 +4437,4 @@ export class et2_nextmatch_sortheader extends et2_nextmatch_header implements et } -et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); - -/** - * Filter from a provided list of options - */ -export class et2_nextmatch_filterheader extends et2_selectbox implements et2_INextmatchHeader, et2_IResizeable -{ - private nextmatch : et2_nextmatch; - - /** - * Override to add change handler - */ - createInputWidget() - { - // Make sure there's an option for all - if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - super.createInputWidget(); - - jQuery(this.getInputNode()).change(this, function(event) - { - if(typeof event.data.nextmatch == 'undefined') - { - // Not fully set up yet - return; - } - const col_filter = {}; - col_filter[event.data.id] = event.data.input.val(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - event.data.set_value(event.data.input.val()); - - event.data.nextmatch.applyFilters({col_filter: col_filter}); - }); - - } - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) - { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - } - - // Make sure selectbox is not longer than the column - resize() - { - this.input.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); - } - -} - -et2_register_widget(et2_nextmatch_filterheader, ['nextmatch-filterheader']); - -/** - * Filter by account - */ -export class et2_nextmatch_accountfilterheader extends et2_selectAccount implements et2_INextmatchHeader, et2_IResizeable -{ - /** - * Override to add change handler - * - */ - createInputWidget() - { - // Make sure there's an option for all - if(!this.options.empty_label && !this.options.select_options[""]) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - super.createInputWidget(); - - this.input.change(this, function(event) - { - if(typeof event.data.nextmatch == 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[event.data.id] = event.data.getValue(); - event.data.nextmatch.applyFilters({col_filter: col_filter}); - }); - - } - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) - { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && this.nextmatch.activeFilters.col_filter[this.id]) - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - } - } - - // Make sure selectbox is not longer than the column - resize() - { - var max = jQuery(this.parentNode).innerWidth() - 4; - var surroundings = this.getSurroundings()._widgetSurroundings; - for(var i = 0; i < surroundings.length; i++) - { - max -= jQuery(surroundings[i]).outerWidth(); - } - this.input.css("max-width", max + "px"); - } - -} - -et2_register_widget(et2_nextmatch_accountfilterheader, ['nextmatch-accountfilter']); - -/** - * Filter allowing multiple values to be selected, base on a taglist instead - * of a regular selectbox - * - * @augments et2_taglist - */ -export class et2_nextmatch_taglistheader extends et2_taglist implements et2_INextmatchHeader, et2_IResizeable -{ - static readonly _attributes : any = { - autocomplete_url: {default: ''}, - multiple: {default: 'toggle'}, - onchange: { - // @ts-ignore - default: function(event) - { - if(typeof this.nextmatch === 'undefined') - { - // Not fully set up yet - return; - } - var col_filter = {}; - col_filter[this.id] = this.getValue(); - // Set value so it's there for response (otherwise it gets cleared if options are updated) - //event.data.set_value(event.data.input.val()); - - this.nextmatch.applyFilters({col_filter: col_filter}); - } - }, - rows: {default: 2}, - class: {default: 'nm_filterheader_taglist'} - }; - private nextmatch : et2_nextmatch; - - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_filterheader - */ - createInputWidget() - { - // Make sure there's an option for all - if(!this.options.empty_label && (!this.options.select_options || !this.options.select_options[""])) - { - this.options.empty_label = this.options.label ? this.options.label : egw.lang("All"); - } - super.createInputWidget(); - } - - /** - * Disable toggle if there are 2 or less options - * @param {Object[]} options - */ - set_select_options(options) - { - if(options && options.length <= 2 && this.options.multiple == 'toggle') - { - this.set_multiple(false); - } - super.set_select_options(options) - } - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) - { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.activeFilters.col_filter && typeof this.nextmatch.activeFilters.col_filter[this.id] != "undefined") - { - this.set_value(this.nextmatch.activeFilters.col_filter[this.id]); - - // Make sure it's set in the nextmatch - _nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - } - - // Make sure selectbox is not longer than the column - resize() - { - this.div.css("height", ''); - this.div.css("max-width", jQuery(this.parentNode).innerWidth() + "px"); - super.resize(); - } - -} - -et2_register_widget(et2_nextmatch_taglistheader, ['nextmatch-taglistheader']); - -/** - * Nextmatch filter that can filter for a selected entry - */ -export class et2_nextmatch_entryheader extends et2_link_entry implements et2_INextmatchHeader -{ - /** - * Override to add change handler - * - * @memberOf et2_nextmatch_entryheader - * @param {object} event - * @param {object} selected - */ - onchange(event, selected) - { - const col_filter = {}; - col_filter[this.id] = this.get_value(); - this.nextmatch.applyFilters.call(this.nextmatch, {col_filter: col_filter}); - } - - /** - * Override to always return a string appname:id (or just id) for simple (one real selection) - * cases, parent returns an object. If multiple are selected, or anything other than app and - * id, the original parent value is returned. - */ - getValue() - { - let value = super.getValue(); - if(typeof value == "object" && value != null) - { - if(!value.app || !value.id) return null; - - // If array with just one value, use a string instead for legacy server handling - if(typeof value.id == 'object' && value.id.shift && value.id.length == 1) - { - value.id = value.id.shift(); - } - // If simple value, format it legacy string style, otherwise - // we return full value - if(typeof value.id == 'string') - { - value = value.app + ":" + value.id; - } - } - return value; - } - - /** - * Set nextmatch is the function which has to be implemented for the - * et2_INextmatchHeader interface. - * - * @param {et2_nextmatch} _nextmatch - */ - setNextmatch(_nextmatch) - { - this.nextmatch = _nextmatch; - - // Set current filter value from nextmatch settings - if(this.nextmatch.options.settings.col_filter && this.nextmatch.options.settings.col_filter[this.id]) - { - this.set_value(this.nextmatch.options.settings.col_filter[this.id]); - - if(this.getValue() != this.nextmatch.activeFilters.col_filter[this.id]) - { - this.nextmatch.activeFilters.col_filter[this.id] = this.getValue(); - } - - // Tell framework to ignore, or it will reset it to ''/empty when it does loadingFinished() - this.attributes.value.ignore = true; - //this.attributes.select_options.ignore = true; - } - // Fire on lost focus, clear filter if user emptied box - } -} - -et2_register_widget(et2_nextmatch_entryheader, ['nextmatch-entryheader']); - -/** - * @augments et2_nextmatch_filterheader - */ -export class et2_nextmatch_customfilter extends et2_nextmatch_filterheader -{ - static readonly _attributes : any = { - "widget_type": { - "name": "Actual type", - "type": "string", - "description": "The actual type of widget you should use", - "no_lang": 1 - }, - "widget_options": { - "name": "Actual options", - "type": "any", - "description": "The options for the actual widget", - "no_lang": 1, - "default": {} - } - }; - public static readonly legacyOptions : ["widget_type", "widget_options"]; - - real_node : et2_selectbox; - - /** - * Constructor - * - * @param _parent - * @param _attrs - * @param _child - * @memberOf et2_nextmatch_customfilter - */ - constructor(_parent? : et2_widget, _attrs? : WidgetConfig, _child? : object) - { - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); - - switch(_attrs.widget_type) - { - case "link-entry": - _attrs.type = 'nextmatch-entryheader'; - break; - default: - if(_attrs.widget_type.indexOf('select') === 0) - { - _attrs.type = 'nextmatch-filterheader'; - } - else - { - _attrs.type = _attrs.widget_type; - } - } - jQuery.extend(_attrs.widget_options, {id: this.id}); - - _attrs.id = ''; - super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_nextmatch_customfilter._attributes, _child || {})); - - this.real_node = et2_createWidget(_attrs.type, _attrs.widget_options, this.getParent()); - const select_options = []; - const correct_type = _attrs.type; - this.real_node['type'] = _attrs.widget_type; - et2_selectbox.find_select_options(this.real_node, select_options, _attrs); - this.real_node["_type"] = correct_type; - if(typeof this.real_node.set_select_options === 'function') - { - this.real_node.set_select_options(select_options); - } - } - - // Just pass the real DOM node through, in case anybody asks - getDOMNode(_sender) - { - return this.real_node ? this.real_node.getDOMNode(_sender) : null; - } - - // Also need to pass through real children - getChildren() - { - return this.real_node.getChildren() || []; - } - - setNextmatch(_nextmatch : et2_nextmatch) - { - if(this.real_node && this.real_node.instanceOf(et2_INextmatchHeader)) - { - return (this.real_node).setNextmatch(_nextmatch); - } - } -} - -et2_register_widget(et2_nextmatch_customfilter, ['nextmatch-customfilter']); \ No newline at end of file +et2_register_widget(et2_nextmatch_sortheader, ['nextmatch-sortheader']); \ No newline at end of file diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 1d940595ef..11cc4a426a 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -49,6 +49,10 @@ import './Et2Link/Et2LinkList'; import './Et2Link/Et2LinkSearch'; import './Et2Link/Et2LinkString'; import './Et2Link/Et2LinkTo'; +import './Et2Nextmatch/Headers/AccountFilterHeader'; +import './Et2Nextmatch/Headers/CustomFilterHeader'; +import './Et2Nextmatch/Headers/EntryHeader'; +import './Et2Nextmatch/Headers/FilterHeader'; import './Et2Select/Et2Select'; import './Et2Select/Et2SelectAccount'; import './Et2Select/Et2SelectCategory'; diff --git a/api/src/Etemplate/Widget/Nextmatch/Customfilter.php b/api/src/Etemplate/Widget/Nextmatch/Customfilter.php index b48e4fb462..ddc3837363 100644 --- a/api/src/Etemplate/Widget/Nextmatch/Customfilter.php +++ b/api/src/Etemplate/Widget/Nextmatch/Customfilter.php @@ -34,7 +34,7 @@ class Customfilter extends Widget\Transformer switch($this->attrs['type']) { case "link-entry": - self::$transformation['type'] = $this->attrs['type'] = 'nextmatch-entryheader'; + self::$transformation['type'] = $this->attrs['type'] = 'et2-nextmatch-header-entry'; break; default: list($type) = explode('-',$this->attrs['type']); @@ -44,7 +44,7 @@ class Customfilter extends Widget\Transformer { $widget_type = $this->attrs['type']; } - $this->attrs['type'] = 'nextmatch-filterheader'; + $this->attrs['type'] = 'et2-nextmatch-header-custom'; } self::$transformation['type'] = $this->attrs['type']; } diff --git a/api/templates/default/etemplate2.css b/api/templates/default/etemplate2.css index 4d3086da40..ee9370a153 100644 --- a/api/templates/default/etemplate2.css +++ b/api/templates/default/etemplate2.css @@ -2188,7 +2188,9 @@ div.message.floating, lion-validation-feedback[type] { } .et2_nextmatch .nextmatch_header_row > div { - display: inline-block; + display: inline-flex; + flex-direction: row; + gap: 1ex; } /* Firefox only search clear button */