convert ListView to typescript

This commit is contained in:
Bruno Windels 2021-09-06 17:12:14 +02:00
parent c6b020a9e7
commit 632d29795a
11 changed files with 230 additions and 185 deletions

View file

@ -15,8 +15,9 @@ limitations under the License.
*/ */
import {el} from "./html.js"; import {el} from "./html.js";
import {mountView} from "./utils.js"; import {mountView} from "./utils";
import {insertAt, ListView} from "./ListView.js"; import {ListView} from "./ListView";
import {insertAt} from "./utils";
class ItemRange { class ItemRange {
constructor(topCount, renderCount, bottomCount) { constructor(topCount, renderCount, bottomCount) {

View file

@ -1,163 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {el} from "./html.js";
import {mountView} from "./utils.js";
export function insertAt(parentNode, idx, childNode) {
const isLast = idx === parentNode.childElementCount;
if (isLast) {
parentNode.appendChild(childNode);
} else {
const nextDomNode = parentNode.children[idx];
parentNode.insertBefore(childNode, nextDomNode);
}
}
export class ListView {
constructor({list, onItemClick, className, tagName = "ul", parentProvidesUpdates = true}, childCreator) {
this._onItemClick = onItemClick;
this._list = list;
this._className = className;
this._tagName = tagName;
this._root = null;
this._subscription = null;
this._childCreator = childCreator;
this._childInstances = null;
this._mountArgs = {parentProvidesUpdates};
this._onClick = this._onClick.bind(this);
}
root() {
return this._root;
}
update(attributes) {
if (attributes.hasOwnProperty("list")) {
if (this._subscription) {
this._unloadList();
while (this._root.lastChild) {
this._root.lastChild.remove();
}
}
this._list = attributes.list;
this.loadList();
}
}
mount() {
const attr = {};
if (this._className) {
attr.className = this._className;
}
this._root = el(this._tagName, attr);
this.loadList();
if (this._onItemClick) {
this._root.addEventListener("click", this._onClick);
}
return this._root;
}
unmount() {
if (this._list) {
this._unloadList();
}
}
_onClick(event) {
if (event.target === this._root) {
return;
}
let childNode = event.target;
while (childNode.parentNode !== this._root) {
childNode = childNode.parentNode;
}
const index = Array.prototype.indexOf.call(this._root.childNodes, childNode);
const childView = this._childInstances[index];
this._onItemClick(childView, event);
}
_unloadList() {
this._subscription = this._subscription();
for (let child of this._childInstances) {
child.unmount();
}
this._childInstances = null;
}
loadList() {
if (!this._list) {
return;
}
this._subscription = this._list.subscribe(this);
this._childInstances = [];
const fragment = document.createDocumentFragment();
for (let item of this._list) {
const child = this._childCreator(item);
this._childInstances.push(child);
fragment.appendChild(mountView(child, this._mountArgs));
}
this._root.appendChild(fragment);
}
onAdd(idx, value) {
this.onBeforeListChanged();
const child = this._childCreator(value);
this._childInstances.splice(idx, 0, child);
insertAt(this._root, idx, mountView(child, this._mountArgs));
this.onListChanged();
}
onRemove(idx/*, _value*/) {
this.onBeforeListChanged();
const [child] = this._childInstances.splice(idx, 1);
child.root().remove();
child.unmount();
this.onListChanged();
}
onMove(fromIdx, toIdx/*, value*/) {
this.onBeforeListChanged();
const [child] = this._childInstances.splice(fromIdx, 1);
this._childInstances.splice(toIdx, 0, child);
child.root().remove();
insertAt(this._root, toIdx, child.root());
this.onListChanged();
}
onUpdate(i, value, params) {
if (this._childInstances) {
const instance = this._childInstances[i];
instance && instance.update(value, params);
}
}
recreateItem(index, value) {
if (this._childInstances) {
const child = this._childCreator(value);
if (!child) {
this.onRemove(index, value);
} else {
const [oldChild] = this._childInstances.splice(index, 1, child);
this._root.replaceChild(child.mount(this._mountArgs), oldChild.root());
oldChild.unmount();
}
}
}
onBeforeListChanged() {}
onListChanged() {}
}

View file

@ -0,0 +1,187 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {el} from "./html.js";
import {mountView, insertAt} from "./utils";
import {BaseObservableList as ObservableList} from "../../../../observable/list/BaseObservableList.js";
import {UIView, IMountArgs} from "./types";
interface IOptions<T, V> {
list: ObservableList<T>,
onItemClick?: (childView: V, evt: UIEvent) => void,
className?: string,
tagName?: string,
parentProvidesUpdates: boolean
}
type SubscriptionHandle = () => undefined;
export class ListView<T, V extends UIView> implements UIView {
private _onItemClick?: (childView: V, evt: UIEvent) => void;
private _list: ObservableList<T>;
private _className?: string;
private _tagName: string;
private _root?: HTMLElement;
private _subscription?: SubscriptionHandle;
private _childCreator: (value: T) => V;
private _childInstances?: V[];
private _mountArgs: IMountArgs;
constructor(
{list, onItemClick, className, tagName = "ul", parentProvidesUpdates = true}: IOptions<T, V>,
childCreator: (value: T) => V
) {
this._onItemClick = onItemClick;
this._list = list;
this._className = className;
this._tagName = tagName;
this._root = undefined;
this._subscription = undefined;
this._childCreator = childCreator;
this._childInstances = undefined;
this._mountArgs = {parentProvidesUpdates};
}
root(): HTMLElement {
// won't be undefined when called between mount and unmount
return this._root!;
}
update(attributes: IOptions<T, V>) {
if (attributes.list) {
if (this._subscription) {
this._unloadList();
while (this._root!.lastChild) {
this._root!.lastChild.remove();
}
}
this._list = attributes.list;
this.loadList();
}
}
mount(): HTMLElement {
const attr: {[name: string]: any} = {};
if (this._className) {
attr.className = this._className;
}
this._root = el(this._tagName, attr);
this.loadList();
if (this._onItemClick) {
this._root!.addEventListener("click", this);
}
return this._root!;
}
handleEvent(evt: Event) {
if (evt.type === "click") {
this._handleClick(evt as UIEvent);
}
}
unmount(): void {
if (this._list) {
this._unloadList();
}
}
private _handleClick(event: UIEvent) {
if (event.target === this._root || !this._onItemClick) {
return;
}
let childNode = event.target as Element;
while (childNode.parentNode !== this._root) {
childNode = childNode.parentNode as Element;
}
const index = Array.prototype.indexOf.call(this._root!.childNodes, childNode);
const childView = this._childInstances![index];
if (childView) {
this._onItemClick(childView, event);
}
}
private _unloadList() {
this._subscription = this._subscription!();
for (let child of this._childInstances!) {
child.unmount();
}
this._childInstances = undefined;
}
private loadList() {
if (!this._list) {
return;
}
this._subscription = this._list.subscribe(this);
this._childInstances = [];
const fragment = document.createDocumentFragment();
for (let item of this._list) {
const child = this._childCreator(item);
this._childInstances!.push(child);
fragment.appendChild(mountView(child, this._mountArgs));
}
this._root!.appendChild(fragment);
}
protected onAdd(idx: number, value: T) {
this.onBeforeListChanged();
const child = this._childCreator(value);
this._childInstances!.splice(idx, 0, child);
insertAt(this._root!, idx, mountView(child, this._mountArgs));
this.onListChanged();
}
protected onRemove(idx: number, value: T) {
this.onBeforeListChanged();
const [child] = this._childInstances!.splice(idx, 1);
child.root().remove();
child.unmount();
this.onListChanged();
}
protected onMove(fromIdx: number, toIdx: number, value: T) {
this.onBeforeListChanged();
const [child] = this._childInstances!.splice(fromIdx, 1);
this._childInstances!.splice(toIdx, 0, child);
child.root().remove();
insertAt(this._root!, toIdx, child.root());
this.onListChanged();
}
protected onUpdate(i: number, value: T, params: any) {
if (this._childInstances) {
const instance = this._childInstances![i];
instance && instance.update(value, params);
}
}
protected recreateItem(index: number, value: T) {
if (this._childInstances) {
const child = this._childCreator(value);
if (!child) {
this.onRemove(index, value);
} else {
const [oldChild] = this._childInstances!.splice(index, 1, child);
this._root!.replaceChild(child.mount(this._mountArgs), oldChild.root());
oldChild.unmount();
}
}
}
protected onBeforeListChanged() {}
protected onListChanged() {}
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html.js"; import { setAttribute, text, isChildren, classNames, TAG_NAMES, HTML_NS } from "./html.js";
import {mountView} from "./utils.js"; import {mountView} from "./utils";
import {BaseUpdateView} from "./BaseUpdateView.js"; import {BaseUpdateView} from "./BaseUpdateView.js";
function objHasFns(obj) { function objHasFns(obj) {

View file

@ -13,15 +13,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
export interface IMountArgs {
// if true, the parent will call update() rather than the view updating itself by binding to a data source.
parentProvidesUpdates: boolean
};
import {errorToDOM} from "./error.js"; export interface UIView {
mount(args?: IMountArgs): HTMLElement;
export function mountView(view, mountArgs = undefined) { root(): HTMLElement; // should only be called between mount() and unmount()
let node; unmount(): void;
try { update(...any); // this isn't really standarized yet
node = view.mount(mountArgs);
} catch (err) {
node = errorToDOM(err);
}
return node;
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 Bruno Windels <bruno@windels.cloud> Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,11 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {UIView, IMountArgs} from "./types";
import {tag} from "./html.js"; import {tag} from "./html.js";
export function errorToDOM(error) { export function mountView(view: UIView, mountArgs: IMountArgs): HTMLElement {
let node;
try {
node = view.mount(mountArgs);
} catch (err) {
node = errorToDOM(err);
}
return node;
}
export function errorToDOM(error: Error): HTMLElement {
const stack = new Error().stack; const stack = new Error().stack;
let callee = null; let callee: string | null = null;
if (stack) { if (stack) {
callee = stack.split("\n")[1]; callee = stack.split("\n")[1];
} }
@ -29,3 +40,13 @@ export function errorToDOM(error) {
tag.pre(error.stack), tag.pre(error.stack),
]); ]);
} }
export function insertAt(parentNode: HTMLElement, idx: number, childNode: HTMLElement): void {
const isLast = idx === parentNode.childElementCount;
if (isLast) {
parentNode.appendChild(childNode);
} else {
const nextDomNode = parentNode.children[idx];
parentNode.insertBefore(childNode, nextDomNode);
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ListView} from "../general/ListView.js"; import {ListView} from "../general/ListView";
import {TemplateView} from "../general/TemplateView.js"; import {TemplateView} from "../general/TemplateView.js";
import {hydrogenGithubLink} from "./common.js"; import {hydrogenGithubLink} from "./common.js";
import {SessionLoadStatusView} from "./SessionLoadStatusView.js"; import {SessionLoadStatusView} from "./SessionLoadStatusView.js";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ListView} from "../../general/ListView.js"; import {ListView} from "../../general/ListView";
import {TemplateView} from "../../general/TemplateView.js"; import {TemplateView} from "../../general/TemplateView.js";
import {RoomTileView} from "./RoomTileView.js"; import {RoomTileView} from "./RoomTileView.js";
import {InviteTileView} from "./InviteTileView.js"; import {InviteTileView} from "./InviteTileView.js";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ListView} from "../../general/ListView.js"; import {ListView} from "../../general/ListView";
import {GapView} from "./timeline/GapView.js"; import {GapView} from "./timeline/GapView.js";
import {TextMessageView} from "./timeline/TextMessageView.js"; import {TextMessageView} from "./timeline/TextMessageView.js";
import {ImageView} from "./timeline/ImageView.js"; import {ImageView} from "./timeline/ImageView.js";

View file

@ -17,7 +17,7 @@ limitations under the License.
import {renderStaticAvatar} from "../../../avatar.js"; import {renderStaticAvatar} from "../../../avatar.js";
import {tag} from "../../../general/html.js"; import {tag} from "../../../general/html.js";
import {mountView} from "../../../general/utils.js"; import {mountView} from "../../../general/utils";
import {TemplateView} from "../../../general/TemplateView.js"; import {TemplateView} from "../../../general/TemplateView.js";
import {Popup} from "../../../general/Popup.js"; import {Popup} from "../../../general/Popup.js";
import {Menu} from "../../../general/Menu.js"; import {Menu} from "../../../general/Menu.js";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ListView} from "../../../general/ListView.js"; import {ListView} from "../../../general/ListView";
import {TemplateView} from "../../../general/TemplateView.js"; import {TemplateView} from "../../../general/TemplateView.js";
export class ReactionsView extends ListView { export class ReactionsView extends ListView {