mirror of
https://github.com/EGroupware/egroupware.git
synced 2025-03-02 17:21:24 +01:00
Api: Et2Dialog automatic tests & some documentation
This commit is contained in:
parent
1980251ac9
commit
b7a12136ec
258
api/js/etemplate/Et2Dialog/Et2Dialog.md
Normal file
258
api/js/etemplate/Et2Dialog/Et2Dialog.md
Normal file
@ -0,0 +1,258 @@
|
||||
```html:preview
|
||||
<et2-dialog title="Dialog" class="dialog-overview" buttons="0">
|
||||
This is the dialog
|
||||
</et2-dialog>
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
<script>
|
||||
const dialog = document.querySelector(".dialog-overview");
|
||||
const button = dialog.nextElementSibling;
|
||||
|
||||
// Our dialogs always open on their own, not so good for docs
|
||||
dialog.open=false;
|
||||
|
||||
button.addEventListener('click', () => dialog.show());
|
||||
</script>
|
||||
```
|
||||
|
||||
While most widgets are expected to be used via .XET files, Et2Dialog is primarily used via javascript, and usually with
|
||||
`Et2Dialog.show_dialog()`.
|
||||
Et2Dialog extends [SlDialog](https://shoelace.style/components/dialog).
|
||||
|
||||
```js
|
||||
// All parameters are optional
|
||||
const dialog = Et2Dialog.show_dialog(
|
||||
/* callback (button, value) => {} or null if you're using the promise*/ null,
|
||||
/* Message */ "Would you like to do the thing?",
|
||||
/* Title */ "Dialog title",
|
||||
/* Value */ {/* Passed to callback */},
|
||||
/* Buttons */ Et2Dialog.BUTTONS_OK_CANCEL
|
||||
);
|
||||
|
||||
// Wait for user
|
||||
let [button, value] = await dialog.getComplete();
|
||||
// Do stuff
|
||||
|
||||
// or
|
||||
dialog.getComplete().then(([button, value]) =>
|
||||
{
|
||||
// Do stuff
|
||||
});
|
||||
```
|
||||
|
||||
In your callback or after the `getComplete()` Promise resolves, you should check which button was pressed.
|
||||
|
||||
```js
|
||||
let callback = function (button_id)
|
||||
{
|
||||
if (button_id == Et2Dialog.YES_BUTTON)
|
||||
{
|
||||
// Do stuff
|
||||
}
|
||||
else if (button_id == Et2Dialog.NO_BUTTON)
|
||||
{
|
||||
// Other stuff
|
||||
}
|
||||
else if (button_id == Et2Dialog.CANCEL_BUTTON)
|
||||
{
|
||||
// Abort
|
||||
}
|
||||
};
|
||||
dialog = Et2Dialog.show_dialog(
|
||||
callback, "Erase the entire database?", "Break things", {}, // value
|
||||
Et2Dialog.BUTTONS_YES_NO_CANCEL, Et2Dialog.WARNING_MESSAGE
|
||||
);
|
||||
```
|
||||
|
||||
The parameters for the Et2Dialog.show_dialog() are all optional.
|
||||
|
||||
- callback - function called when the dialog closes, or false/null.
|
||||
The ID of the button will be passed. Button ID will be one of the Et2Dialog.*_BUTTON constants.
|
||||
The callback is _not_ called if the user closes the dialog with the X in the corner, or presses ESC.
|
||||
- message - (plain) text to display
|
||||
- title - Dialog title
|
||||
- value (for prompt)
|
||||
- buttons - Et2Dialog BUTTONS_* constant, or an array of button settings. Use DialogButton interface.
|
||||
- dialog_type - Et2Dialog *_MESSAGE constant
|
||||
- icon - name of icon
|
||||
|
||||
Note that these methods will _not_ block program flow while waiting for user input.
|
||||
|
||||
## Examples
|
||||
|
||||
### Pre-defined dialogs
|
||||
|
||||
We have several pre-defined dialogs that can be easily used from javascript for specific purposes.
|
||||
`Et2Dialog.alert(message, title)`, `Et2Dialog.prompt(message, title)` and `Et2Dialog.confirm(message, title)`
|
||||
|
||||
```html:preview
|
||||
<et2-hbox>
|
||||
<et2-button class="alert">Alert</et2-button>
|
||||
<et2-button class="prompt">Prompt</et2-button>
|
||||
<et2-button class="confirm">Confirm</et2-button>
|
||||
</et2-hbox>
|
||||
<script>
|
||||
const alertButton = document.querySelector(".alert");
|
||||
alertButton.addEventListener("click", () => {
|
||||
Et2Dialog.alert("Alert dialog message", "Alert title");
|
||||
});
|
||||
|
||||
const promptButton = document.querySelector(".prompt");
|
||||
promptButton.addEventListener("click", () => {
|
||||
Et2Dialog.show_prompt((button, value) => {
|
||||
Et2Dialog.alert("Button: " + button+ " You entered " + value, "Prompt value");
|
||||
},
|
||||
"Please enter your name", "Prompt dialog"
|
||||
);});
|
||||
|
||||
const confirmButton = document.querySelector(".confirm");
|
||||
confirmButton.addEventListener("click", () => {
|
||||
Et2Dialog.confirm(/* senders? */null, "Are you sure you want to delete this?", "Confirm title");
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Template
|
||||
|
||||
You can define a dialog inside your template, and use it as needed in your app:
|
||||
|
||||
```xml
|
||||
|
||||
<template id="dialog_example">
|
||||
<!-- The rest of the application template goes here -->
|
||||
<!-- destroyOnClose="false" because we intend to re-use the dialog -->
|
||||
<et2-dialog id="change_owner" destroyOnClose="false" buttons="1">
|
||||
<et2-select-account id="new_owner" label="New owner"></et2-select-account>
|
||||
<!-- Anything can go here -->
|
||||
</et2-dialog>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts
|
||||
async function changeOwner(entry : { owner : number })
|
||||
{
|
||||
const dialog = document.querySelector("#change_owner");
|
||||
dialog.show();
|
||||
|
||||
// Wait for answer
|
||||
let [button, value] = await dialog.getComplete();
|
||||
if(button)
|
||||
{
|
||||
entry.owner = value.new_owner;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or more commonly, load a template inside the dialog with the `template` attribute:
|
||||
|
||||
```xml
|
||||
|
||||
<template id="dialog_contents">
|
||||
<et2-select-account id="owner" label="Set owner"></et2-select-account>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts
|
||||
async function changeOwner(entry : { owner : number })
|
||||
{
|
||||
// Pass egw in the constructor
|
||||
let dialog = new Et2Dialog(this.egw);
|
||||
dialog.transformAttributes({
|
||||
template: "my_app/templates/default/dialog_contents.xet",
|
||||
value: {owner: entry.owner}
|
||||
});
|
||||
|
||||
// Add to DOM, dialog will auto-open
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
// Wait for answer
|
||||
let [button, value] = await dialog.getComplete();
|
||||
if(button)
|
||||
{
|
||||
entry.owner = value.new_owner;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Buttons
|
||||
|
||||
The easiest way to put buttons on the dialog is to use one of the button constants: `Et2Dialog.BUTTONS_OK`,
|
||||
`Et2Dialog.BUTTONS_OK_CANCEL`, `Et2Dialog.BUTTONS_YES_NO`, `Et2Dialog.BUTTONS_YES_NO_CANCEL`. This also ensures
|
||||
consistancy across all dialogs.
|
||||
|
||||
```html:preview
|
||||
<et2-hbox class="button-constants">
|
||||
<et2-button class="OK">BUTTONS_OK</et2-button>
|
||||
<et2-button class="OK_CANCEL">BUTTONS_OK_CANCEL</et2-button>
|
||||
<et2-button class="YES_NO">BUTTONS_YES_NO</et2-button>
|
||||
<et2-button class="YES_NO_CANCEL">BUTTONS_YES_NO_CANCEL</et2-button>
|
||||
</et2-hbox>
|
||||
<script>
|
||||
const buttonBox = document.querySelector(".button-constants");
|
||||
Array.from(buttonBox.children).forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
Et2Dialog.show_dialog(null, button.textContent.trim() + " = " + Et2Dialog[button.textContent.trim()], "Button constant", null, Et2Dialog[button.textContent.trim()]);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Custom buttons
|
||||
|
||||
Sometimes the pre-defined buttons are insufficient. You can provide your own list of buttons, following the
|
||||
`DialogButton` interface.
|
||||
|
||||
```html:preview
|
||||
<et2-button class="custom-buttons">Custom buttons</et2-button>
|
||||
<script>
|
||||
const button = document.querySelector(".custom-buttons");
|
||||
const customButtons /* : DialogButton[] */ = [
|
||||
// These buttons will use the callback or getComplete() Promise, just like normal.
|
||||
{label: "OK", id: "OK", default: true},
|
||||
{label: "Yes", id: "Yes"},
|
||||
{label: "Sure", id: "Sure", disabled: true},
|
||||
{label: "Maybe", click: function() {
|
||||
// If you override the click handler, 'this' will be the dialog.
|
||||
// Things get more complicated, so doing this is not recommended
|
||||
}
|
||||
},
|
||||
{label: "Negative choice", id:"No", align: "right"}
|
||||
];
|
||||
button.addEventListener("click", () => {
|
||||
let dialog = Et2Dialog.show_dialog(null, "Custom buttons", "Custom buttons", null, customButtons);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```ts
|
||||
// Pass egw in the constructor
|
||||
let dialog = new Et2Dialog(my_egw_reference);
|
||||
|
||||
// Set attributes. They can be set in any way, but this is convenient.
|
||||
dialog.transformAttributes({
|
||||
// If you use a template, the second parameter will be the value of the template, as if it were submitted.
|
||||
callback: function(button_id, value) {...}, // return false to prevent dialog closing
|
||||
buttons: [
|
||||
// These ones will use the callback, just like normal. Use DialogButton interface.
|
||||
{label: egw.lang("OK"), id: "OK", default: true},
|
||||
{label: egw.lang("Yes"), id: "Yes"},
|
||||
{label: egw.lang("Sure"), id: "Sure"},
|
||||
{
|
||||
label: egw.lang("Maybe"), click: function()
|
||||
{
|
||||
// If you override, 'this' will be the dialog DOMNode.
|
||||
// Things get more complicated.
|
||||
// Do what you like here
|
||||
}
|
||||
},
|
||||
|
||||
],
|
||||
title: 'Why would you want to do this?',
|
||||
template: "/egroupware/addressbook/templates/default/edit.xet",
|
||||
value: {content: {...default values}, sel_options: {...}...}
|
||||
});
|
||||
// Add to DOM, dialog will auto-open
|
||||
document.body.appendChild(dialog);
|
||||
// If you want, wait for close
|
||||
let result = await dialog.getComplete();
|
||||
```
|
@ -15,7 +15,6 @@ import {classMap} from "lit/directives/class-map.js";
|
||||
import {ifDefined} from "lit/directives/if-defined.js";
|
||||
import {repeat} from "lit/directives/repeat.js";
|
||||
import {styleMap} from "lit/directives/style-map.js";
|
||||
import {et2_template} from "../et2_widget_template";
|
||||
import type {etemplate2} from "../etemplate2";
|
||||
import {egw, IegwAppLocal} from "../../jsapi/egw_global";
|
||||
import interact from "@interactjs/interactjs";
|
||||
@ -25,6 +24,7 @@ import shoelace from "../Styles/shoelace";
|
||||
import {SlDialog} from "@shoelace-style/shoelace";
|
||||
import {egwIsMobile} from "../../egw_action/egw_action_common";
|
||||
import {waitForEvent} from "../Et2Widget/event";
|
||||
import {property} from "lit/decorators/property.js";
|
||||
|
||||
export interface DialogButton
|
||||
{
|
||||
@ -40,90 +40,14 @@ export interface DialogButton
|
||||
/**
|
||||
* A common dialog widget that makes it easy to inform users or prompt for information.
|
||||
*
|
||||
* It is possible to have a custom dialog by using a template, but you can also use
|
||||
* the static method Et2Dialog.show_dialog(). At its simplest, you can just use:
|
||||
* ```ts
|
||||
* Et2Dialog.show_dialog(false, "Operation completed");
|
||||
* ```
|
||||
* @slot - The dialog's main content
|
||||
* @slot label - The dialog's title. Alternatively, you can use the title attribute.
|
||||
* @slot header-actions - Optional actions to add to the header. Works best with <et2-button-icon>
|
||||
* @slot footer - The dialog's footer, where we put the buttons.
|
||||
*
|
||||
* Or a more complete example:
|
||||
* ```js
|
||||
* let callback = function (button_id)
|
||||
* {
|
||||
* if(button_id == Et2Dialog.YES_BUTTON)
|
||||
* {
|
||||
* // Do stuff
|
||||
* }
|
||||
* else if (button_id == Et2Dialog.NO_BUTTON)
|
||||
* {
|
||||
* // Other stuff
|
||||
* }
|
||||
* else if (button_id == Et2Dialog.CANCEL_BUTTON)
|
||||
* {
|
||||
* // Abort
|
||||
* }
|
||||
* }.
|
||||
* let dialog = Et2Dialog.show_dialog(
|
||||
* callback, "Erase the entire database?","Break things", {} // value
|
||||
* et2_dialog.BUTTONS_YES_NO_CANCEL, et2_dialog.WARNING_MESSAGE
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* Or, using Promises instead of a callback:
|
||||
* ```ts
|
||||
* let result = await Et2Dialog.show_prompt(null, "Name").getComplete();
|
||||
* if(result.button_id == Et2Dialog.OK_BUTTON)
|
||||
* {
|
||||
* // Do stuff with result.value
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The parameters for the above are all optional, except callback (which can be null) and message:
|
||||
* - callback - function called when the dialog closes, or false/null.
|
||||
* The ID of the button will be passed. Button ID will be one of the Et2Dialog.*_BUTTON constants.
|
||||
* The callback is _not_ called if the user closes the dialog with the X in the corner, or presses ESC.
|
||||
* - message - (plain) text to display
|
||||
* - title - Dialog title
|
||||
* - value (for prompt)
|
||||
* - buttons - Et2Dialog BUTTONS_* constant, or an array of button settings. Use DialogButton interface.
|
||||
* - dialog_type - Et2Dialog *_MESSAGE constant
|
||||
* - icon - URL of icon
|
||||
*
|
||||
* Note that these methods will _not_ block program flow while waiting for user input unless you use "await" on getComplete().
|
||||
* The user's input will be provided to the callback.
|
||||
*
|
||||
* You can also create a custom dialog using an etemplate, even setting all the buttons yourself.
|
||||
* ```ts
|
||||
* // Pass egw in the constructor
|
||||
* let dialog = new Et2Dialog(my_egw_reference);
|
||||
*
|
||||
* // Set attributes. They can be set in any way, but this is convenient.
|
||||
* dialog.transformAttributes({
|
||||
* // If you use a template, the second parameter will be the value of the template, as if it were submitted.
|
||||
* callback: function(button_id, value) {...}, // return false to prevent dialog closing
|
||||
* buttons: [
|
||||
* // These ones will use the callback, just like normal. Use DialogButton interface.
|
||||
* {label: egw.lang("OK"),id:"OK", default: true},
|
||||
* {label: egw.lang("Yes"),id:"Yes"},
|
||||
* {label: egw.lang("Sure"),id:"Sure"},
|
||||
* {label: egw.lang("Maybe"),click: function() {
|
||||
* // If you override, 'this' will be the dialog DOMNode.
|
||||
* // Things get more complicated.
|
||||
* // Do what you like here
|
||||
* }},
|
||||
*
|
||||
* ],
|
||||
* title: 'Why would you want to do this?',
|
||||
* template:"/egroupware/addressbook/templates/default/edit.xet",
|
||||
* value: { content: {...default values}, sel_options: {...}...}
|
||||
* });
|
||||
* // Add to DOM, dialog will auto-open
|
||||
* document.body.appendChild(dialog);
|
||||
* // If you want, wait for close
|
||||
* let result = await dialog.getComplete();
|
||||
*```
|
||||
*
|
||||
* Customize initial focus by setting the "autofocus" attribute on a control, otherwise first input will have focus
|
||||
* @event open - Emitted when the dialog opens
|
||||
* @event close - Emitted when the dialog closes
|
||||
* @event before-load - Emitted before the dialog opens
|
||||
*/
|
||||
export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
{
|
||||
@ -144,6 +68,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
* @protected
|
||||
* @internal
|
||||
*/
|
||||
private __template : string; // Name
|
||||
protected _template_widget : etemplate2 | null;
|
||||
protected _template_promise : Promise<boolean>;
|
||||
|
||||
@ -274,64 +199,72 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
];
|
||||
}
|
||||
|
||||
static get properties()
|
||||
{
|
||||
return {
|
||||
...super.properties,
|
||||
callback: Function,
|
||||
/**
|
||||
* Function called when the dialog is closed
|
||||
*
|
||||
* Wait for dialog.getComplete() instead
|
||||
*/
|
||||
@property({type: Function})
|
||||
callback : Function;
|
||||
|
||||
/**
|
||||
* Allow other controls to be accessed while the dialog is visible
|
||||
* while not conflicting with internal attribute
|
||||
*/
|
||||
isModal: {type: Boolean, reflect: true},
|
||||
/**
|
||||
* Allow other controls to be accessed while the dialog is visible
|
||||
* while not conflicting with internal attribute
|
||||
*/
|
||||
@property({type: Boolean, reflect: true})
|
||||
isModal : boolean;
|
||||
|
||||
/**
|
||||
* Title for the dialog, goes in the header
|
||||
*/
|
||||
title: String,
|
||||
/**
|
||||
* Pre-defined group of buttons, one of the BUTTONS_*
|
||||
*/
|
||||
@property({type: Number})
|
||||
buttons : Number;
|
||||
|
||||
/**
|
||||
* Pre-defined group of buttons, one of the BUTTONS_*
|
||||
*/
|
||||
buttons: Number,
|
||||
// Force size on the dialog. Normally it sizes to content.
|
||||
@property({type: Number})
|
||||
width : number;
|
||||
// Force size on the dialog. Normally it sizes to content.
|
||||
@property({type: Number})
|
||||
height : number;
|
||||
|
||||
/**
|
||||
* Instead of a message, show this template file instead
|
||||
*/
|
||||
template: String,
|
||||
/**
|
||||
* Message to show to user
|
||||
*/
|
||||
@property({type: String})
|
||||
message : string;
|
||||
|
||||
// Force size on the dialog. Normally it sizes to content.
|
||||
width: Number,
|
||||
height: Number,
|
||||
/**
|
||||
* Pre-defined dialog styles
|
||||
*/
|
||||
@property({type: Number})
|
||||
dialog_type : number;
|
||||
|
||||
// We just pass these on to Et2DialogContent
|
||||
message: String,
|
||||
dialog_type: Number,
|
||||
icon: String,
|
||||
value: Object,
|
||||
/**
|
||||
* Include an icon on the dialog
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
@property({type: String})
|
||||
icon : string;
|
||||
|
||||
/**
|
||||
* Automatically destroy the dialog when it closes. Set to false to keep the dialog around.
|
||||
*/
|
||||
destroyOnClose: Boolean,
|
||||
/**
|
||||
* Automatically destroy the dialog when it closes. Set to false to keep the dialog around.
|
||||
*/
|
||||
@property({type: Boolean})
|
||||
destroyOnClose : boolean;
|
||||
|
||||
/**
|
||||
* Legacy-option for appending dialog into a specific dom node
|
||||
*/
|
||||
appendTo: String,
|
||||
/**
|
||||
* When it's set to false dialog won't get closed by hitting Esc
|
||||
*/
|
||||
@property({type: Boolean})
|
||||
hideOnEscape : boolean;
|
||||
|
||||
/**
|
||||
* When it's set to false dialog won't get closed by hitting Esc
|
||||
*/
|
||||
hideOnEscape: Boolean,
|
||||
/**
|
||||
* When set to true it removes the close button from dialog's header
|
||||
*/
|
||||
@property({type: Boolean, reflect: true})
|
||||
noCloseButton : boolean;
|
||||
|
||||
/**
|
||||
* When set to true it removes the close button from dialog's header
|
||||
*/
|
||||
noCloseButton: {type: Boolean, reflect: true}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* List of properties that get translated
|
||||
@ -456,9 +389,9 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
|
||||
destroy()
|
||||
{
|
||||
if(this.template)
|
||||
if(this._template_widget)
|
||||
{
|
||||
this.template.clear(true);
|
||||
this._template_widget.clear(true);
|
||||
}
|
||||
this.remove();
|
||||
}
|
||||
@ -476,7 +409,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
|
||||
addOpenListeners()
|
||||
{
|
||||
//super.addOpenListeners();
|
||||
super.addOpenListeners();
|
||||
|
||||
// Bind on the ancestor, not the buttons, so their click handler gets a chance to run
|
||||
this.addEventListener("click", this._onButtonClick);
|
||||
@ -485,7 +418,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
|
||||
removeOpenListeners()
|
||||
{
|
||||
//super.removeOpenListeners();
|
||||
super.removeOpenListeners();
|
||||
this.removeEventListener("click", this._onButtonClick);
|
||||
this.removeEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
@ -707,12 +640,17 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set value(new_value)
|
||||
@property({type: Object})
|
||||
set value(new_value : Object)
|
||||
{
|
||||
this.__value = new_value;
|
||||
}
|
||||
|
||||
set template(new_template_name)
|
||||
/**
|
||||
* Instead of a simple message, show this template file instead
|
||||
*/
|
||||
@property({type: String})
|
||||
set template(new_template_name : string)
|
||||
{
|
||||
let old_template = this.__template;
|
||||
this.__template = new_template_name;
|
||||
@ -732,16 +670,20 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
get template()
|
||||
{
|
||||
// Can't return undefined or requestUpdate() will not notice a change
|
||||
return this._template_widget || null;
|
||||
return this.__template || null;
|
||||
}
|
||||
|
||||
get title() : string { return this.label }
|
||||
|
||||
/**
|
||||
* Title for the dialog, goes in the header
|
||||
*/
|
||||
@property()
|
||||
set title(new_title : string)
|
||||
{
|
||||
this.label = new_title;
|
||||
}
|
||||
|
||||
get title() : string { return this.label }
|
||||
|
||||
updated(changedProperties)
|
||||
{
|
||||
super.updated(changedProperties);
|
||||
@ -1004,28 +946,35 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if(this.template)
|
||||
if(this._template_widget && typeof this._template_widget.focusOnFirstInput == "function")
|
||||
{
|
||||
this.template.focusOnFirstInput();
|
||||
this._template_widget.focusOnFirstInput();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a template, but maybe something?
|
||||
const $input = jQuery('input:visible,et2-textbox:visible,et2-select-email:visible', this)
|
||||
// Date fields open the calendar popup on focus
|
||||
.not('.et2_date')
|
||||
.filter(function()
|
||||
const input = Array.from(this.querySelectorAll('input,et2-textbox,et2-select-email')).filter(element =>
|
||||
{
|
||||
// Skip invisible
|
||||
if(!element.checkVisibility())
|
||||
{
|
||||
// Skip inputs that are out of tab ordering
|
||||
const $this = jQuery(this);
|
||||
return !$this.attr('tabindex') || parseInt($this.attr('tabIndex')) >= 0;
|
||||
}).first();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Date fields open the calendar popup on focus
|
||||
if(element.classList.contains("et2_date"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Skip inputs that are out of tab ordering
|
||||
return !element.hasAttribute('tabindex') || parseInt(element.getAttribute('tabIndex')) >= 0
|
||||
}).pop();
|
||||
|
||||
// mobile device, focus only if the field is empty (usually means new entry)
|
||||
// should focus always for non-mobile one
|
||||
if(egwIsMobile() && $input.val() == "" || !egwIsMobile())
|
||||
if(input && (egwIsMobile() && typeof input.getValue == "function" && input.getValue() == "" || !egwIsMobile()))
|
||||
{
|
||||
$input.focus();
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1118,7 +1067,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
/**
|
||||
* Show a confirmation dialog
|
||||
*
|
||||
* @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in.
|
||||
* @param {function} _callback Function called when the user clicks a button. The context will be the Et2Dialog widget, and the button constant is passed in.
|
||||
* @param {string} _message Message to be place in the dialog.
|
||||
* @param {string} _title Text in the top bar of the dialog.
|
||||
* @param _value passed unchanged to callback as 2. parameter
|
||||
@ -1277,7 +1226,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
* check to avoid executing more than intended.
|
||||
*
|
||||
* @param {function} _callback Function called when the user clicks a button,
|
||||
* or when the list is done processing. The context will be the et2_dialog
|
||||
* or when the list is done processing. The context will be the Et2Dialog
|
||||
* widget, and the button constant is passed in.
|
||||
* @param {string} _message Message to be place in the dialog. Usually just
|
||||
* text, but DOM nodes will work too.
|
||||
@ -1289,7 +1238,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
* address. Multiple parameters are allowed, in an array.
|
||||
* @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for
|
||||
*
|
||||
* @return {et2_dialog}
|
||||
* @return {Et2Dialog}
|
||||
*/
|
||||
static long_task(_callback, _message, _title, _menuaction, _list, _egw_or_appname)
|
||||
{
|
||||
@ -1302,7 +1251,11 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
{
|
||||
// Cancel run
|
||||
cancel = true;
|
||||
jQuery("button[button_id=" + Et2Dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable");
|
||||
let button = <Et2Button>dialog.querySelector("button[button_id=" + Et2Dialog.CANCEL_BUTTON + "]");
|
||||
if(button)
|
||||
{
|
||||
button.disabled = true;
|
||||
}
|
||||
updateUi.call(_list.length, '');
|
||||
}
|
||||
}
|
||||
@ -1535,8 +1488,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
|
||||
|
||||
//@ts-ignore TS doesn't recognize Et2Dialog as HTMLEntry
|
||||
customElements.define("et2-dialog", Et2Dialog);
|
||||
// make et2_dialog publicly available as we need to call it from templates
|
||||
// make Et2Dialog publicly available as we need to call it from templates
|
||||
{
|
||||
window['et2_dialog'] = Et2Dialog;
|
||||
window['Et2Dialog'] = Et2Dialog;
|
||||
}
|
140
api/js/etemplate/Et2Dialog/test/Et2Dialog.test.ts
Normal file
140
api/js/etemplate/Et2Dialog/test/Et2Dialog.test.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import {assert, elementUpdated, expect, fixture, html, oneEvent} from '@open-wc/testing';
|
||||
import {sendKeys} from "@web/test-runner-commands";
|
||||
import * as sinon from 'sinon';
|
||||
import {Et2Dialog} from "../Et2Dialog";
|
||||
|
||||
/**
|
||||
* Test file for Etemplate webComponent Et2Dialog
|
||||
*
|
||||
* In here we test just the simple, basic widget stuff.
|
||||
*/
|
||||
// Stub global egw for egw_action to find
|
||||
const egw = {
|
||||
ajaxUrl: () => "",
|
||||
app: () => "addressbook",
|
||||
app_name: () => "addressbook",
|
||||
decodePath: (_path : string) => _path,
|
||||
image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=",
|
||||
jsonq: () => Promise.resolve({}),
|
||||
lang: i => i + "*",
|
||||
link: i => i,
|
||||
preference: i => "",
|
||||
tooltipUnbind: () => {},
|
||||
webserverUrl: ""
|
||||
}
|
||||
window.egw = function() {return egw};
|
||||
Object.assign(window.egw, egw);
|
||||
|
||||
let element : Et2Dialog;
|
||||
|
||||
async function before()
|
||||
{
|
||||
// Create an element to test with, and wait until it's ready
|
||||
// @ts-ignore
|
||||
element = await fixture<Et2Dialog>(html`
|
||||
<et2-dialog title="I'm a dialog">
|
||||
</et2-dialog>
|
||||
`);
|
||||
|
||||
// Stub egw()
|
||||
sinon.stub(element, "egw").returns(window.egw);
|
||||
await elementUpdated(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
describe("Dialog widget basics", () =>
|
||||
{
|
||||
// Setup run before each test
|
||||
beforeEach(before);
|
||||
|
||||
// Make sure it works
|
||||
it('is defined', () =>
|
||||
{
|
||||
assert.instanceOf(element, Et2Dialog);
|
||||
});
|
||||
|
||||
it('has a title', async() =>
|
||||
{
|
||||
element.title = "Title set";
|
||||
await elementUpdated(element);
|
||||
|
||||
assert.equal(element.shadowRoot.querySelector("#title").textContent.trim(), "Title set");
|
||||
});
|
||||
});
|
||||
describe("Properties", async() =>
|
||||
{
|
||||
// Setup run before each test
|
||||
beforeEach(before);
|
||||
|
||||
it("destroyOnClose = true", async() =>
|
||||
{
|
||||
element.destroyOnClose = true;
|
||||
await element.show();
|
||||
assert.isNotNull(document.querySelector("et2-dialog"));
|
||||
await element.hide();
|
||||
|
||||
assert.isNull(document.querySelector("et2-dialog"));
|
||||
});
|
||||
it("destroyOnClose = false", async() =>
|
||||
{
|
||||
element.destroyOnClose = false;
|
||||
await element.show();
|
||||
assert.isNotNull(document.querySelector("et2-dialog"));
|
||||
|
||||
await element.hide();
|
||||
assert.isNotNull(document.querySelector("et2-dialog"));
|
||||
});
|
||||
it("noCloseButton", async() =>
|
||||
{
|
||||
await element.show();
|
||||
const closeButton = element.shadowRoot.querySelector("[part=close-button]");
|
||||
assert.isNotNull(closeButton);
|
||||
assert.isTrue(closeButton.checkVisibility());
|
||||
|
||||
element.noCloseButton = true;
|
||||
await element.show();
|
||||
|
||||
assert.isFalse(closeButton.checkVisibility());
|
||||
});
|
||||
it("hideOnEscape = true", async() =>
|
||||
{
|
||||
element.hideOnEscape = true;
|
||||
|
||||
await element.show();
|
||||
const listener = oneEvent(element, "close");
|
||||
|
||||
await sendKeys({down: "Escape"});
|
||||
const event = await listener;
|
||||
expect(event).to.exist;
|
||||
});
|
||||
it("hideOnEscape = false", (done) =>
|
||||
{
|
||||
element.hideOnEscape = false;
|
||||
|
||||
element.show().then(async() =>
|
||||
{
|
||||
// Listen for events
|
||||
const requestCloseListener = oneEvent(element, "sl-request-close");
|
||||
const closeListener = oneEvent(element, "close");
|
||||
|
||||
let event = null;
|
||||
|
||||
// Press Escape
|
||||
let keysSender = await sendKeys({down: "Escape"});
|
||||
|
||||
// Request close gets sent, but Et2Dialog cancels it if hideOnEscape=false
|
||||
await requestCloseListener;
|
||||
|
||||
// Can't really test that an event didn't happen
|
||||
setTimeout(() =>
|
||||
{
|
||||
assert.isNull(event, "Close happened");
|
||||
done();
|
||||
}, 500)
|
||||
|
||||
event = await closeListener;
|
||||
return requestCloseListener;
|
||||
});
|
||||
});
|
||||
});
|
@ -1445,6 +1445,7 @@ const Et2WidgetMixin = <T extends Constructor>(superClass : T) =>
|
||||
// These methods are used inside widgets, but may not always be available depending on egw() loading (tests, docs)
|
||||
const required = {
|
||||
debug: () => {console.log(arguments);},
|
||||
image: () => "",
|
||||
lang: (l) => {return l;},
|
||||
preference: () => {return false;},
|
||||
};
|
||||
|
28
package-lock.json
generated
28
package-lock.json
generated
@ -42,6 +42,7 @@
|
||||
"@web/dev-server-esbuild": "^1.0.2",
|
||||
"@web/dev-server-rollup": "^0.6.3",
|
||||
"@web/test-runner": "^0.18.2",
|
||||
"@web/test-runner-commands": "^0.9.0",
|
||||
"@web/test-runner-playwright": "^0.11.0",
|
||||
"browser-sync": "^3.0.2",
|
||||
"cem": "^1.0.4",
|
||||
@ -4179,13 +4180,6 @@
|
||||
"integrity": "sha512-ARATsLdrGPUnaBvxLhUlnltcMgn7pQG312S8ccdYlnyijabrX9RN/KN/iGj9Am96CoW8e/K9628BA7Bv4XHdrA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@ -4198,17 +4192,6 @@
|
||||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
|
||||
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||
@ -6907,13 +6890,6 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/custom-element-jet-brains-integration": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/custom-element-jet-brains-integration/-/custom-element-jet-brains-integration-1.2.1.tgz",
|
||||
@ -13287,7 +13263,7 @@
|
||||
"version": "2.79.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
|
@ -30,6 +30,7 @@
|
||||
"@web/dev-server-esbuild": "^1.0.2",
|
||||
"@web/dev-server-rollup": "^0.6.3",
|
||||
"@web/test-runner": "^0.18.2",
|
||||
"@web/test-runner-commands": "^0.9.0",
|
||||
"@web/test-runner-playwright": "^0.11.0",
|
||||
"browser-sync": "^3.0.2",
|
||||
"cem": "^1.0.4",
|
||||
|
Loading…
Reference in New Issue
Block a user