Extract ModalManagerState from ModalManager (#2162)

This commit is contained in:
Alexander Skvortsov 2020-06-30 19:59:16 -04:00 committed by GitHub
parent 0b20cf4eb7
commit d4def36de8
17 changed files with 130 additions and 111 deletions

View File

@ -82,7 +82,7 @@ export default class AppearancePage extends Page {
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_header_button'), children: app.translator.trans('core.admin.appearance.edit_header_button'),
onclick: () => app.modal.show(new EditCustomHeaderModal()), onclick: () => app.modal.show(EditCustomHeaderModal),
})} })}
</fieldset> </fieldset>
@ -92,7 +92,7 @@ export default class AppearancePage extends Page {
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_footer_button'), children: app.translator.trans('core.admin.appearance.edit_footer_button'),
onclick: () => app.modal.show(new EditCustomFooterModal()), onclick: () => app.modal.show(EditCustomFooterModal),
})} })}
</fieldset> </fieldset>
@ -102,7 +102,7 @@ export default class AppearancePage extends Page {
{Button.component({ {Button.component({
className: 'Button', className: 'Button',
children: app.translator.trans('core.admin.appearance.edit_css_button'), children: app.translator.trans('core.admin.appearance.edit_css_button'),
onclick: () => app.modal.show(new EditCustomCssModal()), onclick: () => app.modal.show(EditCustomCssModal),
})} })}
</fieldset> </fieldset>
</div> </div>

View File

@ -16,7 +16,7 @@ export default class ExtensionsPage extends Page {
children: app.translator.trans('core.admin.extensions.add_button'), children: app.translator.trans('core.admin.extensions.add_button'),
icon: 'fas fa-plus', icon: 'fas fa-plus',
className: 'Button Button--primary', className: 'Button Button--primary',
onclick: () => app.modal.show(new AddExtensionModal()), onclick: () => app.modal.show(AddExtensionModal),
})} })}
</div> </div>
</div> </div>
@ -94,7 +94,7 @@ export default class ExtensionsPage extends Page {
}) })
.then(() => window.location.reload()); .then(() => window.location.reload());
app.modal.show(new LoadingModal()); app.modal.show(LoadingModal);
}, },
}) })
); );
@ -123,6 +123,6 @@ export default class ExtensionsPage extends Page {
window.location.reload(); window.location.reload();
}); });
app.modal.show(new LoadingModal()); app.modal.show(LoadingModal);
} }
} }

View File

