mirror of
https://github.com/flarum/core.git
synced 2025-08-06 08:27:42 +02:00
Add ModalManager & LogInModal, add bidi attribute, fix Translator issues with text and vnodes
This commit is contained in:
11733
js/dist/admin.js
vendored
11733
js/dist/admin.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/admin.js.map
vendored
2
js/dist/admin.js.map
vendored
File diff suppressed because one or more lines are too long
19251
js/dist/forum.js
vendored
19251
js/dist/forum.js
vendored
File diff suppressed because one or more lines are too long
2
js/dist/forum.js.map
vendored
2
js/dist/forum.js.map
vendored
File diff suppressed because one or more lines are too long
@@ -19,12 +19,13 @@ import Notification from './models/Notification';
|
||||
|
||||
import RequestError from './utils/RequestError';
|
||||
import Alert from './components/Alert';
|
||||
import ModalManager from './components/ModalManager';
|
||||
|
||||
export type ApplicationData = {
|
||||
apiDocument: any;
|
||||
locale: string;
|
||||
locales: any;
|
||||
resources: Array<any>;
|
||||
resources: any[];
|
||||
session: any;
|
||||
};
|
||||
|
||||
@@ -42,7 +43,7 @@ export default abstract class Application {
|
||||
/**
|
||||
* The app's session.
|
||||
*/
|
||||
session?: Session;
|
||||
session: Session;
|
||||
|
||||
/**
|
||||
* The app's data store.
|
||||
@@ -58,6 +59,8 @@ export default abstract class Application {
|
||||
|
||||
drawer = new Drawer();
|
||||
|
||||
modal: ModalManager;
|
||||
|
||||
/**
|
||||
* A local cache that can be used to store data at the application level, so
|
||||
* that is persists between different routes.
|
||||
@@ -76,7 +79,8 @@ export default abstract class Application {
|
||||
private requestError: Alert = null;
|
||||
|
||||
mount(basePath = '') {
|
||||
// this.modal = m.mount(document.getElementById('modal'), <ModalManager />);
|
||||
m.mount(document.getElementById('modal'), new ModalManager());
|
||||
|
||||
// this.alerts = m.mount(document.getElementById('alerts'), <AlertManager />);
|
||||
|
||||
m.route(document.getElementById('content'), basePath + '/', mapRoutes(this.routes, basePath));
|
||||
|
@@ -9,9 +9,9 @@ export type ComponentProps = {
|
||||
}
|
||||
|
||||
export default class Component<T extends ComponentProps = any> {
|
||||
protected element: HTMLElement;
|
||||
element: HTMLElement;
|
||||
|
||||
protected props = <T> {};
|
||||
props = <T> {};
|
||||
|
||||
view(vnode) {
|
||||
throw new Error('Component#view must be implemented by subclass');
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import User from './models/User';
|
||||
|
||||
/**
|
||||
* The `Session` class defines the current user session. It stores a reference
|
||||
* to the current authenticated user, and provides methods to log in/out.
|
||||
@@ -22,11 +24,11 @@ export default class Session {
|
||||
/**
|
||||
* Attempt to log in a user.
|
||||
*/
|
||||
login(data: { identification: string, password: string }, options = {}): Promise {
|
||||
login(body: { identification: string, password: string }, options = {}) {
|
||||
return app.request(Object.assign({
|
||||
method: 'POST',
|
||||
url: app.forum.attribute('baseUrl') + '/login',
|
||||
data
|
||||
body
|
||||
}, options));
|
||||
}
|
||||
|
||||
@@ -36,6 +38,6 @@ export default class Session {
|
||||
* @public
|
||||
*/
|
||||
logout() {
|
||||
window.location = app.forum.attribute('baseUrl') + '/logout?token=' + this.csrfToken;
|
||||
window.location.href = `${app.forum.attribute('baseUrl')}/logout?token=${this.csrfToken}`;
|
||||
}
|
||||
}
|
||||
|
@@ -77,7 +77,7 @@ export default class Store {
|
||||
* @param query
|
||||
* @param options
|
||||
*/
|
||||
find<T extends Model = Model>(type: string, id?: number|Array<number>|any, query = {}, options = {}): Promise<T[]> {
|
||||
find<T extends Model = Model>(type: string, id?: number|number[]|any, query = {}, options = {}): Promise<T[]> {
|
||||
let data = query;
|
||||
let url = `${app.forum.attribute('apiUrl')}/${type}`;
|
||||
|
||||
|
@@ -65,7 +65,7 @@ export default class Translator {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
open[0].push(part);
|
||||
open[0].push({ tag: 'span', text: part });
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -54,7 +54,7 @@ export default class Button<T extends ButtonProps = ButtonProps> extends Compone
|
||||
delete attrs.onclick;
|
||||
}
|
||||
|
||||
return <button {...attrs}>{this.getButtonContent(attrs.icon, attrs.loading, children)}</button>;
|
||||
return <button {...attrs}>{this.getButtonContent(iconName, attrs.loading, children)}</button>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
116
js/src/common/components/Modal.tsx
Normal file
116
js/src/common/components/Modal.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import Component, {ComponentProps} from '../Component';
|
||||
import Alert from './Alert';
|
||||
import Button from './Button';
|
||||
import RequestError from "../utils/RequestError";
|
||||
|
||||
/**
|
||||
* 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<T extends ComponentProps = ComponentProps> extends Component<T> {
|
||||
/**
|
||||
* An alert component to show below the header.
|
||||
*/
|
||||
alert: Alert;
|
||||
|
||||
loading: boolean;
|
||||
|
||||
view() {
|
||||
if (this.alert) {
|
||||
this.alert.props.dismissible = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`Modal modal-dialog ${this.className()}`}>
|
||||
<div className="Modal-content">
|
||||
{this.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>
|
||||
|
||||
{alert && <div className="Modal-alert">{this.alert}</div>}
|
||||
|
||||
{this.content()}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether or not the modal should be dismissible via an 'x' button.
|
||||
*/
|
||||
isDismissible(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* Handle the modal form's submit event.
|
||||
*/
|
||||
onsubmit(e: Event) {}
|
||||
|
||||
/**
|
||||
* Focus on the first input when the modal is ready to be used.
|
||||
*/
|
||||
onready() {
|
||||
this.$('form').find('input, select, textarea').first().focus().select();
|
||||
}
|
||||
|
||||
onhide() {}
|
||||
|
||||
/**
|
||||
* Hide the modal.
|
||||
*/
|
||||
hide() {
|
||||
app.modal.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.
|
||||
*/
|
||||
onerror(error: RequestError) {
|
||||
this.alert = 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();
|
||||
}
|
||||
}
|
||||
}
|
115
js/src/common/components/ModalManager.tsx
Normal file
115
js/src/common/components/ModalManager.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import MicroModal from 'micromodal';
|
||||
|
||||
import Component from "../Component";
|
||||
import Modal from './Modal';
|
||||
|
||||
/**
|
||||
* The `ModalManager` component manages a modal dialog. Only one modal dialog
|
||||
* can be shown at once; loading a new component into the ModalManager will
|
||||
* overwrite the previous one.
|
||||
*/
|
||||
export default class ModalManager extends Component {
|
||||
showing: boolean;
|
||||
component: Modal;
|
||||
hideTimeout: number;
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
app.modal = this;
|
||||
|
||||
window.vnode = vnode;
|
||||
}
|
||||
|
||||
view() {
|
||||
return (
|
||||
<div className="ModalManager modal" id="Modal" onclick={this.onclick.bind(this)} key="modal">
|
||||
{this.component && m(this.component)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a modal dialog.
|
||||
*/
|
||||
show(component: Modal) {
|
||||
if (!(component instanceof Modal)) {
|
||||
throw new Error('The ModalManager component can only show Modal components');
|
||||
}
|
||||
|
||||
clearTimeout(this.hideTimeout);
|
||||
|
||||
this.showing = true;
|
||||
this.component = component;
|
||||
|
||||
// if (app.current) app.current.retain = true;
|
||||
|
||||
m.redraw(true);
|
||||
|
||||
if (!$('.modal-backdrop').length) {
|
||||
$('<div />').addClass('modal-backdrop')
|
||||
.appendTo('body');
|
||||
}
|
||||
|
||||
MicroModal.show('Modal', {
|
||||
awaitCloseAnimation: true,
|
||||
onClose: () => {
|
||||
$('.modal-backdrop').fadeOut(200, function () {
|
||||
this.remove();
|
||||
});
|
||||
|
||||
this.showing = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.onready();
|
||||
}
|
||||
|
||||
onclick(e) {
|
||||
if (e.target === this.element) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal dialog.
|
||||
*/
|
||||
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 the new modal dialog to disappear. Instead we will wait for a tiny
|
||||
// bit to give the `show` method the opportunity to prevent this from going
|
||||
// ahead.
|
||||
this.hideTimeout = setTimeout(() => MicroModal.close('Modal'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.$());
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,12 +2,21 @@ import Mithril from "mithril";
|
||||
|
||||
import Alert from "../components/Alert";
|
||||
|
||||
export interface RequestErrorResponse extends JSON {
|
||||
errors?: {
|
||||
code: string;
|
||||
source?: {
|
||||
pointer: string;
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export default class RequestError {
|
||||
status: number;
|
||||
responseText: string;
|
||||
options: Mithril.RequestOptions;
|
||||
xhr: XMLHttpRequest;
|
||||
response?: JSON;
|
||||
response?: RequestErrorResponse;
|
||||
alert?: Alert;
|
||||
|
||||
constructor(status, responseText, options, xhr) {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
* @param {function} compute The function which computes the value using the
|
||||
* dependent values.
|
||||
*/
|
||||
export default function computed(...dependentKeys: Array<string|Function>): () => any {
|
||||
export default function computed(...dependentKeys: string[]|Function[]): () => any {
|
||||
const keys = <string[]> dependentKeys.slice(0, -1);
|
||||
const compute = <Function> dependentKeys.slice(-1)[0];
|
||||
|
||||
|
@@ -8,7 +8,7 @@ export default function extractText(vdom: any): string {
|
||||
if (vdom instanceof Array) {
|
||||
return vdom.map(element => extractText(element)).join('');
|
||||
} else if (typeof vdom === 'object' && vdom !== null) {
|
||||
return extractText(vdom.children);
|
||||
return vdom.text || extractText(vdom.children);
|
||||
} else {
|
||||
return vdom;
|
||||
}
|
||||
|
@@ -1,9 +1,34 @@
|
||||
import prop from 'mithril/stream';
|
||||
|
||||
export default () => {
|
||||
m.withAttr = (key: string, cb: Function) => function () {
|
||||
cb(this.getAttribute(key) || this[key]);
|
||||
};
|
||||
const mo = global.m;
|
||||
|
||||
m.prop = prop;
|
||||
const m = function (comp, ...args) {
|
||||
const node = mo.apply(this, arguments);
|
||||
|
||||
if (!node.attrs) node.attrs = {};
|
||||
|
||||
if (node.attrs.bidi) {
|
||||
m.bidi(node, node.attrs.bidi);
|
||||
}
|
||||
|
||||
if (node.attrs.route) {
|
||||
node.attrs.href = node.attrs.route;
|
||||
node.attrs.tag = m.route.Link;
|
||||
|
||||
delete node.attrs.route;
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
|
||||
Object.keys(mo).forEach(key => m[key] = mo[key]);
|
||||
|
||||
m.withAttr = (key: string, cb: Function) => function () {
|
||||
cb(this.getAttribute(key) || this[key]);
|
||||
};
|
||||
|
||||
m.prop = prop;
|
||||
|
||||
global.m = m;
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import Component from '../../common/Component';
|
||||
import Button from '../../common/components/Button';
|
||||
// import LogInModal from './LogInModal';
|
||||
import LogInModal from './LogInModal';
|
||||
// import SignUpModal from './SignUpModal';
|
||||
import SessionDropdown from './SessionDropdown';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
|
186
js/src/forum/components/LogInModal.tsx
Normal file
186
js/src/forum/components/LogInModal.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import Stream from 'mithril/stream';
|
||||
|
||||
import Modal from '../../common/components/Modal';
|
||||
import {ComponentProps} from '../../common/Component';
|
||||
import ItemList from "../../common/utils/ItemList";
|
||||
import Button from "../../common/components/Button";
|
||||
|
||||
export interface LogInModalProps extends ComponentProps {
|
||||
username?: string;
|
||||
password?: string;
|
||||
remember?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `LogInModal` component displays a modal dialog with a login form.
|
||||
*
|
||||
* ### Props
|
||||
*
|
||||
* - `identification`
|
||||
* - `password`
|
||||
* - `remember`
|
||||
*/
|
||||
export default class LogInModal extends Modal<LogInModalProps> {
|
||||
/**
|
||||
* The value of the identification input.
|
||||
*/
|
||||
identification: Stream<string>;
|
||||
|
||||
/**
|
||||
* The value of the password input.
|
||||
*/
|
||||
password: Stream<string>;
|
||||
|
||||
/**
|
||||
* The value of the remember me input.
|
||||
*/
|
||||
remember: Stream<string>;
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
console.log('#oninit');
|
||||
|
||||
this.identification = m.prop(this.props.identification || '');
|
||||
|
||||
this.password = m.prop(this.props.password || '');
|
||||
|
||||
this.remember = m.prop(!!this.props.remember);
|
||||
}
|
||||
|
||||
className(): string {
|
||||
return 'LogInModal Modal--small';
|
||||
}
|
||||
|
||||
title(): string {
|
||||
return app.translator.transText('core.forum.log_in.title');
|
||||
}
|
||||
|
||||
content() {
|
||||
return [
|
||||
<div className="Modal-body">
|
||||
{this.body()}
|
||||
</div>,
|
||||
<div className="Modal-footer">
|
||||
{this.footer()}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
body() {
|
||||
return [
|
||||
// <LogInButtons/>,
|
||||
|
||||
<div className="Form Form--centered">
|
||||
{this.fields().toArray()}
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
fields() {
|
||||
const items = new ItemList();
|
||||
|
||||
items.add('identification', <div className="Form-group">
|
||||
<input className="FormControl" name="identification" type="text" placeholder={app.translator.transText('core.forum.log_in.username_or_email_placeholder')}
|
||||
bidi={this.identification}
|
||||
disabled={this.loading} />
|
||||
</div>, 30);
|
||||
|
||||
items.add('password', <div className="Form-group">
|
||||
<input className="FormControl" name="password" type="password" placeholder={app.translator.transText('core.forum.log_in.password_placeholder')}
|
||||
bidi={this.password}
|
||||
disabled={this.loading} />
|
||||
</div>, 20);
|
||||
|
||||
items.add('remember', <div className="Form-group">
|
||||
<div>
|
||||
<label className="checkbox">
|
||||
<input type="checkbox" bidi={this.remember} disabled={this.loading} />
|
||||
{app.translator.trans('core.forum.log_in.remember_me_label')}
|
||||
</label>
|
||||
</div>
|
||||
</div>, 10);
|
||||
|
||||
items.add('submit', <div className="Form-group">
|
||||
{Button.component({
|
||||
className: 'Button Button--primary Button--block',
|
||||
type: 'submit',
|
||||
loading: this.loading,
|
||||
children: app.translator.trans('core.forum.log_in.submit_button')
|
||||
})}
|
||||
</div>, -10);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
footer() {
|
||||
return [
|
||||
<p className="LogInModal-forgotPassword">
|
||||
<a onclick={this.forgotPassword.bind(this)}>{app.translator.trans('core.forum.log_in.forgot_password_link')}</a>
|
||||
</p>,
|
||||
|
||||
app.forum.attribute('allowSignUp') && (
|
||||
<p className="LogInModal-signUp">
|
||||
{app.translator.trans('core.forum.log_in.sign_up_text', {a: <a onclick={this.signUp.bind(this)}/> })}
|
||||
</p>
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the forgot password modal, prefilling it with an email if the user has
|
||||
* entered one.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
forgotPassword() {
|
||||
const email = this.identification();
|
||||
const props = email.indexOf('@') !== -1 ? {email} : undefined;
|
||||
|
||||
app.modal.show(new ForgotPasswordModal(props));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the sign up modal, prefilling it with an email/username/password if
|
||||
* the user has entered one.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
signUp() {
|
||||
const props = {password: this.password()};
|
||||
const identification = this.identification();
|
||||
props[identification.indexOf('@') !== -1 ? 'email' : 'username'] = identification;
|
||||
|
||||
// app.modal.show(new SignUpModal(props));
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
this.$(`[name="${this.identification() ? 'password' : 'identification'}"]`).select();
|
||||
}
|
||||
|
||||
onsubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const identification = this.identification();
|
||||
const password = this.password();
|
||||
const remember = this.remember();
|
||||
|
||||
app.session.login({identification, password, remember}, {errorHandler: this.onerror.bind(this)})
|
||||
.then(
|
||||
() => window.location.reload(),
|
||||
this.loaded.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
onerror(error) {
|
||||
if (error.status === 401) {
|
||||
error.alert.props.children = app.translator.trans('core.forum.log_in.invalid_login_message');
|
||||
}
|
||||
|
||||
super.onerror(error);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user