mirror of
https://github.com/flarum/core.git
synced 2025-08-03 23:17:43 +02:00
feat: ItemList component
This commit is contained in:
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 PostTypes from './PostTypes';
|
||||||
import Routes from './Routes';
|
import Routes from './Routes';
|
||||||
import Store from './Store';
|
import Store from './Store';
|
||||||
|
import ItemList from './ItemList';
|
||||||
|
|
||||||
const extenders = {
|
const extenders = {
|
||||||
Model,
|
Model,
|
||||||
PostTypes,
|
PostTypes,
|
||||||
Routes,
|
Routes,
|
||||||
Store,
|
Store,
|
||||||
|
ItemList,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default extenders;
|
export default extenders;
|
||||||
|
@@ -5,7 +5,6 @@ import Page, { IPageAttrs } from '../../common/components/Page';
|
|||||||
import ItemList from '../../common/utils/ItemList';
|
import ItemList from '../../common/utils/ItemList';
|
||||||
import DiscussionHero from './DiscussionHero';
|
import DiscussionHero from './DiscussionHero';
|
||||||
import DiscussionListPane from './DiscussionListPane';
|
import DiscussionListPane from './DiscussionListPane';
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
|
||||||
import SplitDropdown from '../../common/components/SplitDropdown';
|
import SplitDropdown from '../../common/components/SplitDropdown';
|
||||||
import listItems from '../../common/helpers/listItems';
|
import listItems from '../../common/helpers/listItems';
|
||||||
import DiscussionControls from '../utils/DiscussionControls';
|
import DiscussionControls from '../utils/DiscussionControls';
|
||||||
|
@@ -2,8 +2,8 @@ import Component from '../../common/Component';
|
|||||||
import type { ComponentAttrs } from '../../common/Component';
|
import type { ComponentAttrs } from '../../common/Component';
|
||||||
import type Mithril from 'mithril';
|
import type Mithril from 'mithril';
|
||||||
import classList from '../../common/utils/classList';
|
import classList from '../../common/utils/classList';
|
||||||
import ItemList from '../../common/utils/ItemList';
|
|
||||||
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
import LoadingIndicator from '../../common/components/LoadingIndicator';
|
||||||
|
import ItemList from '../../common/components/ItemList';
|
||||||
|
|
||||||
export interface PageStructureAttrs extends ComponentAttrs {
|
export interface PageStructureAttrs extends ComponentAttrs {
|
||||||
hero?: () => Mithril.Children;
|
hero?: () => Mithril.Children;
|
||||||
@@ -21,73 +21,44 @@ export default class PageStructure<CustomAttrs extends PageStructureAttrs = Page
|
|||||||
|
|
||||||
this.content = vnode.children;
|
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> {
|
<div key="main" className="Page-main">
|
||||||
const items = new ItemList<Mithril.Children>();
|
{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);
|
<div key="container" className="Page-container container">
|
||||||
items.add('main', this.main(), 10);
|
<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;
|
<div key="content" className="Page-content">
|
||||||
}
|
{this.content}
|
||||||
|
</div>
|
||||||
mainItems(): ItemList<Mithril.Children> {
|
</div>
|
||||||
const items = new ItemList<Mithril.Children>();
|
</ItemList>
|
||||||
|
)}
|
||||||
items.add('hero', this.providedHero(), 100);
|
</div>
|
||||||
items.add('container', this.container(), 10);
|
</ItemList>
|
||||||
|
</div>
|
||||||
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>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -40,6 +40,10 @@
|
|||||||
|
|
||||||
&-sidebar {
|
&-sidebar {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|
||||||
|
&-main {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user