@ -1,9 +1,10 @@
import Modal from '../../common/components/Modal'; import Modal from '../../common/components/Modal';
export default class LoadingModal extends Modal { export default class LoadingModal extends Modal {
isDismissible() { /**
return false; * @inheritdoc
} */
static isDismissible = false;
className() { className() {
return 'LoadingModal Modal--small'; return 'LoadingModal Modal--small';

View File

@ -15,7 +15,7 @@ export default class PermissionsPage extends Page {
.all('groups') .all('groups')
.filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1) .filter((group) => [Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map((group) => ( .map((group) => (
<button className="Button Group" onclick={() => app.modal.show(new EditGroupModal({ group }))}> <button className="Button Group" onclick={() => app.modal.show(EditGroupModal, { group })}>
{GroupBadge.component({ {GroupBadge.component({
group, group,
className: 'Group-icon', className: 'Group-icon',
@ -24,7 +24,7 @@ export default class PermissionsPage extends Page {
<span className="Group-name">{group.namePlural()}</span> <span className="Group-name">{group.namePlural()}</span>
</button> </button>
))} ))}
<button className="Button Group Group--add" onclick={() => app.modal.show(new EditGroupModal())}> <button className="Button Group Group--add" onclick={() => app.modal.show(EditGroupModal)}>
{icon('fas fa-plus', { className: 'Group-icon' })} {icon('fas fa-plus', { className: 'Group-icon' })}
<span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span> <span className="Group-name">{app.translator.trans('core.admin.permissions.new_group_button')}</span>
</button> </button>

View File

@ -46,7 +46,7 @@ export default class StatusWidget extends DashboardWidget {
} }
handleClearCache(e) { handleClearCache(e) {
app.modal.show(new LoadingModal()); app.modal.show(LoadingModal);
app app
.request({ .request({

View File

@ -22,6 +22,7 @@ import Group from './models/Group';
import Notification from './models/Notification'; import Notification from './models/Notification';
import { flattenDeep } from 'lodash-es'; import { flattenDeep } from 'lodash-es';
import PageState from './states/PageState'; import PageState from './states/PageState';
import ModalManagerState from './states/ModalManagerState';
import AlertManagerState from './states/AlertManagerState'; import AlertManagerState from './states/AlertManagerState';
/** /**
@ -140,7 +141,16 @@ export default class Application {
previous = new PageState(null); previous = new PageState(null);
/* /*
* An object that manages modal state.
*
* @type {ModalManagerState}
*/
modal = new ModalManagerState();
/**
* An object that manages the state of active alerts. * An object that manages the state of active alerts.
*
* @type {AlertManagerState}
*/ */
alerts = new AlertManagerState(); alerts = new AlertManagerState();
@ -179,7 +189,7 @@ export default class Application {
} }
mount(basePath = '') { mount(basePath = '') {
this.modal = m.mount(document.getElementById('modal'), <ModalManager />); m.mount(document.getElementById('modal'), <ModalManager state={this.modal} />);
m.mount(document.getElementById('alerts'), <AlertManager state={this.alerts} />); m.mount(document.getElementById('alerts'), <AlertManager state={this.alerts} />);
this.drawer = new Drawer(); this.drawer = new Drawer();
@ -402,7 +412,7 @@ export default class Application {
showDebug(error, formattedError) { showDebug(error, formattedError) {
this.alerts.dismiss(this.requestErrorAlert); this.alerts.dismiss(this.requestErrorAlert);
this.modal.show(new RequestErrorModal({ error, formattedError })); this.modal.show(RequestErrorModal, { error, formattedError });
} }
/** /**

View File

@ -9,6 +9,11 @@ import Button from './Button';
* @abstract * @abstract
*/ */
export default class Modal extends Component { export default class Modal extends Component {
/**
* Determine whether or not the modal should be dismissible via an 'x' button.
*/
static isDismissible = true;
init() { init() {
/** /**
* Attributes for an alert component to show below the header. * Attributes for an alert component to show below the header.
@ -18,6 +23,16 @@ export default class Modal extends Component {
this.alertAttrs = null; this.alertAttrs = null;
} }
config(isInitialized, context) {
if (isInitialized) return;
this.props.onshow(() => this.onready());
context.onunload = () => {
this.props.onhide();
};
}
view() { view() {
if (this.alertAttrs) { if (this.alertAttrs) {
this.alertAttrs.dismissible = false; this.alertAttrs.dismissible = false;
@ -26,7 +41,7 @@ export default class Modal extends Component {
return ( return (
<div className={'Modal modal-dialog ' + this.className()}> <div className={'Modal modal-dialog ' + this.className()}>
<div className="Modal-content"> <div className="Modal-content">
{this.isDismissible() ? ( {this.constructor.isDismissible ? (
<div className="Modal-close App-backControl"> <div className="Modal-close App-backControl">
{Button.component({ {Button.component({
icon: 'fas fa-times', icon: 'fas fa-times',
@ -52,15 +67,6 @@ export default class Modal extends Component {
); );
} }
/**
* Determine whether or not the modal should be dismissible via an 'x' button.
*
* @return {Boolean}
*/
isDismissible() {
return true;
}
/** /**
* Get the class name to apply to the modal. * Get the class name to apply to the modal.
* *
@ -105,7 +111,7 @@ export default class Modal extends Component {
* Hide the modal. * Hide the modal.
*/ */
hide() { hide() {
app.modal.close(); this.props.onhide();
} }
/** /**

View File

@ -1,5 +1,4 @@
import Component from '../Component'; import Component from '../Component';
import Modal from './Modal';
/** /**
* The `ModalManager` component manages a modal dialog. Only one modal dialog * The `ModalManager` component manages a modal dialog. Only one modal dialog
@ -8,12 +7,17 @@ import Modal from './Modal';
*/ */
export default class ModalManager extends Component { export default class ModalManager extends Component {
init() { init() {
this.showing = false; this.state = this.props.state;
this.component = null;
} }
view() { view() {
return <div className="ModalManager modal fade">{this.component && this.component.render()}</div>; const modal = this.state.modal;
return (
<div className="ModalManager modal fade">
{modal ? modal.componentClass.component({ ...modal.attrs, onshow: this.animateShow.bind(this), onhide: this.animateHide.bind(this) }) : ''}
</div>
);
} }
config(isInitialized, context) { config(isInitialized, context) {
@ -24,29 +28,17 @@ export default class ModalManager extends Component {
// to be retained across route changes. // to be retained across route changes.
context.retain = true; context.retain = true;
this.$().on('hidden.bs.modal', this.clear.bind(this)).on('shown.bs.modal', this.onready.bind(this)); // Ensure the modal state is notified about a closed modal, even when the
// DOM-based Bootstrap JavaScript code triggered the closing of the modal,
// e.g. via ESC key or a click on the modal backdrop.
this.$().on('hidden.bs.modal', this.state.close.bind(this.state));
} }
/** animateShow(readyCallback) {
* Show a modal dialog. const dismissible = !!this.state.modal.componentClass.isDismissible;
*
* @param {Modal} component
* @public
*/
show(component) {
if (!(component instanceof Modal)) {
throw new Error('The ModalManager component can only show Modal components');
}
clearTimeout(this.hideTimeout);
this.showing = true;
this.component = component;
m.redraw(true);
const dismissible = !!this.component.isDismissible();
this.$() this.$()
.one('shown.bs.modal', readyCallback)
.modal({ .modal({
backdrop: dismissible || 'static', backdrop: dismissible || 'static',
keyboard: dismissible, keyboard: dismissible,
@ -54,50 +46,7 @@ export default class ModalManager extends Component {
.modal('show'); .modal('show');
} }
/** animateHide() {
* Close the modal dialog. this.$().modal('hide');
*
* @public
*/
close() {
if (!this.showing) 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.hideTimeout = setTimeout(() => {
this.$().modal('hide');
this.showing = false;
});
}
/**
* Clear content from the modal area.
*
* @protected
*/
clear() {
if (this.component) {
this.component.onhide();
}
this.component = null;
app.current.retain = false;
m.lazyRedraw();
}
/**
* When the modal dialog is ready to be used, tell it!
*
* @protected
*/
onready() {
if (this.component && this.component.onready) {
this.component.onready(this.$());
}
} }
} }

View File

@ -0,0 +1,56 @@
import Modal from '../components/Modal';
export default class ModalManagerState {
constructor() {
this.modal = null;
}
/**
* Show a modal dialog.
*
* @public
*/
show(componentClass, attrs) {
// Breaking Change Compliance Warning, Remove in Beta 15.
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.
console.error('The ModalManager can only show Modals');
throw new Error('The ModalManager can only show Modals');
}
if (componentClass.init) {
// This is duplicated so that if the error is caught, an error message still shows up in the debug console.
console.error(
'The componentClass parameter must be a modal class, not a modal instance. Whichever extension triggered this modal should be updated to comply with beta 14.'
);
throw new Error(
'The componentClass parameter must be a modal class, not a modal instance. Whichever extension triggered this modal should be updated to comply with beta 14.'
);
}
// End Change Compliance Warning, Remove in Beta 15
clearTimeout(this.closeTimeout);
this.modal = { componentClass, attrs };
m.redraw(true);
}
/**
* 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.lazyRedraw();
});
}
}

View File

@ -180,8 +180,7 @@ export default class ForumApplication extends Application {
if (payload.loggedIn) { if (payload.loggedIn) {
window.location.reload(); window.location.reload();
} else { } else {
const modal = new SignUpModal(payload); this.modal.show(SignUpModal, payload);
this.modal.show(modal);
} }
} }
} }

View File

@ -77,7 +77,7 @@ export default class HeaderSecondary extends Component {
Button.component({ Button.component({
children: app.translator.trans('core.forum.header.sign_up_link'), children: app.translator.trans('core.forum.header.sign_up_link'),
className: 'Button Button--link', className: 'Button Button--link',
onclick: () => app.modal.show(new SignUpModal()), onclick: () => app.modal.show(SignUpModal),
}), }),
10 10
); );
@ -88,7 +88,7 @@ export default class HeaderSecondary extends Component {
Button.component({ Button.component({
children: app.translator.trans('core.forum.header.log_in_link'), children: app.translator.trans('core.forum.header.log_in_link'),
className: 'Button Button--link', className: 'Button Button--link',
onclick: () => app.modal.show(new LogInModal()), onclick: () => app.modal.show(LogInModal),
}), }),
0 0
); );

View File

@ -282,7 +282,7 @@ export default class IndexPage extends Page {
} else { } else {
deferred.reject(); deferred.reject();
app.modal.show(new LogInModal()); app.modal.show(LogInModal);
} }
return deferred.promise; return deferred.promise;

View File

@ -142,7 +142,7 @@ export default class LogInModal extends Modal {
const email = this.identification(); const email = this.identification();
const props = email.indexOf('@') !== -1 ? { email } : undefined; const props = email.indexOf('@') !== -1 ? { email } : undefined;
app.modal.show(new ForgotPasswordModal(props)); app.modal.show(ForgotPasswordModal, props);
} }
/** /**
@ -156,7 +156,7 @@ export default class LogInModal extends Modal {
const identification = this.identification(); const identification = this.identification();
props[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification; props[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification;
app.modal.show(new SignUpModal(props)); app.modal.show(SignUpModal, props);
} }
onready() { onready() {

View File

@ -79,7 +79,7 @@ export default class SettingsPage extends UserPage {
Button.component({ Button.component({
children: app.translator.trans('core.forum.settings.change_password_button'), children: app.translator.trans('core.forum.settings.change_password_button'),
className: 'Button', className: 'Button',
onclick: () => app.modal.show(new ChangePasswordModal()), onclick: () => app.modal.show(ChangePasswordModal),
}) })
); );
@ -88,7 +88,7 @@ export default class SettingsPage extends UserPage {
Button.component({ Button.component({
children: app.translator.trans('core.forum.settings.change_email_button'), children: app.translator.trans('core.forum.settings.change_email_button'),
className: 'Button', className: 'Button',
onclick: () => app.modal.show(new ChangeEmailModal()), onclick: () => app.modal.show(ChangeEmailModal),
}) })
); );

View File

@ -145,7 +145,7 @@ export default class SignUpModal extends Modal {
password: this.password(), password: this.password(),
}; };
app.modal.show(new LogInModal(props)); app.modal.show(LogInModal, props);
} }
onready() { onready() {

View File

@ -188,7 +188,7 @@ export default {
} else { } else {
deferred.reject(); deferred.reject();
app.modal.show(new LogInModal()); app.modal.show(LogInModal);
} }
return deferred.promise; return deferred.promise;
@ -239,11 +239,9 @@ export default {
* @return {Promise} * @return {Promise}
*/ */
renameAction() { renameAction() {
return app.modal.show( return app.modal.show(RenameDiscussionModal, {
new RenameDiscussionModal({ currentTitle: this.title(),
currentTitle: this.title(), discussion: this,
discussion: this, });
})
);
}, },
}; };

View File

@ -145,6 +145,6 @@ export default {
* @param {User} user * @param {User} user
*/ */
editAction(user) { editAction(user) {
app.modal.show(new EditUserModal({ user })); app.modal.show(EditUserModal, { user });
}, },
}; };