mirror of
https://github.com/flarum/framework.git
synced 2025-02-27 04:00:28 +08:00
feat: ItemList component
This commit is contained in:
parent
d01c0e5210
commit
13f997d784
55
framework/core/js/src/common/components/ItemList.tsx
Normal file
55
framework/core/js/src/common/components/ItemList.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import ItemListUtil from '../utils/ItemList';
|
||||
import Component from '../Component';
|
||||
import type Mithril from 'mithril';
|
||||
import listItems from '../helpers/listItems';
|
||||
|
||||
export interface IItemListAttrs {
|
||||
/** Unique key for the list. Use the convention of `componentName.listName` */
|
||||
key: string;
|
||||
/** The context of the list. Usually the component instance. Will be automatically set if not provided. */
|
||||
context?: any;
|
||||
/** Optionally, the element tag to wrap each item in. Defaults to none. */
|
||||
wrapper?: string;
|
||||
}
|
||||
|
||||
export default class ItemList<CustomAttrs extends IItemListAttrs = IItemListAttrs> extends Component<CustomAttrs> {
|
||||
view(vnode: Mithril.Vnode<CustomAttrs>) {
|
||||
const items = this.items(vnode.children).toArray();
|
||||
|
||||
return vnode.attrs.wrapper ? listItems(items, vnode.attrs.wrapper) : items;
|
||||
}
|
||||
|
||||
items(children: Mithril.ChildArrayOrPrimitive | undefined): ItemListUtil<Mithril.Children> {
|
||||
const items = new ItemListUtil<Mithril.Children>();
|
||||
|
||||
let priority = 10;
|
||||
|
||||
this.validateChildren(children)
|
||||
.reverse()
|
||||
.forEach((child: Mithril.Vnode<any, any>) => {
|
||||
items.add(child.key!.toString(), child, (priority += 10));
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private validateChildren(children: Mithril.ChildArrayOrPrimitive | undefined): Mithril.Vnode<any, any>[] {
|
||||
if (!children) return [];
|
||||
|
||||
children = Array.isArray(children) ? children : [children];
|
||||
children = children.filter((child: Mithril.Children) => child !== null && child !== undefined);
|
||||
|
||||
// It must be a Vnode array
|
||||
children.forEach((child: Mithril.Children) => {
|
||||
if (typeof child !== 'object' || !('tag' in child!)) {
|
||||
throw new Error(`[${this.attrs.key}] The ItemList component requires a valid mithril Vnode array. Found: ${typeof child}.`);
|
||||
}
|
||||
|
||||
if (!child.key) {
|
||||
throw new Error('The ItemList component requires a unique key for each child in the list.');
|
||||
}
|
||||
});
|
||||
|
||||
return children as Mithril.Vnode<any, any>[];
|
||||
}
|
||||
}
|
89
framework/core/js/src/common/extenders/ItemList.ts
Normal file
89
framework/core/js/src/common/extenders/ItemList.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import type IExtender from './IExtender';
|
||||
import type { IExtensionModule } from './IExtender';
|
||||
import type Application from '../Application';
|
||||
import type Mithril from 'mithril';
|
||||
import type { IItemObject } from '../utils/ItemList';
|
||||
import { extend } from '../extend';
|
||||
import ItemListComponent from '../components/ItemList';
|
||||
|
||||
type LazyContent<T> = (context: T) => Mithril.Children;
|
||||
|
||||
/**
|
||||
* The `ItemList` extender allows you to add, remove, and replace items in an
|
||||
* `ItemList` component. Each ItemList has a unique key, which is used to
|
||||
* identify it.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import Extend from 'flarum/common/extenders';
|
||||
*
|
||||
* export default [
|
||||
* new Extend.ItemList<PageStructure>('PageStructure.mainItems')
|
||||
* .add('test', (context) => app.forum.attribute('baseUrl'), 400)
|
||||
* .setContent('hero', (context) => <div>My new content</div>)
|
||||
* .setPriority('hero', 0)
|
||||
* .remove('hero')
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
export default class ItemList<T = Component<any>> implements IExtender {
|
||||
protected key: string;
|
||||
protected additions: Array<IItemObject<LazyContent<T>>> = [];
|
||||
protected removals: string[] = [];
|
||||
protected contentReplacements: Record<string, LazyContent<T>> = {};
|
||||
protected priorityReplacements: Record<string, number> = {};
|
||||
|
||||
constructor(key: string) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
add(itemName: string, content: LazyContent<T>, priority: number = 0) {
|
||||
this.additions.push({ itemName, content, priority });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
remove(itemName: string) {
|
||||
this.removals.push(itemName);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setContent(itemName: string, content: LazyContent<T>) {
|
||||
this.contentReplacements[itemName] = content;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setPriority(itemName: string, priority: number) {
|
||||
this.priorityReplacements[itemName] = priority;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
extend(app: Application, extension: IExtensionModule) {
|
||||
const { key, additions, removals, contentReplacements, priorityReplacements } = this;
|
||||
|
||||
extend(ItemListComponent.prototype, 'items', function (this: ItemListComponent, items) {
|
||||
if (key !== this.attrs.key) return;
|
||||
|
||||
const safeContent = (content: Mithril.Children) => (typeof content === 'string' ? [content] : content);
|
||||
|
||||
for (const itemName of removals) {
|
||||
items.remove(itemName);
|
||||
}
|
||||
|
||||
for (const { itemName, content, priority } of additions) {
|
||||
items.add(itemName, safeContent(content(this.attrs.context)), priority);
|
||||
}
|
||||
|
||||
for (const [itemName, content] of Object.entries(contentReplacements)) {
|
||||
items.setContent(itemName, safeContent(content(this.attrs.context)));
|
||||
}
|
||||
|
||||
for (const [itemName, priority] of Object.entries(priorityReplacements)) {
|
||||
items.setPriority(itemName, priority);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -2,12 +2,14 @@ import Model from './Model';
|
||||
import PostTypes from './PostTypes';
|
||||
import Routes from './Routes';
|
||||
import Store from './Store';
|
||||
import ItemList from './ItemList';
|
||||
|
||||
const extenders = {
|
||||
Model,
|
||||
PostTypes,
|
||||
Routes,
|
||||
Store,
|
||||
ItemList,
|
||||
};
|
||||
|
||||
export default extenders;
|
||||
|
@ -5,7 +5,6 @@ import Page, { IPageAttrs } from '../../common/components/Page';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import DiscussionHero from './DiscussionHero';
|
||||
import DiscussionListPane from './DiscussionListPane';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import SplitDropdown from '../../common/components/SplitDropdown';
|
||||
import listItems from '../../common/helpers/listItems';
|
||||
import DiscussionControls from '../utils/DiscussionControls';
|
||||
|
@ -2,8 +2,8 @@ import Component from '../../common/Component';
|
||||
import type { ComponentAttrs } from '../../common/Component';
|
||||
import type Mithril from 'mithril';
|
||||
import classList from '../../common/utils/classList';
|
||||
import ItemList from '../../common/utils/ItemList';
|
||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||
import ItemList from '../../common/components/ItemList';
|
||||
|
||||
export interface PageStructureAttrs extends ComponentAttrs {
|
||||
hero?: () => Mithril.Children;
|
||||
@ -21,73 +21,44 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
|
||||
|
||||
this.content = vnode.children;
|
||||
|
||||
return <div className={classList('Page', className)}>{this.rootItems().toArray()}</div>;
|
||||
}
|
||||
return (
|
||||
<div className={classList('Page', className)}>
|
||||
<ItemList key="PageStructure.rootItems" context={this}>
|
||||
<div key="pane" className="Page-pane">
|
||||
{(this.attrs.pane && this.attrs.pane()) || null}
|
||||
</div>
|
||||
|
||||
rootItems(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
<div key="main" className="Page-main">
|
||||
{this.attrs.loading ? (
|
||||
<ItemList key="PageStructure.loadingItems" context={this}>
|
||||
<LoadingIndicator key="spinner" display="block" />
|
||||
</ItemList>
|
||||
) : (
|
||||
<ItemList key="PageStructure.mainItems" context={this}>
|
||||
<div key="hero" className="Page-hero">
|
||||
{(this.attrs.hero && this.attrs.hero()) || null}
|
||||
</div>
|
||||
|
||||
items.add('pane', this.providedPane(), 100);
|
||||
items.add('main', this.main(), 10);
|
||||
<div key="container" className="Page-container container">
|
||||
<div key="sidebar" className="Page-sidebar">
|
||||
<ItemList key="PageStructure.sidebarItems" context={this}>
|
||||
{this.attrs.sidebar && (
|
||||
<div key="provided" className="Page-sidebar-main">
|
||||
{this.attrs.sidebar()}
|
||||
</div>
|
||||
)}
|
||||
</ItemList>
|
||||
</div>
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
mainItems(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add('hero', this.providedHero(), 100);
|
||||
items.add('container', this.container(), 10);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
loadingItems(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add('spinner', <LoadingIndicator display="block" />, 100);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
main(): Mithril.Children {
|
||||
return <div className="Page-main">{this.attrs.loading ? this.loadingItems().toArray() : this.mainItems().toArray()}</div>;
|
||||
}
|
||||
|
||||
containerItems(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add('sidebar', this.sidebar(), 100);
|
||||
items.add('content', this.providedContent(), 10);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
container(): Mithril.Children {
|
||||
return <div className="Page-container container">{this.containerItems().toArray()}</div>;
|
||||
}
|
||||
|
||||
sidebarItems(): ItemList<Mithril.Children> {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
items.add('sidebar', (this.attrs.sidebar && this.attrs.sidebar()) || null, 100);
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
sidebar(): Mithril.Children {
|
||||
return <div className="Page-sidebar">{this.sidebarItems().toArray()}</div>;
|
||||
}
|
||||
|
||||
providedPane(): Mithril.Children {
|
||||
return <div className="Page-pane">{(this.attrs.pane && this.attrs.pane()) || null}</div>;
|
||||
}
|
||||
|
||||
providedHero(): Mithril.Children {
|
||||
return <div className="Page-hero">{(this.attrs.hero && this.attrs.hero()) || null}</div>;
|
||||
}
|
||||
|
||||
providedContent(): Mithril.Children {
|
||||
return <div className="Page-content">{this.content}</div>;
|
||||
<div key="content" className="Page-content">
|
||||
{this.content}
|
||||
</div>
|
||||
</div>
|
||||
</ItemList>
|
||||
)}
|
||||
</div>
|
||||
</ItemList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,10 @@
|
||||
|
||||
&-sidebar {
|
||||
margin-top: 0;
|
||||
|
||||
&-main {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user