Merge pull request #521 from DanilaFe/typescript-observable

Start migrating Observable code to TypeScript.
This commit is contained in:
Bruno Windels 2021-10-01 09:54:29 +02:00 committed by GitHub
commit a0f443ccc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 198 additions and 168 deletions

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue.js";
import {BaseObservableValue, ObservableValue} from "../../observable/ObservableValue";
export class Navigation {
constructor(allowsChild) {

View file

@ -186,7 +186,7 @@ export class RoomGridViewModel extends ViewModel {
}
import {createNavigation} from "../navigation/index.js";
import {ObservableValue} from "../../observable/ObservableValue.js";
import {ObservableValue} from "../../observable/ObservableValue";
export function tests() {
class RoomVMMock {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ObservableValue} from "../../observable/ObservableValue.js";
import {ObservableValue} from "../../observable/ObservableValue";
/**
Depending on the status of a room (invited, joined, archived, or none),
@ -77,4 +77,4 @@ export class RoomViewModelObservable extends ObservableValue {
this.unsubscribeAll();
this.get()?.dispose();
}
}
}

View file

@ -189,7 +189,7 @@ import {HomeServer as MockHomeServer} from "../../../../mocks/HomeServer.js";
// other imports
import {BaseMessageTile} from "./tiles/BaseMessageTile.js";
import {MappedList} from "../../../../observable/list/MappedList.js";
import {ObservableValue} from "../../../../observable/ObservableValue.js";
import {ObservableValue} from "../../../../observable/ObservableValue";
import {PowerLevels} from "../../../../matrix/room/PowerLevels.js";
export function tests() {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableList} from "../../../../observable/list/BaseObservableList.js";
import {BaseObservableList} from "../../../../observable/list/BaseObservableList";
import {sortedIndex} from "../../../../utils/sortedIndex.js";
// maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary

View file

@ -40,7 +40,7 @@ import {
writeKey as ssssWriteKey,
} from "./ssss/index.js";
import {SecretStorage} from "./ssss/SecretStorage.js";
import {ObservableValue, RetainedObservableValue} from "../observable/ObservableValue.js";
import {ObservableValue, RetainedObservableValue} from "../observable/ObservableValue";
const PICKLE_KEY = "DEFAULT_KEY";
const PUSHER_KEY = "pusher";

View file

@ -18,7 +18,7 @@ limitations under the License.
import {createEnum} from "../utils/enum.js";
import {lookupHomeserver} from "./well-known.js";
import {AbortableOperation} from "../utils/AbortableOperation";
import {ObservableValue} from "../observable/ObservableValue.js";
import {ObservableValue} from "../observable/ObservableValue";
import {HomeServerApi} from "./net/HomeServerApi.js";
import {Reconnector, ConnectionStatus} from "./net/Reconnector.js";
import {ExponentialRetryDelay} from "./net/ExponentialRetryDelay.js";

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ObservableValue} from "../observable/ObservableValue.js";
import {ObservableValue} from "../observable/ObservableValue";
import {createEnum} from "../utils/enum.js";
const INCREMENTAL_TIMEOUT = 30000;

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {createEnum} from "../../utils/enum.js";
import {ObservableValue} from "../../observable/ObservableValue.js";
import {ObservableValue} from "../../observable/ObservableValue";
export const ConnectionStatus = createEnum(
"Waiting",

View file

@ -29,7 +29,7 @@ import {ObservedEventMap} from "./ObservedEventMap.js";
import {DecryptionSource} from "../e2ee/common.js";
import {ensureLogItem} from "../../logging/utils.js";
import {PowerLevels} from "./PowerLevels.js";
import {RetainedObservableValue} from "../../observable/ObservableValue.js";
import {RetainedObservableValue} from "../../observable/ObservableValue";
const EVENT_ENCRYPTED_TYPE = "m.room.encrypted";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableValue} from "../../observable/ObservableValue.js";
import {BaseObservableValue} from "../../observable/ObservableValue";
export class ObservedEventMap {
constructor(notifyEmpty) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ObservableValue} from "../observable/ObservableValue.js";
import {ObservableValue} from "../observable/ObservableValue";
class Timeout {
constructor(elapsed, ms) {

View file

@ -14,20 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export class BaseObservable {
constructor() {
this._handlers = new Set();
}
export abstract class BaseObservable<T> {
protected _handlers: Set<T> = new Set<T>();
onSubscribeFirst() {
onSubscribeFirst(): void {
}
onUnsubscribeLast() {
onUnsubscribeLast(): void {
}
subscribe(handler) {
subscribe(handler: T): () => void {
this._handlers.add(handler);
if (this._handlers.size === 1) {
this.onSubscribeFirst();
@ -37,7 +35,7 @@ export class BaseObservable {
};
}
unsubscribe(handler) {
unsubscribe(handler: T | null): null {
if (handler) {
this._handlers.delete(handler);
if (this._handlers.size === 0) {
@ -48,14 +46,14 @@ export class BaseObservable {
return null;
}
unsubscribeAll() {
unsubscribeAll(): void {
if (this._handlers.size !== 0) {
this._handlers.clear();
this.onUnsubscribeLast();
}
}
get hasSubscriptions() {
get hasSubscriptions(): boolean {
return this._handlers.size !== 0;
}
@ -63,13 +61,11 @@ export class BaseObservable {
}
export function tests() {
class Collection extends BaseObservable {
constructor() {
super();
this.firstSubscribeCalls = 0;
this.firstUnsubscribeCalls = 0;
}
onSubscribeFirst() { this.firstSubscribeCalls += 1; }
class Collection extends BaseObservable<{}> {
firstSubscribeCalls: number= 0;
firstUnsubscribeCalls: number = 0;
onSubscribeFirst() { this.firstSubscribeCalls += 1; }
onUnsubscribeLast() { this.firstUnsubscribeCalls += 1; }
}

View file

@ -15,21 +15,19 @@ limitations under the License.
*/
import {AbortError} from "../utils/error.js";
import {BaseObservable} from "./BaseObservable.js";
import {BaseObservable} from "./BaseObservable";
// like an EventEmitter, but doesn't have an event type
export class BaseObservableValue extends BaseObservable {
emit(argument) {
export abstract class BaseObservableValue<T> extends BaseObservable<(value: T) => void> {
emit(argument: T) {
for (const h of this._handlers) {
h(argument);
}
}
get() {
throw new Error("unimplemented");
}
abstract get(): T;
waitFor(predicate) {
waitFor(predicate: (value: T) => boolean): IWaitHandle<T> {
if (predicate(this.get())) {
return new ResolvedWaitForHandle(Promise.resolve(this.get()));
} else {
@ -38,8 +36,17 @@ export class BaseObservableValue extends BaseObservable {
}
}
class WaitForHandle {
constructor(observable, predicate) {
interface IWaitHandle<T> {
promise: Promise<T>;
dispose(): void;
}
class WaitForHandle<T> implements IWaitHandle<T> {
private _promise: Promise<T>
private _reject: ((reason?: any) => void) | null;
private _subscription: (() => void) | null;
constructor(observable: BaseObservableValue<T>, predicate: (value: T) => boolean) {
this._promise = new Promise((resolve, reject) => {
this._reject = reject;
this._subscription = observable.subscribe(v => {
@ -52,7 +59,7 @@ class WaitForHandle {
});
}
get promise() {
get promise(): Promise<T> {
return this._promise;
}
@ -68,25 +75,24 @@ class WaitForHandle {
}
}
class ResolvedWaitForHandle {
constructor(promise) {
this.promise = promise;
}
class ResolvedWaitForHandle<T> implements IWaitHandle<T> {
constructor(public promise: Promise<T>) {}
dispose() {}
}
export class ObservableValue extends BaseObservableValue {
constructor(initialValue) {
export class ObservableValue<T> extends BaseObservableValue<T> {
private _value: T;
constructor(initialValue: T) {
super();
this._value = initialValue;
}
get() {
get(): T {
return this._value;
}
set(value) {
set(value: T): void {
if (value !== this._value) {
this._value = value;
this.emit(this._value);
@ -94,8 +100,10 @@ export class ObservableValue extends BaseObservableValue {
}
}
export class RetainedObservableValue extends ObservableValue {
constructor(initialValue, freeCallback) {
export class RetainedObservableValue<T> extends ObservableValue<T> {
private _freeCallback: () => void;
constructor(initialValue: T, freeCallback: () => void) {
super(initialValue);
this._freeCallback = freeCallback;
}
@ -109,7 +117,7 @@ export class RetainedObservableValue extends ObservableValue {
export function tests() {
return {
"set emits an update": assert => {
const a = new ObservableValue();
const a = new ObservableValue<number>(0);
let fired = false;
const subscription = a.subscribe(v => {
fired = true;
@ -140,7 +148,7 @@ export function tests() {
assert.strictEqual(a.get(), 6);
},
"waitFor promise rejects when disposed": async assert => {
const a = new ObservableValue();
const a = new ObservableValue<number>(0);
const handle = a.waitFor(() => false);
Promise.resolve().then(() => {
handle.dispose();

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList.js";
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
export class AsyncMappedList extends BaseMappedList {
constructor(sourceList, mapper, updater, removeCallback) {

View file

@ -1,77 +0,0 @@
/*
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");
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 {BaseObservableList} from "./BaseObservableList.js";
import {findAndUpdateInArray} from "./common.js";
export class BaseMappedList extends BaseObservableList {
constructor(sourceList, mapper, updater, removeCallback) {
super();
this._sourceList = sourceList;
this._mapper = mapper;
this._updater = updater;
this._removeCallback = removeCallback;
this._mappedValues = null;
this._sourceUnsubscribe = null;
}
findAndUpdate(predicate, updater) {
return findAndUpdateInArray(predicate, this._mappedValues, this, updater);
}
get length() {
return this._mappedValues.length;
}
[Symbol.iterator]() {
return this._mappedValues.values();
}
}
export function runAdd(list, index, mappedValue) {
list._mappedValues.splice(index, 0, mappedValue);
list.emitAdd(index, mappedValue);
}
export function runUpdate(list, index, value, params) {
const mappedValue = list._mappedValues[index];
if (list._updater) {
list._updater(mappedValue, params, value);
}
list.emitUpdate(index, mappedValue, params);
}
export function runRemove(list, index) {
const mappedValue = list._mappedValues[index];
list._mappedValues.splice(index, 1);
if (list._removeCallback) {
list._removeCallback(mappedValue);
}
list.emitRemove(index, mappedValue);
}
export function runMove(list, fromIdx, toIdx) {
const mappedValue = list._mappedValues[fromIdx];
list._mappedValues.splice(fromIdx, 1);
list._mappedValues.splice(toIdx, 0, mappedValue);
list.emitMove(fromIdx, toIdx, mappedValue);
}
export function runReset(list) {
list._mappedValues = [];
list.emitReset();
}

View file

@ -0,0 +1,85 @@
/*
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");
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 {BaseObservableList} from "./BaseObservableList";
import {findAndUpdateInArray} from "./common";
export type Mapper<F,T> = (value: F) => T
export type Updater<F,T> = (mappedValue: T, params: any, value: F) => void;
export class BaseMappedList<F,T> extends BaseObservableList<T> {
protected _sourceList: BaseObservableList<F>;
protected _sourceUnsubscribe: (() => void) | null = null;
_mapper: Mapper<F,T>;
_updater: Updater<F,T>;
_removeCallback: ((value: T) => void) | null;
_mappedValues: T[] | null = null;
constructor(sourceList: BaseObservableList<F>, mapper: Mapper<F,T>, updater: Updater<F,T>, removeCallback: ((value: T) => void) | null = null) {
super();
this._sourceList = sourceList;
this._mapper = mapper;
this._updater = updater;
this._removeCallback = removeCallback;
}
findAndUpdate(predicate: (value: T) => boolean, updater: (value: T) => any | false) {
return findAndUpdateInArray(predicate, this._mappedValues!, this, updater);
}
get length() {
return this._mappedValues!.length;
}
[Symbol.iterator]() {
return this._mappedValues!.values();
}
}
export function runAdd<F,T>(list: BaseMappedList<F,T>, index: number, mappedValue: T): void {
list._mappedValues!.splice(index, 0, mappedValue);
list.emitAdd(index, mappedValue);
}
export function runUpdate<F,T>(list: BaseMappedList<F,T>, index: number, value: F, params: any): void {
const mappedValue = list._mappedValues![index];
if (list._updater) {
list._updater(mappedValue, params, value);
}
list.emitUpdate(index, mappedValue, params);
}
export function runRemove<F,T>(list: BaseMappedList<F,T>, index: number): void {
const mappedValue = list._mappedValues![index];
list._mappedValues!.splice(index, 1);
if (list._removeCallback) {
list._removeCallback(mappedValue);
}
list.emitRemove(index, mappedValue);
}
export function runMove<F,T>(list: BaseMappedList<F,T>, fromIdx: number, toIdx: number): void {
const mappedValue = list._mappedValues![fromIdx];
list._mappedValues!.splice(fromIdx, 1);
list._mappedValues!.splice(toIdx, 0, mappedValue);
list.emitMove(fromIdx, toIdx, mappedValue);
}
export function runReset<F,T>(list: BaseMappedList<F,T>): void {
list._mappedValues = [];
list.emitReset();
}

View file

@ -14,9 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservable} from "../BaseObservable.js";
import {BaseObservable} from "../BaseObservable";
export class BaseObservableList extends BaseObservable {
export interface IListObserver<T> {
onReset(list: BaseObservableList<T>): void;
onAdd(index: number, value:T, list: BaseObservableList<T>): void;
onUpdate(index: number, value: T, params: any, list: BaseObservableList<T>): void;
onRemove(index: number, value: T, list: BaseObservableList<T>): void
onMove(from: number, to: number, value: T, list: BaseObservableList<T>): void
}
export abstract class BaseObservableList<T> extends BaseObservable<IListObserver<T>> {
emitReset() {
for(let h of this._handlers) {
h.onReset(this);
@ -24,19 +32,19 @@ export class BaseObservableList extends BaseObservable {
}
// we need batch events, mostly on index based collection though?
// maybe we should get started without?
emitAdd(index, value) {
emitAdd(index: number, value: T): void {
for(let h of this._handlers) {
h.onAdd(index, value, this);
}
}
emitUpdate(index, value, params) {
emitUpdate(index: number, value: T, params: any): void {
for(let h of this._handlers) {
h.onUpdate(index, value, params, this);
}
}
emitRemove(index, value) {
emitRemove(index: number, value: T): void {
for(let h of this._handlers) {
h.onRemove(index, value, this);
}
@ -44,17 +52,12 @@ export class BaseObservableList extends BaseObservable {
// toIdx assumes the item has already
// been removed from its fromIdx
emitMove(fromIdx, toIdx, value) {
emitMove(fromIdx: number, toIdx: number, value: T): void {
for(let h of this._handlers) {
h.onMove(fromIdx, toIdx, value, this);
}
}
[Symbol.iterator]() {
throw new Error("unimplemented");
}
get length() {
throw new Error("unimplemented");
}
abstract [Symbol.iterator](): IterableIterator<T>;
abstract get length(): number;
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableList} from "./BaseObservableList.js";
import {BaseObservableList} from "./BaseObservableList";
export class ConcatList extends BaseObservableList {
constructor(...sourceLists) {

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList.js";
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
export class MappedList extends BaseMappedList {
onSubscribeFirst() {
@ -57,7 +57,7 @@ export class MappedList extends BaseMappedList {
}
import {ObservableArray} from "./ObservableArray.js";
import {BaseObservableList} from "./BaseObservableList.js";
import {BaseObservableList} from "./BaseObservableList";
export async function tests() {
class MockList extends BaseObservableList {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableList} from "./BaseObservableList.js";
import {BaseObservableList} from "./BaseObservableList";
export class ObservableArray extends BaseObservableList {
constructor(initialValues = []) {

View file

@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableList} from "./BaseObservableList.js";
import {BaseObservableList} from "./BaseObservableList";
import {sortedIndex} from "../../utils/sortedIndex.js";
import {findAndUpdateInArray} from "./common.js";
import {findAndUpdateInArray} from "./common";
export class SortedArray extends BaseObservableList {
constructor(comparator) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableList} from "./BaseObservableList.js";
import {BaseObservableList} from "./BaseObservableList";
import {sortedIndex} from "../../utils/sortedIndex.js";
/*

View file

@ -14,9 +14,10 @@ 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 {BaseObservableList} from "./BaseObservableList";
/* inline update of item in collection backed by array, without replacing the preexising item */
export function findAndUpdateInArray(predicate, array, observable, updater) {
export function findAndUpdateInArray<T>(predicate: (value: T) => boolean, array: T[], observable: BaseObservableList<T>, updater: (value: T) => any | false) {
const index = array.findIndex(predicate);
if (index !== -1) {
const value = array[index];
@ -29,4 +30,4 @@ export function findAndUpdateInArray(predicate, array, observable, updater) {
return true;
}
return false;
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservable} from "../BaseObservable.js";
import {BaseObservable} from "../BaseObservable";
export class BaseObservableMap extends BaseObservable {
emitReset() {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableValue} from "../../../observable/ObservableValue.js";
import {BaseObservableValue} from "../../../observable/ObservableValue";
export class History extends BaseObservableValue {
handleEvent(event) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableValue} from "../../../observable/ObservableValue.js";
import {BaseObservableValue} from "../../../observable/ObservableValue";
export class OnlineStatus extends BaseObservableValue {
constructor() {

View file

@ -16,7 +16,7 @@ limitations under the License.
import {el} from "./html";
import {mountView, insertAt} from "./utils";
import {BaseObservableList as ObservableList} from "../../../../observable/list/BaseObservableList.js";
import {BaseObservableList as ObservableList} from "../../../../observable/list/BaseObservableList";
import {IView, IMountArgs} from "./types";
interface IOptions<T, V> {
@ -27,7 +27,7 @@ interface IOptions<T, V> {
parentProvidesUpdates?: boolean
}
type SubscriptionHandle = () => undefined;
type SubscriptionHandle = () => void;
export class ListView<T, V extends IView> implements IView {
@ -115,7 +115,8 @@ export class ListView<T, V extends IView> implements IView {
}
private _unloadList() {
this._subscription = this._subscription!();
this._subscription!()
this._subscription = undefined;
for (let child of this._childInstances!) {
child.unmount();
}
@ -137,26 +138,34 @@ export class ListView<T, V extends IView> implements IView {
this._root!.appendChild(fragment);
}
protected onAdd(idx: number, value: T) {
onReset() {
for (const child of this._childInstances!) {
child.root()!.remove();
child.unmount();
}
this._childInstances!.length = 0;
}
onAdd(idx: number, value: T) {
const child = this._childCreator(value);
this._childInstances!.splice(idx, 0, child);
insertAt(this._root!, idx, mountView(child, this._mountArgs));
}
protected onRemove(idx: number, value: T) {
onRemove(idx: number, value: T) {
const [child] = this._childInstances!.splice(idx, 1);
child.root()!.remove();
child.unmount();
}
protected onMove(fromIdx: number, toIdx: number, value: T) {
onMove(fromIdx: number, toIdx: number, value: T) {
const [child] = this._childInstances!.splice(fromIdx, 1);
this._childInstances!.splice(toIdx, 0, child);
child.root()!.remove();
insertAt(this._root!, toIdx, child.root()! as Element);
}
protected onUpdate(i: number, value: T, params: any) {
onUpdate(i: number, value: T, params: any) {
if (this._childInstances) {
const instance = this._childInstances![i];
instance && instance.update(value, params);

View file

@ -26,7 +26,7 @@ import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
import {AnnouncementView} from "./timeline/AnnouncementView.js";
import {RedactedView} from "./timeline/RedactedView.js";
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList.js";
import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList";
//import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
export interface TimelineViewModel extends IObservableValue {
@ -211,7 +211,12 @@ class TilesListView extends ListView<SimpleTile, TileView> {
this.onChanged = onChanged;
}
protected onUpdate(index: number, value: SimpleTile, param: any) {
onReset() {
super.onReset();
this.onChanged();
}
onUpdate(index: number, value: SimpleTile, param: any) {
if (param === "shape") {
const ExpectedClass = viewClassForEntry(value);
const child = this.getChildInstanceByIndex(index);
@ -227,17 +232,17 @@ class TilesListView extends ListView<SimpleTile, TileView> {
this.onChanged();
}
protected onAdd(idx: number, value: SimpleTile) {
onAdd(idx: number, value: SimpleTile) {
super.onAdd(idx, value);
this.onChanged();
}
protected onRemove(idx: number, value: SimpleTile) {
onRemove(idx: number, value: SimpleTile) {
super.onRemove(idx, value);
this.onChanged();
}
protected onMove(fromIdx: number, toIdx: number, value: SimpleTile) {
onMove(fromIdx: number, toIdx: number, value: SimpleTile) {
super.onMove(fromIdx, toIdx, value);
this.onChanged();
}