mirror of
https://github.com/flarum/framework.git
synced 2025-02-02 23:11:44 +08:00
chore: convert Dropdown
components to TS (#3608)
* chore: convert `Dropdown` components to TS * chore(review): `buttonClassName` technically not required * chore(review): `accessibleToggleLabel` technically not required * chore(review): use `classList` where possible * chore: `yarn format` * Update framework/core/js/src/common/components/Dropdown.tsx * chore(review): use `includes` * chore(review): define constant of excluded groups * chore(review): use `null coalesce` and `logical or` assignments * chore(review): `null coalesce` * chore(review): `any` to `typeof Component` * chore(review): `classList` * chore(review): `yarn format` * chore: fix typing issues after typescript update Signed-off-by: Sami Mazouz <sychocouldy@gmail.com> Co-authored-by: David Wheatley <hi@davwheat.dev>
This commit is contained in:
parent
0e238a9c82
commit
5bc47c0278
|
@ -40,6 +40,7 @@ export interface AdminApplicationData extends ApplicationData {
|
|||
modelStatistics: Record<string, { total: number }>;
|
||||
displayNameDrivers: string[];
|
||||
slugDrivers: Record<string, string[]>;
|
||||
permissions: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export default class AdminApplication extends Application {
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
import app from '../../admin/app';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
|
||||
import Button from '../../common/components/Button';
|
||||
import Separator from '../../common/components/Separator';
|
||||
import Group from '../../common/models/Group';
|
||||
import Badge from '../../common/components/Badge';
|
||||
import GroupBadge from '../../common/components/GroupBadge';
|
||||
import Mithril from 'mithril';
|
||||
|
||||
function badgeForId(id) {
|
||||
function badgeForId(id: string) {
|
||||
const group = app.store.getById('groups', id);
|
||||
|
||||
return group ? GroupBadge.component({ group, label: null }) : '';
|
||||
}
|
||||
|
||||
function filterByRequiredPermissions(groupIds, permission) {
|
||||
function filterByRequiredPermissions(groupIds: string[], permission: string) {
|
||||
app.getRequiredPermissions(permission).forEach((required) => {
|
||||
const restrictToGroupIds = app.data.permissions[required] || [];
|
||||
|
||||
|
@ -32,24 +33,28 @@ function filterByRequiredPermissions(groupIds, permission) {
|
|||
return groupIds;
|
||||
}
|
||||
|
||||
export default class PermissionDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
export interface IPermissionDropdownAttrs extends IDropdownAttrs {
|
||||
permission: string;
|
||||
}
|
||||
|
||||
export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownAttrs = IPermissionDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: IPermissionDropdownAttrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className = 'PermissionDropdown';
|
||||
attrs.buttonClassName = 'Button Button--text';
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
const children = [];
|
||||
|
||||
let groupIds = app.data.permissions[this.attrs.permission] || [];
|
||||
|
||||
groupIds = filterByRequiredPermissions(groupIds, this.attrs.permission);
|
||||
|
||||
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
|
||||
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
|
||||
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
||||
const everyone = groupIds.includes(Group.GUEST_ID);
|
||||
const members = groupIds.includes(Group.MEMBER_ID);
|
||||
const adminGroup = app.store.getById<Group>('groups', Group.ADMINISTRATOR_ID)!;
|
||||
|
||||
if (everyone) {
|
||||
this.attrs.label = Badge.component({ icon: 'fas fa-globe' });
|
||||
|
@ -89,40 +94,42 @@ export default class PermissionDropdown extends Dropdown {
|
|||
{
|
||||
icon: !everyone && !members ? 'fas fa-check' : true,
|
||||
disabled: !everyone && !members,
|
||||
onclick: (e) => {
|
||||
onclick: (e: MouseEvent) => {
|
||||
if (e.shiftKey) e.stopPropagation();
|
||||
this.save([]);
|
||||
},
|
||||
},
|
||||
[badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()]
|
||||
[badgeForId(adminGroup.id()!), ' ', adminGroup.namePlural()]
|
||||
)
|
||||
);
|
||||
|
||||
[].push.apply(
|
||||
children,
|
||||
app.store
|
||||
.all('groups')
|
||||
.filter((group) => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
||||
.map((group) =>
|
||||
Button.component(
|
||||
{
|
||||
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
|
||||
onclick: (e) => {
|
||||
if (e.shiftKey) e.stopPropagation();
|
||||
this.toggle(group.id());
|
||||
},
|
||||
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID),
|
||||
// These groups are defined above, appearing first in the list.
|
||||
const excludedGroups = [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID];
|
||||
|
||||
const groupButtons = app.store
|
||||
.all<Group>('groups')
|
||||
.filter((group) => !excludedGroups.includes(group.id()!))
|
||||
.map((group) =>
|
||||
Button.component(
|
||||
{
|
||||
icon: groupIds.includes(group.id()!) ? 'fas fa-check' : true,
|
||||
onclick: (e: MouseEvent) => {
|
||||
if (e.shiftKey) e.stopPropagation();
|
||||
this.toggle(group.id()!);
|
||||
},
|
||||
[badgeForId(group.id()), ' ', group.namePlural()]
|
||||
)
|
||||
disabled: this.isGroupDisabled(group.id()!) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID),
|
||||
},
|
||||
[badgeForId(group.id()!), ' ', group.namePlural()]
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
children.push(...groupButtons);
|
||||
}
|
||||
|
||||
return super.view({ ...vnode, children });
|
||||
}
|
||||
|
||||
save(groupIds) {
|
||||
save(groupIds: string[]) {
|
||||
const permission = this.attrs.permission;
|
||||
|
||||
app.data.permissions[permission] = groupIds;
|
||||
|
@ -134,7 +141,7 @@ export default class PermissionDropdown extends Dropdown {
|
|||
});
|
||||
}
|
||||
|
||||
toggle(groupId) {
|
||||
toggle(groupId: string) {
|
||||
const permission = this.attrs.permission;
|
||||
|
||||
let groupIds = app.data.permissions[permission] || [];
|
||||
|
@ -151,7 +158,7 @@ export default class PermissionDropdown extends Dropdown {
|
|||
this.save(groupIds);
|
||||
}
|
||||
|
||||
isGroupDisabled(id) {
|
||||
return filterByRequiredPermissions([id], this.attrs.permission).indexOf(id) === -1;
|
||||
isGroupDisabled(id: string) {
|
||||
return !filterByRequiredPermissions([id], this.attrs.permission).includes(id);
|
||||
}
|
||||
}
|
|
@ -1,16 +1,19 @@
|
|||
import app from '../../admin/app';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
|
||||
import Button from '../../common/components/Button';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export interface ISessionDropdownAttrs extends IDropdownAttrs {}
|
||||
|
||||
/**
|
||||
* The `SessionDropdown` component shows a button with the current user's
|
||||
* avatar/name, with a dropdown of session controls.
|
||||
*/
|
||||
export default class SessionDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
export default class SessionDropdown<CustomAttrs extends ISessionDropdownAttrs = ISessionDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: ISessionDropdownAttrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className = 'SessionDropdown';
|
||||
|
@ -18,7 +21,7 @@ export default class SessionDropdown extends Dropdown {
|
|||
attrs.menuClassName = 'Dropdown-menu--right';
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
return super.view({ ...vnode, children: this.items().toArray() });
|
||||
}
|
||||
|
||||
|
@ -30,11 +33,9 @@ export default class SessionDropdown extends Dropdown {
|
|||
|
||||
/**
|
||||
* Build an item list for the contents of the dropdown menu.
|
||||
*
|
||||
* @return {ItemList}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
items(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add(
|
||||
'logOut',
|
|
@ -1,10 +1,21 @@
|
|||
import app from '../app';
|
||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
||||
import SelectDropdown, { ISelectDropdownAttrs } from '../../common/components/SelectDropdown';
|
||||
import Button from '../../common/components/Button';
|
||||
import saveSettings from '../utils/saveSettings';
|
||||
import Mithril from 'mithril';
|
||||
|
||||
export default class SettingDropdown extends SelectDropdown {
|
||||
static initAttrs(attrs) {
|
||||
export type SettingDropdownOption = {
|
||||
value: any;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export interface ISettingDropdownAttrs extends ISelectDropdownAttrs {
|
||||
setting?: string;
|
||||
options: Array<SettingDropdownOption>;
|
||||
}
|
||||
|
||||
export default class SettingDropdown<CustomAttrs extends ISettingDropdownAttrs = ISettingDropdownAttrs> extends SelectDropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: ISettingDropdownAttrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className = 'SettingDropdown';
|
||||
|
@ -13,21 +24,21 @@ export default class SettingDropdown extends SelectDropdown {
|
|||
attrs.defaultLabel = 'Custom';
|
||||
|
||||
if ('key' in attrs) {
|
||||
attrs.setting = attrs.key;
|
||||
attrs.setting = attrs.key?.toString();
|
||||
delete attrs.key;
|
||||
}
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
return super.view({
|
||||
...vnode,
|
||||
children: this.attrs.options.map(({ value, label }) => {
|
||||
const active = app.data.settings[this.attrs.setting] === value;
|
||||
const active = app.data.settings[this.attrs.setting!] === value;
|
||||
|
||||
return Button.component(
|
||||
{
|
||||
icon: active ? 'fas fa-check' : true,
|
||||
onclick: saveSettings.bind(this, { [this.attrs.setting]: value }),
|
||||
onclick: saveSettings.bind(this, { [this.attrs.setting!]: value }),
|
||||
active,
|
||||
},
|
||||
label
|
|
@ -1,140 +0,0 @@
|
|||
import app from '../../common/app';
|
||||
import Component from '../Component';
|
||||
import icon from '../helpers/icon';
|
||||
import listItems from '../helpers/listItems';
|
||||
|
||||
/**
|
||||
* The `Dropdown` component displays a button which, when clicked, shows a
|
||||
* dropdown menu beneath it.
|
||||
*
|
||||
* ### Attrs
|
||||
*
|
||||
* - `buttonClassName` A class name to apply to the dropdown toggle button.
|
||||
* - `menuClassName` A class name to apply to the dropdown menu.
|
||||
* - `icon` The name of an icon to show in the dropdown toggle button.
|
||||
* - `caretIcon` The name of an icon to show on the right of the button.
|
||||
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
|
||||
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
|
||||
* - `onhide`
|
||||
* - `onshow`
|
||||
*
|
||||
* The children will be displayed as a list inside of the dropdown menu.
|
||||
*/
|
||||
export default class Dropdown extends Component {
|
||||
static initAttrs(attrs) {
|
||||
attrs.className = attrs.className || '';
|
||||
attrs.buttonClassName = attrs.buttonClassName || '';
|
||||
attrs.menuClassName = attrs.menuClassName || '';
|
||||
attrs.label = attrs.label || '';
|
||||
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
|
||||
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
|
||||
}
|
||||
|
||||
oninit(vnode) {
|
||||
super.oninit(vnode);
|
||||
|
||||
this.showing = false;
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
const items = vnode.children ? listItems(vnode.children) : [];
|
||||
const renderItems = this.attrs.lazyDraw ? this.showing : true;
|
||||
|
||||
return (
|
||||
<div className={'ButtonGroup Dropdown dropdown ' + this.attrs.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
|
||||
{this.getButton(vnode.children)}
|
||||
{renderItems && this.getMenu(items)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
// When opening the dropdown menu, work out if the menu goes beyond the
|
||||
// bottom of the viewport. If it does, we will apply class to make it show
|
||||
// above the toggle button instead of below it.
|
||||
this.$().on('shown.bs.dropdown', () => {
|
||||
const { lazyDraw, onshow } = this.attrs;
|
||||
|
||||
this.showing = true;
|
||||
|
||||
// If using lazy drawing, redraw before calling `onshow` function
|
||||
// to make sure the menu DOM exists in case the callback tries to use it.
|
||||
if (lazyDraw) {
|
||||
m.redraw.sync();
|
||||
}
|
||||
|
||||
if (typeof onshow === 'function') {
|
||||
onshow();
|
||||
}
|
||||
|
||||
// If not using lazy drawing, keep previous functionality
|
||||
// of redrawing after calling onshow()
|
||||
if (!lazyDraw) {
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
const $menu = this.$('.Dropdown-menu');
|
||||
const isRight = $menu.hasClass('Dropdown-menu--right');
|
||||
|
||||
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
|
||||
|
||||
$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
|
||||
|
||||
if ($menu.offset().top < 0) {
|
||||
$menu.removeClass('Dropdown-menu--top');
|
||||
}
|
||||
|
||||
$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
|
||||
});
|
||||
|
||||
this.$().on('hidden.bs.dropdown', () => {
|
||||
this.showing = false;
|
||||
|
||||
if (this.attrs.onhide) {
|
||||
this.attrs.onhide();
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the button.
|
||||
*
|
||||
* @return {import('mithril').Children}
|
||||
* @protected
|
||||
*/
|
||||
getButton(children) {
|
||||
return (
|
||||
<button
|
||||
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
|
||||
aria-haspopup="menu"
|
||||
aria-label={this.attrs.accessibleToggleLabel}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.attrs.onclick}
|
||||
>
|
||||
{this.getButtonContent(children)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the button's content.
|
||||
*
|
||||
* @return {import('mithril').Children}
|
||||
* @protected
|
||||
*/
|
||||
getButtonContent(children) {
|
||||
return [
|
||||
this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : '',
|
||||
<span className="Button-label">{this.attrs.label}</span>,
|
||||
this.attrs.caretIcon ? icon(this.attrs.caretIcon, { className: 'Button-caret' }) : '',
|
||||
];
|
||||
}
|
||||
|
||||
getMenu(items) {
|
||||
return <ul className={'Dropdown-menu dropdown-menu ' + this.attrs.menuClassName}>{items}</ul>;
|
||||
}
|
||||
}
|
152
framework/core/js/src/common/components/Dropdown.tsx
Normal file
152
framework/core/js/src/common/components/Dropdown.tsx
Normal file
|
@ -0,0 +1,152 @@
|
|||
import app from '../../common/app';
|
||||
import Component, { ComponentAttrs } from '../Component';
|
||||
import icon from '../helpers/icon';
|
||||
import listItems, { ModdedChildrenWithItemName } from '../helpers/listItems';
|
||||
import extractText from '../utils/extractText';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export interface IDropdownAttrs extends ComponentAttrs {
|
||||
/** A class name to apply to the dropdown toggle button. */
|
||||
buttonClassName?: string;
|
||||
/** A class name to apply to the dropdown menu. */
|
||||
menuClassName?: string;
|
||||
/** The name of an icon to show in the dropdown toggle button. */
|
||||
icon?: string;
|
||||
/** The name of an icon to show on the right of the button. */
|
||||
caretIcon?: string;
|
||||
/** The label of the dropdown toggle button. Defaults to 'Controls'. */
|
||||
label: Mithril.Children;
|
||||
/** The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'. */
|
||||
accessibleToggleLabel?: string;
|
||||
/** An action to take when the dropdown is collapsed. */
|
||||
onhide?: () => void;
|
||||
/** An action to take when the dropdown is opened. */
|
||||
onshow?: () => void;
|
||||
|
||||
lazyDraw?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `Dropdown` component displays a button which, when clicked, shows a
|
||||
* dropdown menu beneath it.
|
||||
*
|
||||
* The children will be displayed as a list inside the dropdown menu.
|
||||
*/
|
||||
export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttrs> extends Component<CustomAttrs> {
|
||||
protected showing = false;
|
||||
|
||||
static initAttrs(attrs: IDropdownAttrs) {
|
||||
attrs.className ||= '';
|
||||
attrs.buttonClassName ||= '';
|
||||
attrs.menuClassName ||= '';
|
||||
attrs.label ||= '';
|
||||
attrs.caretIcon ??= 'fas fa-caret-down';
|
||||
attrs.accessibleToggleLabel ||= extractText(app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label'));
|
||||
}
|
||||
|
||||
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
const items = vnode.children ? listItems(vnode.children as ModdedChildrenWithItemName[]) : [];
|
||||
const renderItems = this.attrs.lazyDraw ? this.showing : true;
|
||||
|
||||
return (
|
||||
<div className={'ButtonGroup Dropdown dropdown ' + this.attrs.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
|
||||
{this.getButton(vnode.children as Mithril.ChildArray)}
|
||||
{renderItems && this.getMenu(items)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
|
||||
super.oncreate(vnode);
|
||||
|
||||
// When opening the dropdown menu, work out if the menu goes beyond the
|
||||
// bottom of the viewport. If it does, we will apply class to make it show
|
||||
// above the toggle button instead of below it.
|
||||
this.$().on('shown.bs.dropdown', () => {
|
||||
const { lazyDraw, onshow } = this.attrs;
|
||||
|
||||
this.showing = true;
|
||||
|
||||
// If using lazy drawing, redraw before calling `onshow` function
|
||||
// to make sure the menu DOM exists in case the callback tries to use it.
|
||||
if (lazyDraw) {
|
||||
m.redraw.sync();
|
||||
}
|
||||
|
||||
if (typeof onshow === 'function') {
|
||||
onshow();
|
||||
}
|
||||
|
||||
// If not using lazy drawing, keep previous functionality
|
||||
// of redrawing after calling onshow()
|
||||
if (!lazyDraw) {
|
||||
m.redraw();
|
||||
}
|
||||
|
||||
const $menu = this.$('.Dropdown-menu');
|
||||
const isRight = $menu.hasClass('Dropdown-menu--right');
|
||||
|
||||
const top = $menu.offset()?.top ?? 0;
|
||||
const height = $menu.height() ?? 0;
|
||||
const windowSrollTop = $(window).scrollTop() ?? 0;
|
||||
const windowHeight = $(window).height() ?? 0;
|
||||
|
||||
$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');
|
||||
|
||||
$menu.toggleClass('Dropdown-menu--top', top + height > windowSrollTop + windowHeight);
|
||||
|
||||
if (($menu.offset()?.top || 0) < 0) {
|
||||
$menu.removeClass('Dropdown-menu--top');
|
||||
}
|
||||
|
||||
const left = $menu.offset()?.left ?? 0;
|
||||
const width = $menu.width() ?? 0;
|
||||
const windowScrollLeft = $(window).scrollLeft() ?? 0;
|
||||
const windowWidth = $(window).width() ?? 0;
|
||||
|
||||
$menu.toggleClass('Dropdown-menu--right', isRight || left + width > windowScrollLeft + windowWidth);
|
||||
});
|
||||
|
||||
this.$().on('hidden.bs.dropdown', () => {
|
||||
this.showing = false;
|
||||
|
||||
if (this.attrs.onhide) {
|
||||
this.attrs.onhide();
|
||||
}
|
||||
|
||||
m.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the button.
|
||||
*/
|
||||
getButton(children: Mithril.ChildArray): Mithril.Vnode<any, any> {
|
||||
return (
|
||||
<button
|
||||
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
|
||||
aria-haspopup="menu"
|
||||
aria-label={this.attrs.accessibleToggleLabel}
|
||||
data-toggle="dropdown"
|
||||
onclick={this.attrs.onclick}
|
||||
>
|
||||
{this.getButtonContent(children)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the template for the button's content.
|
||||
*/
|
||||
getButtonContent(children: Mithril.ChildArray): Mithril.ChildArray {
|
||||
return [
|
||||
this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : '',
|
||||
<span className="Button-label">{this.attrs.label}</span>,
|
||||
this.attrs.caretIcon ? icon(this.attrs.caretIcon, { className: 'Button-caret' }) : '',
|
||||
];
|
||||
}
|
||||
|
||||
getMenu(items: Mithril.Vnode<any, any>[]): Mithril.Vnode<any, any> {
|
||||
return <ul className={'Dropdown-menu dropdown-menu ' + this.attrs.menuClassName}>{items}</ul>;
|
||||
}
|
||||
}
|
|
@ -47,7 +47,7 @@ export default class Navigation extends Component {
|
|||
icon: 'fas fa-chevron-left',
|
||||
'aria-label': previous?.title,
|
||||
onclick: (e: MouseEvent) => {
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey || e.which === 2) return;
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey || e.button === 1) return;
|
||||
e.preventDefault();
|
||||
history?.back();
|
||||
},
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
import Dropdown from './Dropdown';
|
||||
import icon from '../helpers/icon';
|
||||
|
||||
/**
|
||||
* Determines via a vnode is currently "active".
|
||||
* Due to changes in Mithril 2, attrs will not be instantiated until AFTER view()
|
||||
* is initially called on the parent component, so we can not always depend on the
|
||||
* active attr to determine which element should be displayed as the "active child".
|
||||
*
|
||||
* This is a temporary patch, and as so, is not exported / placed in utils.
|
||||
*/
|
||||
function isActive(vnode) {
|
||||
const tag = vnode.tag;
|
||||
|
||||
// Allow non-selectable dividers/headers to be added.
|
||||
if (typeof tag === 'string' && tag !== 'a' && tag !== 'button') return false;
|
||||
|
||||
if ('initAttrs' in tag) {
|
||||
tag.initAttrs(vnode.attrs);
|
||||
}
|
||||
|
||||
return 'isActive' in tag ? tag.isActive(vnode.attrs) : vnode.attrs.active;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
|
||||
* button's label is set as the label of the first child which has a truthy
|
||||
* `active` prop.
|
||||
*
|
||||
* ### Attrs
|
||||
*
|
||||
* - `caretIcon`
|
||||
* - `defaultLabel`
|
||||
*/
|
||||
export default class SelectDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-sort';
|
||||
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className += ' Dropdown--select';
|
||||
}
|
||||
|
||||
getButtonContent(children) {
|
||||
const activeChild = children.find(isActive);
|
||||
let label = (activeChild && activeChild.children) || this.attrs.defaultLabel;
|
||||
|
||||
if (label instanceof Array) label = label[0];
|
||||
|
||||
return [<span className="Button-label">{label}</span>, icon(this.attrs.caretIcon, { className: 'Button-caret' })];
|
||||
}
|
||||
}
|
57
framework/core/js/src/common/components/SelectDropdown.tsx
Normal file
57
framework/core/js/src/common/components/SelectDropdown.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import Dropdown, { IDropdownAttrs } from './Dropdown';
|
||||
import icon from '../helpers/icon';
|
||||
import extractText from '../utils/extractText';
|
||||
import classList from '../utils/classList';
|
||||
import type Component from '../Component';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
/**
|
||||
* Determines via a vnode is currently "active".
|
||||
* Due to changes in Mithril 2, attrs will not be instantiated until AFTER view()
|
||||
* is initially called on the parent component, so we can not always depend on the
|
||||
* active attr to determine which element should be displayed as the "active child".
|
||||
*
|
||||
* This is a temporary patch, and as so, is not exported / placed in utils.
|
||||
*/
|
||||
function isActive(vnode: Mithril.Children): boolean {
|
||||
if (!vnode || typeof vnode !== 'object' || vnode instanceof Array) return false;
|
||||
|
||||
const tag = vnode.tag;
|
||||
|
||||
// Allow non-selectable dividers/headers to be added.
|
||||
if (typeof tag === 'string' && tag !== 'a' && tag !== 'button') return false;
|
||||
|
||||
if (typeof tag === 'object' && 'initAttrs' in tag) {
|
||||
(tag as unknown as typeof Component).initAttrs(vnode.attrs);
|
||||
}
|
||||
|
||||
return typeof tag === 'object' && 'isActive' in tag ? (tag as any).isActive(vnode.attrs) : vnode.attrs.active;
|
||||
}
|
||||
|
||||
export interface ISelectDropdownAttrs extends IDropdownAttrs {
|
||||
defaultLabel: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `SelectDropdown` component is the same as a `Dropdown`, except the toggle
|
||||
* button's label is set as the label of the first child which has a truthy
|
||||
* `active` prop.
|
||||
*/
|
||||
export default class SelectDropdown<CustomAttrs extends ISelectDropdownAttrs = ISelectDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: ISelectDropdownAttrs) {
|
||||
attrs.caretIcon ??= 'fas fa-sort';
|
||||
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className = classList(attrs.className, 'Dropdown--select');
|
||||
}
|
||||
|
||||
getButtonContent(children: Mithril.ChildArray): Mithril.ChildArray {
|
||||
const activeChild = children.find(isActive);
|
||||
let label = (activeChild && typeof activeChild === 'object' && 'children' in activeChild && activeChild.children) || this.attrs.defaultLabel;
|
||||
|
||||
label = extractText(label);
|
||||
|
||||
return [<span className="Button-label">{label}</span>, this.attrs.caretIcon ? icon(this.attrs.caretIcon, { className: 'Button-caret' }) : null];
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
import Dropdown from './Dropdown';
|
||||
import Button from './Button';
|
||||
import icon from '../helpers/icon';
|
||||
|
||||
/**
|
||||
* The `SplitDropdown` component is similar to `Dropdown`, but the first child
|
||||
* is displayed as its own button prior to the toggle button.
|
||||
*/
|
||||
export default class SplitDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className += ' Dropdown--split';
|
||||
attrs.menuClassName += ' Dropdown-menu--right';
|
||||
}
|
||||
|
||||
getButton(children) {
|
||||
// Make a copy of the attrs of the first child component. We will assign
|
||||
// these attrs to a new button, so that it has exactly the same behaviour as
|
||||
// the first child.
|
||||
const firstChild = this.getFirstChild(children);
|
||||
const buttonAttrs = Object.assign({}, firstChild.attrs);
|
||||
buttonAttrs.className = (buttonAttrs.className || '') + ' SplitDropdown-button Button ' + this.attrs.buttonClassName;
|
||||
|
||||
return [
|
||||
Button.component(buttonAttrs, firstChild.children),
|
||||
<button
|
||||
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
|
||||
aria-haspopup="menu"
|
||||
aria-label={this.attrs.accessibleToggleLabel}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
{icon(this.attrs.icon, { className: 'Button-icon' })}
|
||||
{icon('fas fa-caret-down', { className: 'Button-caret' })}
|
||||
</button>,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child. If the first child is an array, the first item in that
|
||||
* array will be returned.
|
||||
*
|
||||
* @param {unknown[] | unknown} children
|
||||
* @return {unknown}
|
||||
* @protected
|
||||
*/
|
||||
getFirstChild(children) {
|
||||
let firstChild = children;
|
||||
|
||||
while (firstChild instanceof Array) firstChild = firstChild[0];
|
||||
|
||||
return firstChild;
|
||||
}
|
||||
}
|
56
framework/core/js/src/common/components/SplitDropdown.tsx
Normal file
56
framework/core/js/src/common/components/SplitDropdown.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
import Dropdown, { IDropdownAttrs } from './Dropdown';
|
||||
import Button from './Button';
|
||||
import icon from '../helpers/icon';
|
||||
import Mithril from 'mithril';
|
||||
import classList from '../utils/classList';
|
||||
|
||||
export interface ISplitDropdownAttrs extends IDropdownAttrs {}
|
||||
|
||||
/**
|
||||
* The `SplitDropdown` component is similar to `Dropdown`, but the first child
|
||||
* is displayed as its own button prior to the toggle button.
|
||||
*/
|
||||
export default class SplitDropdown extends Dropdown {
|
||||
static initAttrs(attrs: ISplitDropdownAttrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className = classList(attrs.className, 'Dropdown--split');
|
||||
attrs.menuClassName = classList(attrs.menuClassName, 'Dropdown-menu--right');
|
||||
}
|
||||
|
||||
getButton(children: Mithril.ChildArray): Mithril.Vnode<any, any> {
|
||||
// Make a copy of the attrs of the first child component. We will assign
|
||||
// these attrs to a new button, so that it has exactly the same behaviour as
|
||||
// the first child.
|
||||
const firstChild = this.getFirstChild(children);
|
||||
const buttonAttrs = Object.assign({}, firstChild?.attrs);
|
||||
buttonAttrs.className = classList(buttonAttrs.className, 'SplitDropdown-button Button', this.attrs.buttonClassName);
|
||||
|
||||
return (
|
||||
<>
|
||||
{Button.component(buttonAttrs, firstChild.children)}
|
||||
<button
|
||||
className={'Dropdown-toggle Button Button--icon ' + this.attrs.buttonClassName}
|
||||
aria-haspopup="menu"
|
||||
aria-label={this.attrs.accessibleToggleLabel}
|
||||
data-toggle="dropdown"
|
||||
>
|
||||
{this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : null}
|
||||
{icon('fas fa-caret-down', { className: 'Button-caret' })}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child. If the first child is an array, the first item in that
|
||||
* array will be returned.
|
||||
*/
|
||||
protected getFirstChild(children: Mithril.Children): Mithril.Vnode<any, any> {
|
||||
let firstChild = children;
|
||||
|
||||
while (firstChild instanceof Array) firstChild = firstChild[0];
|
||||
|
||||
return firstChild as Mithril.Vnode<any, any>;
|
||||
}
|
||||
}
|
|
@ -126,7 +126,7 @@ export default class ForumApplication extends Application {
|
|||
// Route the home link back home when clicked. We do not want it to register
|
||||
// if the user is opening it in a new tab, however.
|
||||
document.getElementById('home-link')!.addEventListener('click', (e) => {
|
||||
if (e.ctrlKey || e.metaKey || e.which === 2) return;
|
||||
if (e.ctrlKey || e.metaKey || e.button === 1) return;
|
||||
e.preventDefault();
|
||||
app.history.home();
|
||||
|
||||
|
|
|
@ -1,26 +1,31 @@
|
|||
import app from '../../forum/app';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
|
||||
import icon from '../../common/helpers/icon';
|
||||
import classList from '../../common/utils/classList';
|
||||
import NotificationList from './NotificationList';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export default class NotificationsDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
attrs.className = attrs.className || 'NotificationsDropdown';
|
||||
attrs.buttonClassName = attrs.buttonClassName || 'Button Button--flat';
|
||||
attrs.menuClassName = attrs.menuClassName || 'Dropdown-menu--right';
|
||||
attrs.label = attrs.label || app.translator.trans('core.forum.notifications.tooltip');
|
||||
attrs.icon = attrs.icon || 'fas fa-bell';
|
||||
export interface INotificationsDropdown extends IDropdownAttrs {}
|
||||
|
||||
export default class NotificationsDropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: INotificationsDropdown) {
|
||||
attrs.className ||= 'NotificationsDropdown';
|
||||
attrs.buttonClassName ||= 'Button Button--flat';
|
||||
attrs.menuClassName ||= 'Dropdown-menu--right';
|
||||
attrs.label ||= extractText(app.translator.trans('core.forum.notifications.tooltip'));
|
||||
attrs.icon ||= 'fas fa-bell';
|
||||
|
||||
// For best a11y support, both `title` and `aria-label` should be used
|
||||
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.forum.notifications.toggle_dropdown_accessible_label');
|
||||
attrs.accessibleToggleLabel ||= extractText(app.translator.trans('core.forum.notifications.toggle_dropdown_accessible_label'));
|
||||
|
||||
super.initAttrs(attrs);
|
||||
}
|
||||
|
||||
getButton() {
|
||||
getButton(children: Mithril.ChildArray): Mithril.Vnode<any, any> {
|
||||
const newNotifications = this.getNewCount();
|
||||
const vdom = super.getButton();
|
||||
|
||||
const vdom = super.getButton(children);
|
||||
|
||||
vdom.attrs.title = this.attrs.label;
|
||||
|
||||
|
@ -30,11 +35,11 @@ export default class NotificationsDropdown extends Dropdown {
|
|||
return vdom;
|
||||
}
|
||||
|
||||
getButtonContent() {
|
||||
getButtonContent(): Mithril.ChildArray {
|
||||
const unread = this.getUnreadCount();
|
||||
|
||||
return [
|
||||
icon(this.attrs.icon, { className: 'Button-icon' }),
|
||||
this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : null,
|
||||
unread !== 0 && <span className="NotificationsDropdown-unread">{unread}</span>,
|
||||
<span className="Button-label">{this.attrs.label}</span>,
|
||||
];
|
||||
|
@ -61,16 +66,16 @@ export default class NotificationsDropdown extends Dropdown {
|
|||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return app.session.user.unreadNotificationCount();
|
||||
return app.session.user!.unreadNotificationCount();
|
||||
}
|
||||
|
||||
getNewCount() {
|
||||
return app.session.user.newNotificationCount();
|
||||
return app.session.user!.newNotificationCount();
|
||||
}
|
||||
|
||||
menuClick(e) {
|
||||
menuClick(e: MouseEvent) {
|
||||
// Don't close the notifications dropdown if the user is opening a link in a
|
||||
// new tab or window.
|
||||
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) e.stopPropagation();
|
||||
if (e.shiftKey || e.metaKey || e.ctrlKey || e.button === 1) e.stopPropagation();
|
||||
}
|
||||
}
|
|
@ -1,28 +1,32 @@
|
|||
import app from '../../forum/app';
|
||||
import avatar from '../../common/helpers/avatar';
|
||||
import username from '../../common/helpers/username';
|
||||
import Dropdown from '../../common/components/Dropdown';
|
||||
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
|
||||
import LinkButton from '../../common/components/LinkButton';
|
||||
import Button from '../../common/components/Button';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import Separator from '../../common/components/Separator';
|
||||
import extractText from '../../common/utils/extractText';
|
||||
import type Mithril from 'mithril';
|
||||
|
||||
export interface ISessionDropdownAttrs extends IDropdownAttrs {}
|
||||
|
||||
/**
|
||||
* The `SessionDropdown` component shows a button with the current user's
|
||||
* avatar/name, with a dropdown of session controls.
|
||||
*/
|
||||
export default class SessionDropdown extends Dropdown {
|
||||
static initAttrs(attrs) {
|
||||
export default class SessionDropdown<CustomAttrs extends ISessionDropdownAttrs = ISessionDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||
static initAttrs(attrs: ISessionDropdownAttrs) {
|
||||
super.initAttrs(attrs);
|
||||
|
||||
attrs.className = 'SessionDropdown';
|
||||
attrs.buttonClassName = 'Button Button--user Button--flat';
|
||||
attrs.menuClassName = 'Dropdown-menu--right';
|
||||
|
||||
attrs.accessibleToggleLabel = app.translator.trans('core.forum.header.session_dropdown_accessible_label');
|
||||
attrs.accessibleToggleLabel = extractText(app.translator.trans('core.forum.header.session_dropdown_accessible_label'));
|
||||
}
|
||||
|
||||
view(vnode) {
|
||||
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
return super.view({ ...vnode, children: this.items().toArray() });
|
||||
}
|
||||
|
||||
|
@ -34,12 +38,10 @@ export default class SessionDropdown extends Dropdown {
|
|||
|
||||
/**
|
||||
* Build an item list for the contents of the dropdown menu.
|
||||
*
|
||||
* @return {ItemList<import('mithril').Children>}
|
||||
*/
|
||||
items() {
|
||||
const items = new ItemList();
|
||||
const user = app.session.user;
|
||||
items(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
const user = app.session.user!;
|
||||
|
||||
items.add(
|
||||
'profile',
|
Loading…
Reference in New Issue
Block a user