mirror of
https://github.com/flarum/framework.git
synced 2025-02-09 10:39:47 +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 }>;
|
modelStatistics: Record<string, { total: number }>;
|
||||||
displayNameDrivers: string[];
|
displayNameDrivers: string[];
|
||||||
slugDrivers: Record<string, string[]>;
|
slugDrivers: Record<string, string[]>;
|
||||||
|
permissions: Record<string, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AdminApplication extends Application {
|
export default class AdminApplication extends Application {
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import app from '../../admin/app';
|
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 Button from '../../common/components/Button';
|
||||||
import Separator from '../../common/components/Separator';
|
import Separator from '../../common/components/Separator';
|
||||||
import Group from '../../common/models/Group';
|
import Group from '../../common/models/Group';
|
||||||
import Badge from '../../common/components/Badge';
|
import Badge from '../../common/components/Badge';
|
||||||
import GroupBadge from '../../common/components/GroupBadge';
|
import GroupBadge from '../../common/components/GroupBadge';
|
||||||
|
import Mithril from 'mithril';
|
||||||
|
|
||||||
function badgeForId(id) {
|
function badgeForId(id: string) {
|
||||||
const group = app.store.getById('groups', id);
|
const group = app.store.getById('groups', id);
|
||||||
|
|
||||||
return group ? GroupBadge.component({ group, label: null }) : '';
|
return group ? GroupBadge.component({ group, label: null }) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterByRequiredPermissions(groupIds, permission) {
|
function filterByRequiredPermissions(groupIds: string[], permission: string) {
|
||||||
app.getRequiredPermissions(permission).forEach((required) => {
|
app.getRequiredPermissions(permission).forEach((required) => {
|
||||||
const restrictToGroupIds = app.data.permissions[required] || [];
|
const restrictToGroupIds = app.data.permissions[required] || [];
|
||||||
|
|
||||||
|
@ -32,24 +33,28 @@ function filterByRequiredPermissions(groupIds, permission) {
|
||||||
return groupIds;
|
return groupIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class PermissionDropdown extends Dropdown {
|
export interface IPermissionDropdownAttrs extends IDropdownAttrs {
|
||||||
static initAttrs(attrs) {
|
permission: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownAttrs = IPermissionDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||||
|
static initAttrs(attrs: IPermissionDropdownAttrs) {
|
||||||
super.initAttrs(attrs);
|
super.initAttrs(attrs);
|
||||||
|
|
||||||
attrs.className = 'PermissionDropdown';
|
attrs.className = 'PermissionDropdown';
|
||||||
attrs.buttonClassName = 'Button Button--text';
|
attrs.buttonClassName = 'Button Button--text';
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
const children = [];
|
const children = [];
|
||||||
|
|
||||||
let groupIds = app.data.permissions[this.attrs.permission] || [];
|
let groupIds = app.data.permissions[this.attrs.permission] || [];
|
||||||
|
|
||||||
groupIds = filterByRequiredPermissions(groupIds, this.attrs.permission);
|
groupIds = filterByRequiredPermissions(groupIds, this.attrs.permission);
|
||||||
|
|
||||||
const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
|
const everyone = groupIds.includes(Group.GUEST_ID);
|
||||||
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
|
const members = groupIds.includes(Group.MEMBER_ID);
|
||||||
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
|
const adminGroup = app.store.getById<Group>('groups', Group.ADMINISTRATOR_ID)!;
|
||||||
|
|
||||||
if (everyone) {
|
if (everyone) {
|
||||||
this.attrs.label = Badge.component({ icon: 'fas fa-globe' });
|
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,
|
icon: !everyone && !members ? 'fas fa-check' : true,
|
||||||
disabled: !everyone && !members,
|
disabled: !everyone && !members,
|
||||||
onclick: (e) => {
|
onclick: (e: MouseEvent) => {
|
||||||
if (e.shiftKey) e.stopPropagation();
|
if (e.shiftKey) e.stopPropagation();
|
||||||
this.save([]);
|
this.save([]);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
[badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()]
|
[badgeForId(adminGroup.id()!), ' ', adminGroup.namePlural()]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
[].push.apply(
|
// These groups are defined above, appearing first in the list.
|
||||||
children,
|
const excludedGroups = [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID];
|
||||||
app.store
|
|
||||||
.all('groups')
|
const groupButtons = app.store
|
||||||
.filter((group) => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
|
.all<Group>('groups')
|
||||||
.map((group) =>
|
.filter((group) => !excludedGroups.includes(group.id()!))
|
||||||
Button.component(
|
.map((group) =>
|
||||||
{
|
Button.component(
|
||||||
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
|
{
|
||||||
onclick: (e) => {
|
icon: groupIds.includes(group.id()!) ? 'fas fa-check' : true,
|
||||||
if (e.shiftKey) e.stopPropagation();
|
onclick: (e: MouseEvent) => {
|
||||||
this.toggle(group.id());
|
if (e.shiftKey) e.stopPropagation();
|
||||||
},
|
this.toggle(group.id()!);
|
||||||
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_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 });
|
return super.view({ ...vnode, children });
|
||||||
}
|
}
|
||||||
|
|
||||||
save(groupIds) {
|
save(groupIds: string[]) {
|
||||||
const permission = this.attrs.permission;
|
const permission = this.attrs.permission;
|
||||||
|
|
||||||
app.data.permissions[permission] = groupIds;
|
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;
|
const permission = this.attrs.permission;
|
||||||
|
|
||||||
let groupIds = app.data.permissions[permission] || [];
|
let groupIds = app.data.permissions[permission] || [];
|
||||||
|
@ -151,7 +158,7 @@ export default class PermissionDropdown extends Dropdown {
|
||||||
this.save(groupIds);
|
this.save(groupIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
isGroupDisabled(id) {
|
isGroupDisabled(id: string) {
|
||||||
return filterByRequiredPermissions([id], this.attrs.permission).indexOf(id) === -1;
|
return !filterByRequiredPermissions([id], this.attrs.permission).includes(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,16 +1,19 @@
|
||||||
import app from '../../admin/app';
|
import app from '../../admin/app';
|
||||||
import avatar from '../../common/helpers/avatar';
|
import avatar from '../../common/helpers/avatar';
|
||||||
import username from '../../common/helpers/username';
|
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 Button from '../../common/components/Button';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
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
|
* The `SessionDropdown` component shows a button with the current user's
|
||||||
* avatar/name, with a dropdown of session controls.
|
* avatar/name, with a dropdown of session controls.
|
||||||
*/
|
*/
|
||||||
export default class SessionDropdown extends Dropdown {
|
export default class SessionDropdown<CustomAttrs extends ISessionDropdownAttrs = ISessionDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||||
static initAttrs(attrs) {
|
static initAttrs(attrs: ISessionDropdownAttrs) {
|
||||||
super.initAttrs(attrs);
|
super.initAttrs(attrs);
|
||||||
|
|
||||||
attrs.className = 'SessionDropdown';
|
attrs.className = 'SessionDropdown';
|
||||||
|
@ -18,7 +21,7 @@ export default class SessionDropdown extends Dropdown {
|
||||||
attrs.menuClassName = 'Dropdown-menu--right';
|
attrs.menuClassName = 'Dropdown-menu--right';
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
return super.view({ ...vnode, children: this.items().toArray() });
|
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.
|
* Build an item list for the contents of the dropdown menu.
|
||||||
*
|
|
||||||
* @return {ItemList}
|
|
||||||
*/
|
*/
|
||||||
items() {
|
items(): ItemList<Mithril.Children> {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'logOut',
|
'logOut',
|
|
@ -1,10 +1,21 @@
|
||||||
import app from '../app';
|
import app from '../app';
|
||||||
import SelectDropdown from '../../common/components/SelectDropdown';
|
import SelectDropdown, { ISelectDropdownAttrs } from '../../common/components/SelectDropdown';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import saveSettings from '../utils/saveSettings';
|
import saveSettings from '../utils/saveSettings';
|
||||||
|
import Mithril from 'mithril';
|
||||||
|
|
||||||
export default class SettingDropdown extends SelectDropdown {
|
export type SettingDropdownOption = {
|
||||||
static initAttrs(attrs) {
|
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);
|
super.initAttrs(attrs);
|
||||||
|
|
||||||
attrs.className = 'SettingDropdown';
|
attrs.className = 'SettingDropdown';
|
||||||
|
@ -13,21 +24,21 @@ export default class SettingDropdown extends SelectDropdown {
|
||||||
attrs.defaultLabel = 'Custom';
|
attrs.defaultLabel = 'Custom';
|
||||||
|
|
||||||
if ('key' in attrs) {
|
if ('key' in attrs) {
|
||||||
attrs.setting = attrs.key;
|
attrs.setting = attrs.key?.toString();
|
||||||
delete attrs.key;
|
delete attrs.key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
view(vnode) {
|
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||||
return super.view({
|
return super.view({
|
||||||
...vnode,
|
...vnode,
|
||||||
children: this.attrs.options.map(({ value, label }) => {
|
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(
|
return Button.component(
|
||||||
{
|
{
|
||||||
icon: active ? 'fas fa-check' : true,
|
icon: active ? 'fas fa-check' : true,
|
||||||
onclick: saveSettings.bind(this, { [this.attrs.setting]: value }),
|
onclick: saveSettings.bind(this, { [this.attrs.setting!]: value }),
|
||||||
active,
|
active,
|
||||||
},
|
},
|
||||||
label
|
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',
|
icon: 'fas fa-chevron-left',
|
||||||
'aria-label': previous?.title,
|
'aria-label': previous?.title,
|
||||||
onclick: (e: MouseEvent) => {
|
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();
|
e.preventDefault();
|
||||||
history?.back();
|
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
|
// 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.
|
// if the user is opening it in a new tab, however.
|
||||||
document.getElementById('home-link')!.addEventListener('click', (e) => {
|
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();
|
e.preventDefault();
|
||||||
app.history.home();
|
app.history.home();
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,31 @@
|
||||||
import app from '../../forum/app';
|
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 icon from '../../common/helpers/icon';
|
||||||
import classList from '../../common/utils/classList';
|
import classList from '../../common/utils/classList';
|
||||||
import NotificationList from './NotificationList';
|
import NotificationList from './NotificationList';
|
||||||
|
import extractText from '../../common/utils/extractText';
|
||||||
|
import type Mithril from 'mithril';
|
||||||
|
|
||||||
export default class NotificationsDropdown extends Dropdown {
|
export interface INotificationsDropdown extends IDropdownAttrs {}
|
||||||
static initAttrs(attrs) {
|
|
||||||
attrs.className = attrs.className || 'NotificationsDropdown';
|
export default class NotificationsDropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||||
attrs.buttonClassName = attrs.buttonClassName || 'Button Button--flat';
|
static initAttrs(attrs: INotificationsDropdown) {
|
||||||
attrs.menuClassName = attrs.menuClassName || 'Dropdown-menu--right';
|
attrs.className ||= 'NotificationsDropdown';
|
||||||
attrs.label = attrs.label || app.translator.trans('core.forum.notifications.tooltip');
|
attrs.buttonClassName ||= 'Button Button--flat';
|
||||||
attrs.icon = attrs.icon || 'fas fa-bell';
|
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
|
// 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);
|
super.initAttrs(attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
getButton() {
|
getButton(children: Mithril.ChildArray): Mithril.Vnode<any, any> {
|
||||||
const newNotifications = this.getNewCount();
|
const newNotifications = this.getNewCount();
|
||||||
const vdom = super.getButton();
|
|
||||||
|
const vdom = super.getButton(children);
|
||||||
|
|
||||||
vdom.attrs.title = this.attrs.label;
|
vdom.attrs.title = this.attrs.label;
|
||||||
|
|
||||||
|
@ -30,11 +35,11 @@ export default class NotificationsDropdown extends Dropdown {
|
||||||
return vdom;
|
return vdom;
|
||||||
}
|
}
|
||||||
|
|
||||||
getButtonContent() {
|
getButtonContent(): Mithril.ChildArray {
|
||||||
const unread = this.getUnreadCount();
|
const unread = this.getUnreadCount();
|
||||||
|
|
||||||
return [
|
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>,
|
unread !== 0 && <span className="NotificationsDropdown-unread">{unread}</span>,
|
||||||
<span className="Button-label">{this.attrs.label}</span>,
|
<span className="Button-label">{this.attrs.label}</span>,
|
||||||
];
|
];
|
||||||
|
@ -61,16 +66,16 @@ export default class NotificationsDropdown extends Dropdown {
|
||||||
}
|
}
|
||||||
|
|
||||||
getUnreadCount() {
|
getUnreadCount() {
|
||||||
return app.session.user.unreadNotificationCount();
|
return app.session.user!.unreadNotificationCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
getNewCount() {
|
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
|
// Don't close the notifications dropdown if the user is opening a link in a
|
||||||
// new tab or window.
|
// 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 app from '../../forum/app';
|
||||||
import avatar from '../../common/helpers/avatar';
|
import avatar from '../../common/helpers/avatar';
|
||||||
import username from '../../common/helpers/username';
|
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 LinkButton from '../../common/components/LinkButton';
|
||||||
import Button from '../../common/components/Button';
|
import Button from '../../common/components/Button';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import Separator from '../../common/components/Separator';
|
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
|
* The `SessionDropdown` component shows a button with the current user's
|
||||||
* avatar/name, with a dropdown of session controls.
|
* avatar/name, with a dropdown of session controls.
|
||||||
*/
|
*/
|
||||||
export default class SessionDropdown extends Dropdown {
|
export default class SessionDropdown<CustomAttrs extends ISessionDropdownAttrs = ISessionDropdownAttrs> extends Dropdown<CustomAttrs> {
|
||||||
static initAttrs(attrs) {
|
static initAttrs(attrs: ISessionDropdownAttrs) {
|
||||||
super.initAttrs(attrs);
|
super.initAttrs(attrs);
|
||||||
|
|
||||||
attrs.className = 'SessionDropdown';
|
attrs.className = 'SessionDropdown';
|
||||||
attrs.buttonClassName = 'Button Button--user Button--flat';
|
attrs.buttonClassName = 'Button Button--user Button--flat';
|
||||||
attrs.menuClassName = 'Dropdown-menu--right';
|
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() });
|
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.
|
* Build an item list for the contents of the dropdown menu.
|
||||||
*
|
|
||||||
* @return {ItemList<import('mithril').Children>}
|
|
||||||
*/
|
*/
|
||||||
items() {
|
items(): ItemList<Mithril.Children> {
|
||||||
const items = new ItemList();
|
const items = new ItemList<Mithril.Children>();
|
||||||
const user = app.session.user;
|
const user = app.session.user!;
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
'profile',
|
'profile',
|
Loading…
Reference in New Issue
Block a user