mirror of
https://github.com/flarum/core.git
synced 2025-08-02 14:37:49 +02:00
chore: rewrite ModalManager and state to Typescript (#3007)
* Rewrite ModalManagerState into Typescript - Fixes `attrs` parameter being marked as required - Add `isModalOpen` method * Rewrite ModalManager into Typescript * Fix incorrect type * Continue modal rewrite * Update attr typings * Fix correctly cast `this.constructor` calls * Cast to bool * Don't extend ModalAttrs by Record * Prevent missing abstract methods in child Modals from bricking frontend * Add missing `app` import * Address review comment Co-authored-by: David Sevilla Martin <6401250+datitisev@users.noreply.github.com> Co-authored-by: David Sevilla Martin <6401250+datitisev@users.noreply.github.com>
This commit is contained in:
@@ -138,13 +138,13 @@ export default abstract class Component<Attrs extends ComponentAttrs = Component
|
|||||||
if ('children' in attrs) {
|
if ('children' in attrs) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`[${
|
`[${
|
||||||
(this.constructor as any).name
|
(this.constructor as typeof Component).name
|
||||||
}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`
|
}] The "children" attribute of attrs should never be used. Either pass children in as the vnode children or rename the attribute`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('tag' in attrs) {
|
if ('tag' in attrs) {
|
||||||
throw new Error(`[${(this.constructor as any).name}] You cannot use the "tag" attribute name with Mithril 2.`);
|
throw new Error(`[${(this.constructor as typeof Component).name}] You cannot use the "tag" attribute name with Mithril 2.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,149 +0,0 @@
|
|||||||
import Component from '../Component';
|
|
||||||
import Alert from './Alert';
|
|
||||||
import Button from './Button';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The `Modal` component displays a modal dialog, wrapped in a form. Subclasses
|
|
||||||
* should implement the `className`, `title`, and `content` methods.
|
|
||||||
*
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
export default class Modal extends Component {
|
|
||||||
/**
|
|
||||||
* Determine whether or not the modal should be dismissible via an 'x' button.
|
|
||||||
*/
|
|
||||||
static isDismissible = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attributes for an alert component to show below the header.
|
|
||||||
*
|
|
||||||
* @type {object}
|
|
||||||
*/
|
|
||||||
alertAttrs = null;
|
|
||||||
|
|
||||||
oncreate(vnode) {
|
|
||||||
super.oncreate(vnode);
|
|
||||||
|
|
||||||
this.attrs.animateShow(() => this.onready());
|
|
||||||
}
|
|
||||||
|
|
||||||
onbeforeremove(vnode) {
|
|
||||||
super.onbeforeremove(vnode);
|
|
||||||
|
|
||||||
// If the global modal state currently contains a modal,
|
|
||||||
// we've just opened up a new one, and accordingly,
|
|
||||||
// we don't need to show a hide animation.
|
|
||||||
if (!this.attrs.state.modal) {
|
|
||||||
this.attrs.animateHide();
|
|
||||||
// Here, we ensure that the animation has time to complete.
|
|
||||||
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
|
|
||||||
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
view() {
|
|
||||||
if (this.alertAttrs) {
|
|
||||||
this.alertAttrs.dismissible = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'Modal modal-dialog ' + this.className()}>
|
|
||||||
<div className="Modal-content">
|
|
||||||
{this.constructor.isDismissible ? (
|
|
||||||
<div className="Modal-close App-backControl">
|
|
||||||
{Button.component({
|
|
||||||
icon: 'fas fa-times',
|
|
||||||
onclick: this.hide.bind(this),
|
|
||||||
className: 'Button Button--icon Button--link',
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
|
|
||||||
<form onsubmit={this.onsubmit.bind(this)}>
|
|
||||||
<div className="Modal-header">
|
|
||||||
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.alertAttrs ? <div className="Modal-alert">{Alert.component(this.alertAttrs)}</div> : ''}
|
|
||||||
|
|
||||||
{this.content()}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the class name to apply to the modal.
|
|
||||||
*
|
|
||||||
* @return {String}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
className() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the title of the modal dialog.
|
|
||||||
*
|
|
||||||
* @return {String}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
title() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content of the modal.
|
|
||||||
*
|
|
||||||
* @return {VirtualElement}
|
|
||||||
* @abstract
|
|
||||||
*/
|
|
||||||
content() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the modal form's submit event.
|
|
||||||
*
|
|
||||||
* @param {Event} e
|
|
||||||
*/
|
|
||||||
onsubmit() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Focus on the first input when the modal is ready to be used.
|
|
||||||
*/
|
|
||||||
onready() {
|
|
||||||
this.$('form').find('input, select, textarea').first().focus().select();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide the modal.
|
|
||||||
*/
|
|
||||||
hide() {
|
|
||||||
this.attrs.state.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop loading.
|
|
||||||
*/
|
|
||||||
loaded() {
|
|
||||||
this.loading = false;
|
|
||||||
m.redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an alert describing an error returned from the API, and give focus to
|
|
||||||
* the first relevant field.
|
|
||||||
*
|
|
||||||
* @param {RequestError} error
|
|
||||||
*/
|
|
||||||
onerror(error) {
|
|
||||||
this.alertAttrs = error.alert;
|
|
||||||
|
|
||||||
m.redraw();
|
|
||||||
|
|
||||||
if (error.status === 422 && error.response.errors) {
|
|
||||||
this.$('form [name=' + error.response.errors[0].source.pointer.replace('/data/attributes/', '') + ']').select();
|
|
||||||
} else {
|
|
||||||
this.onready();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
171
js/src/common/components/Modal.tsx
Normal file
171
js/src/common/components/Modal.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import Component from '../Component';
|
||||||
|
import Alert, { AlertAttrs } from './Alert';
|
||||||
|
import Button from './Button';
|
||||||
|
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import type ModalManagerState from '../states/ModalManagerState';
|
||||||
|
import type RequestError from '../utils/RequestError';
|
||||||
|
import type ModalManager from './ModalManager';
|
||||||
|
import fireDebugWarning from '../helpers/fireDebugWarning';
|
||||||
|
|
||||||
|
interface IInternalModalAttrs {
|
||||||
|
state: ModalManagerState;
|
||||||
|
animateShow: ModalManager['animateShow'];
|
||||||
|
animateHide: ModalManager['animateHide'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `Modal` component displays a modal dialog, wrapped in a form. Subclasses
|
||||||
|
* should implement the `className`, `title`, and `content` methods.
|
||||||
|
*/
|
||||||
|
export default abstract class Modal<ModalAttrs = {}> extends Component<ModalAttrs & IInternalModalAttrs> {
|
||||||
|
/**
|
||||||
|
* Determine whether or not the modal should be dismissible via an 'x' button.
|
||||||
|
*/
|
||||||
|
static readonly isDismissible = true;
|
||||||
|
|
||||||
|
protected loading: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attributes for an alert component to show below the header.
|
||||||
|
*/
|
||||||
|
alertAttrs!: AlertAttrs;
|
||||||
|
|
||||||
|
oninit(vnode: Mithril.VnodeDOM<ModalAttrs & IInternalModalAttrs, this>) {
|
||||||
|
super.oninit(vnode);
|
||||||
|
|
||||||
|
// TODO: [Flarum 2.0] Remove the code below.
|
||||||
|
// This code prevents extensions which do not implement all abstract methods of this class from breaking
|
||||||
|
// the forum frontend. Without it, function calls would would error rather than returning `undefined.`
|
||||||
|
|
||||||
|
const missingMethods: string[] = [];
|
||||||
|
|
||||||
|
['className', 'title', 'content', 'onsubmit'].forEach((method) => {
|
||||||
|
if (!(this as any)[method]) {
|
||||||
|
(this as any)[method] = function (): void {};
|
||||||
|
missingMethods.push(method);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingMethods.length > 0) {
|
||||||
|
fireDebugWarning(
|
||||||
|
`Modal \`${this.constructor.name}\` does not implement all abstract methods of the Modal super class. Missing methods: ${missingMethods.join(
|
||||||
|
', '
|
||||||
|
)}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oncreate(vnode: Mithril.VnodeDOM<ModalAttrs & IInternalModalAttrs, this>) {
|
||||||
|
super.oncreate(vnode);
|
||||||
|
|
||||||
|
this.attrs.animateShow(() => this.onready());
|
||||||
|
}
|
||||||
|
|
||||||
|
onbeforeremove(vnode: Mithril.VnodeDOM<ModalAttrs & IInternalModalAttrs, this>): Promise<void> | void {
|
||||||
|
super.onbeforeremove(vnode);
|
||||||
|
|
||||||
|
// If the global modal state currently contains a modal,
|
||||||
|
// we've just opened up a new one, and accordingly,
|
||||||
|
// we don't need to show a hide animation.
|
||||||
|
if (!this.attrs.state.modal) {
|
||||||
|
this.attrs.animateHide();
|
||||||
|
// Here, we ensure that the animation has time to complete.
|
||||||
|
// See https://mithril.js.org/lifecycle-methods.html#onbeforeremove
|
||||||
|
// Bootstrap's Modal.TRANSITION_DURATION is 300 ms.
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view() {
|
||||||
|
if (this.alertAttrs) {
|
||||||
|
this.alertAttrs.dismissible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'Modal modal-dialog ' + this.className()}>
|
||||||
|
<div className="Modal-content">
|
||||||
|
{(this.constructor as typeof Modal).isDismissible && (
|
||||||
|
<div className="Modal-close App-backControl">
|
||||||
|
{Button.component({
|
||||||
|
icon: 'fas fa-times',
|
||||||
|
onclick: this.hide.bind(this),
|
||||||
|
className: 'Button Button--icon Button--link',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onsubmit={this.onsubmit.bind(this)}>
|
||||||
|
<div className="Modal-header">
|
||||||
|
<h3 className="App-titleControl App-titleControl--text">{this.title()}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.alertAttrs ? <div className="Modal-alert">{Alert.component(this.alertAttrs)}</div> : ''}
|
||||||
|
|
||||||
|
{this.content()}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the class name to apply to the modal.
|
||||||
|
*/
|
||||||
|
abstract className(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the title of the modal dialog.
|
||||||
|
*/
|
||||||
|
abstract title(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the content of the modal.
|
||||||
|
*/
|
||||||
|
abstract content(): Mithril.Children;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the modal form's submit event.
|
||||||
|
*/
|
||||||
|
abstract onsubmit(e: Event): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback executed when the modal is shown and ready to be interacted with.
|
||||||
|
*
|
||||||
|
* @remark Focuses the first input in the modal.
|
||||||
|
*/
|
||||||
|
onready(): void {
|
||||||
|
this.$().find('input, select, textarea').first().trigger('focus').trigger('select');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the modal.
|
||||||
|
*/
|
||||||
|
hide() {
|
||||||
|
this.attrs.state.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets `loading` to false and triggers a redraw.
|
||||||
|
*/
|
||||||
|
loaded() {
|
||||||
|
this.loading = false;
|
||||||
|
m.redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows an alert describing an error returned from the API, and gives focus to
|
||||||
|
* the first relevant field involved in the error.
|
||||||
|
*/
|
||||||
|
onerror(error: RequestError) {
|
||||||
|
this.alertAttrs = error.alert;
|
||||||
|
|
||||||
|
m.redraw();
|
||||||
|
|
||||||
|
if (error.status === 422 && error.response?.errors) {
|
||||||
|
this.$('form [name=' + (error.response.errors as any[])[0].source.pointer.replace('/data/attributes/', '') + ']').trigger('select');
|
||||||
|
} else {
|
||||||
|
this.onready();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,29 +1,35 @@
|
|||||||
import Component from '../Component';
|
import Component from '../Component';
|
||||||
|
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
import type ModalManagerState from '../states/ModalManagerState';
|
||||||
|
|
||||||
|
interface IModalManagerAttrs {
|
||||||
|
state: ModalManagerState;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The `ModalManager` component manages a modal dialog. Only one modal dialog
|
* The `ModalManager` component manages a modal dialog. Only one modal dialog
|
||||||
* can be shown at once; loading a new component into the ModalManager will
|
* can be shown at once; loading a new component into the ModalManager will
|
||||||
* overwrite the previous one.
|
* overwrite the previous one.
|
||||||
*/
|
*/
|
||||||
export default class ModalManager extends Component {
|
export default class ModalManager extends Component<IModalManagerAttrs> {
|
||||||
view() {
|
view() {
|
||||||
const modal = this.attrs.state.modal;
|
const modal = this.attrs.state.modal;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ModalManager modal fade">
|
<div className="ModalManager modal fade">
|
||||||
{modal
|
{!!modal &&
|
||||||
? modal.componentClass.component({
|
modal.componentClass.component({
|
||||||
...modal.attrs,
|
...modal.attrs,
|
||||||
animateShow: this.animateShow.bind(this),
|
animateShow: this.animateShow.bind(this),
|
||||||
animateHide: this.animateHide.bind(this),
|
animateHide: this.animateHide.bind(this),
|
||||||
state: this.attrs.state,
|
state: this.attrs.state,
|
||||||
})
|
})}
|
||||||
: ''}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
oncreate(vnode) {
|
oncreate(vnode: Mithril.VnodeDOM<IModalManagerAttrs, this>) {
|
||||||
super.oncreate(vnode);
|
super.oncreate(vnode);
|
||||||
|
|
||||||
// Ensure the modal state is notified about a closed modal, even when the
|
// Ensure the modal state is notified about a closed modal, even when the
|
||||||
@@ -32,7 +38,9 @@ export default class ModalManager extends Component {
|
|||||||
this.$().on('hidden.bs.modal', this.attrs.state.close.bind(this.attrs.state));
|
this.$().on('hidden.bs.modal', this.attrs.state.close.bind(this.attrs.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
animateShow(readyCallback) {
|
animateShow(readyCallback: () => void): void {
|
||||||
|
if (!this.attrs.state.modal) return;
|
||||||
|
|
||||||
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
|
const dismissible = !!this.attrs.state.modal.componentClass.isDismissible;
|
||||||
|
|
||||||
// If we are opening this modal while another modal is already open,
|
// If we are opening this modal while another modal is already open,
|
||||||
@@ -45,6 +53,7 @@ export default class ModalManager extends Component {
|
|||||||
|
|
||||||
this.$()
|
this.$()
|
||||||
.one('shown.bs.modal', readyCallback)
|
.one('shown.bs.modal', readyCallback)
|
||||||
|
// @ts-expect-error: No typings available for Bootstrap modals.
|
||||||
.modal({
|
.modal({
|
||||||
backdrop: dismissible || 'static',
|
backdrop: dismissible || 'static',
|
||||||
keyboard: dismissible,
|
keyboard: dismissible,
|
||||||
@@ -52,7 +61,8 @@ export default class ModalManager extends Component {
|
|||||||
.modal('show');
|
.modal('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
animateHide() {
|
animateHide(): void {
|
||||||
|
// @ts-expect-error: No typings available for Bootstrap modals.
|
||||||
this.$().modal('hide');
|
this.$().modal('hide');
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import app from '../app';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls `console.warn` with the provided arguments, but only if the forum is in debug mode.
|
* Calls `console.warn` with the provided arguments, but only if the forum is in debug mode.
|
||||||
*
|
*
|
||||||
|
@@ -1,46 +0,0 @@
|
|||||||
import Modal from '../components/Modal';
|
|
||||||
|
|
||||||
export default class ModalManagerState {
|
|
||||||
constructor() {
|
|
||||||
this.modal = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show a modal dialog.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
show(componentClass, attrs) {
|
|
||||||
if (!(componentClass.prototype instanceof Modal)) {
|
|
||||||
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
|
||||||
const invalidModalWarning = 'The ModalManager can only show Modals.';
|
|
||||||
console.error(invalidModalWarning);
|
|
||||||
throw new Error(invalidModalWarning);
|
|
||||||
}
|
|
||||||
|
|
||||||
clearTimeout(this.closeTimeout);
|
|
||||||
|
|
||||||
this.modal = { componentClass, attrs };
|
|
||||||
|
|
||||||
m.redraw.sync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the modal dialog.
|
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
|
||||||
close() {
|
|
||||||
if (!this.modal) return;
|
|
||||||
|
|
||||||
// Don't hide the modal immediately, because if the consumer happens to call
|
|
||||||
// the `show` method straight after to show another modal dialog, it will
|
|
||||||
// cause Bootstrap's modal JS to misbehave. Instead we will wait for a tiny
|
|
||||||
// bit to give the `show` method the opportunity to prevent this from going
|
|
||||||
// ahead.
|
|
||||||
this.closeTimeout = setTimeout(() => {
|
|
||||||
this.modal = null;
|
|
||||||
m.redraw();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
71
js/src/common/states/ModalManagerState.ts
Normal file
71
js/src/common/states/ModalManagerState.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import Modal from '../components/Modal';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to manage modal state.
|
||||||
|
*
|
||||||
|
* Accessible on the `app` object via `app.modal` property.
|
||||||
|
*/
|
||||||
|
export default class ModalManagerState {
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
modal: null | {
|
||||||
|
componentClass: typeof Modal;
|
||||||
|
attrs?: Record<string, unknown>;
|
||||||
|
} = null;
|
||||||
|
|
||||||
|
private closeTimeout?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a modal dialog.
|
||||||
|
*
|
||||||
|
* If a modal is already open, the existing one will close and the new modal will replace it.
|
||||||
|
*
|
||||||
|
* @example <caption>Show a modal</caption>
|
||||||
|
* app.modal.show(MyCoolModal, { attr: 'value' });
|
||||||
|
*
|
||||||
|
* @example <caption>Show a modal from a lifecycle method (`oncreate`, `view`, etc.)</caption>
|
||||||
|
* // This "hack" is needed due to quirks with nested redraws in Mithril.
|
||||||
|
* setTimeout(() => app.modal.show(MyCoolModal, { attr: 'value' }), 0);
|
||||||
|
*/
|
||||||
|
show(componentClass: typeof Modal, attrs: Record<string, unknown> = {}): void {
|
||||||
|
if (!(componentClass.prototype instanceof Modal)) {
|
||||||
|
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
|
||||||
|
const invalidModalWarning = 'The ModalManager can only show Modals.';
|
||||||
|
console.error(invalidModalWarning);
|
||||||
|
throw new Error(invalidModalWarning);
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(this.closeTimeout);
|
||||||
|
|
||||||
|
this.modal = { componentClass, attrs };
|
||||||
|
|
||||||
|
m.redraw.sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the currently open dialog, if one is open.
|
||||||
|
*/
|
||||||
|
close(): void {
|
||||||
|
if (!this.modal) return;
|
||||||
|
|
||||||
|
// Don't hide the modal immediately, because if the consumer happens to call
|
||||||
|
// the `show` method straight after to show another modal dialog, it will
|
||||||
|
// cause Bootstrap's modal JS to misbehave. Instead we will wait for a tiny
|
||||||
|
// bit to give the `show` method the opportunity to prevent this from going
|
||||||
|
// ahead.
|
||||||
|
this.closeTimeout = setTimeout(() => {
|
||||||
|
this.modal = null;
|
||||||
|
m.redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a modal is currently open.
|
||||||
|
*
|
||||||
|
* @returns `true` if a modal dialog is currently open, otherwise `false`.
|
||||||
|
*/
|
||||||
|
isModalOpen(): boolean {
|
||||||
|
return !!this.modal;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,21 +1,21 @@
|
|||||||
export default class RequestError {
|
export default class RequestError {
|
||||||
status: string;
|
status: number;
|
||||||
options: object;
|
options: Record<string, unknown>;
|
||||||
xhr: XMLHttpRequest;
|
xhr: XMLHttpRequest;
|
||||||
|
|
||||||
responseText: string | null;
|
responseText: string | null;
|
||||||
response: object | null;
|
response: Record<string, unknown> | null;
|
||||||
|
|
||||||
alert: any;
|
alert: any;
|
||||||
|
|
||||||
constructor(status: string, responseText: string | null, options: object, xhr: XMLHttpRequest) {
|
constructor(status: number, responseText: string | null, options: Record<string, unknown>, xhr: XMLHttpRequest) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.responseText = responseText;
|
this.responseText = responseText;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.xhr = xhr;
|
this.xhr = xhr;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.response = JSON.parse(responseText);
|
this.response = JSON.parse(responseText ?? 'null');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.response = null;
|
this.response = null;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user