wip on collections and listview
This commit is contained in:
parent
952f1abddf
commit
5bff41c1ee
14 changed files with 561 additions and 45 deletions
|
@ -21,6 +21,8 @@
|
||||||
- build a very basic interface with
|
- build a very basic interface with
|
||||||
- a start/stop sync button
|
- a start/stop sync button
|
||||||
- a room list sorted alphabetically
|
- a room list sorted alphabetically
|
||||||
|
- do some preprocessing on sync response which can then be used by persister, summary, timeline
|
||||||
|
- support timeline
|
||||||
- clicking on a room list, you see messages (userId -> body)
|
- clicking on a room list, you see messages (userId -> body)
|
||||||
- send messages
|
- send messages
|
||||||
- fill gaps with call to /messages
|
- fill gaps with call to /messages
|
||||||
|
@ -28,3 +30,4 @@
|
||||||
- lazy loading members
|
- lazy loading members
|
||||||
- decide denormalized data in summary vs reading from multiple stores PER room on load
|
- decide denormalized data in summary vs reading from multiple stores PER room on load
|
||||||
- allow Room/Summary class to be subclassed and store additional data?
|
- allow Room/Summary class to be subclassed and store additional data?
|
||||||
|
- store account data, support read markers
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
import RoomSummary from "./summary.js";
|
import RoomSummary from "./summary.js";
|
||||||
import RoomPersister from "./persister.js";
|
import RoomPersister from "./persister.js";
|
||||||
|
import EventEmitter from "../../EventEmitter.js";
|
||||||
|
|
||||||
export default class Room {
|
export default class Room extends EventEmitter {
|
||||||
constructor(roomId, storage) {
|
constructor(roomId, storage, emitCollectionChange) {
|
||||||
|
super();
|
||||||
this._roomId = roomId;
|
this._roomId = roomId;
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._summary = new RoomSummary(roomId);
|
this._summary = new RoomSummary(roomId);
|
||||||
this._persister = new RoomPersister(roomId);
|
this._persister = new RoomPersister(roomId);
|
||||||
|
this._emitCollectionChange = emitCollectionChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
async applySync(roomResponse, membership, txn) {
|
async applySync(roomResponse, membership, txn) {
|
||||||
this._summary.applySync(roomResponse, membership, txn);
|
const changed = this._summary.applySync(roomResponse, membership, txn);
|
||||||
this._persister.persistSync(roomResponse, txn);
|
this._persister.persistSync(roomResponse, txn);
|
||||||
|
if (changed) {
|
||||||
|
this.emit("change");
|
||||||
|
(this._emitCollectionChange)();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load(summary, txn) {
|
load(summary, txn) {
|
||||||
this._summary.load(summary);
|
this._summary.load(summary);
|
||||||
return this._persister.load(txn);
|
return this._persister.load(txn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import Room from "./room/room.js";
|
import Room from "./room/room.js";
|
||||||
|
import ObservableMap from "../observable/map.js";
|
||||||
|
|
||||||
export default class Session {
|
export default class Session {
|
||||||
constructor(storage) {
|
constructor(storage) {
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._session = null;
|
this._session = null;
|
||||||
// use Map here?
|
// use Map here?
|
||||||
this._rooms = {};
|
this._rooms = new ObservableMap();
|
||||||
}
|
}
|
||||||
// should be called before load
|
// should be called before load
|
||||||
// loginData has device_id, user_id, home_server, access_token
|
// loginData has device_id, user_id, home_server, access_token
|
||||||
|
@ -37,13 +38,18 @@ export default class Session {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get rooms() {
|
||||||
|
return this._rooms;
|
||||||
|
}
|
||||||
|
|
||||||
getRoom(roomId) {
|
getRoom(roomId) {
|
||||||
return this._rooms[roomId];
|
return this._rooms.get(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoom(roomId) {
|
createRoom(roomId) {
|
||||||
const room = new Room(roomId, this._storage);
|
const updateCallback = (params) => this._rooms.update(roomId, params);
|
||||||
this._rooms[roomId] = room;
|
const room = new Room(roomId, this._storage, updateCallback);
|
||||||
|
this._rooms.add(roomId, room);
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,4 +67,4 @@ export default class Session {
|
||||||
get accessToken() {
|
get accessToken() {
|
||||||
return this._session.loginData.access_token;
|
return this._session.loginData.access_token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {
|
||||||
HomeServerError,
|
HomeServerError,
|
||||||
StorageError
|
StorageError
|
||||||
} from "./error.js";
|
} from "./error.js";
|
||||||
import EventEmitter from "../event-emitter.js";
|
import EventEmitter from "../EventEmitter.js";
|
||||||
|
|
||||||
const INCREMENTAL_TIMEOUT = 30000;
|
const INCREMENTAL_TIMEOUT = 30000;
|
||||||
const SYNC_EVENT_LIMIT = 10;
|
const SYNC_EVENT_LIMIT = 10;
|
||||||
|
@ -117,4 +117,4 @@ export default class Sync extends EventEmitter {
|
||||||
this._currentRequest = null;
|
this._currentRequest = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
55
src/observable/map.js
Normal file
55
src/observable/map.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import BaseObservableMap from "./map/BaseObservableMap.js";
|
||||||
|
|
||||||
|
export default class ObservableMap extends BaseObservableMap {
|
||||||
|
constructor(initialValues) {
|
||||||
|
super();
|
||||||
|
this._values = new Map(initialValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(key, params) {
|
||||||
|
const value = this._values.get(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
this._values.add(key, value);
|
||||||
|
this.emitChange(key, value, params);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // or return existing value?
|
||||||
|
}
|
||||||
|
|
||||||
|
add(key, value) {
|
||||||
|
if (!this._values.has(key)) {
|
||||||
|
this._values.add(key, value);
|
||||||
|
this.emitAdd(key, value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false; // or return existing value?
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(key) {
|
||||||
|
const value = this._values.get(key);
|
||||||
|
if (value !== undefined) {
|
||||||
|
this._values.delete(key);
|
||||||
|
this.emitRemove(key, value);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this._values.clear();
|
||||||
|
this.emitReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
return this._values.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this._values.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this._values.entries()[Symbol.iterator];
|
||||||
|
}
|
||||||
|
}
|
66
src/observable/map/BaseObservableMap.js
Normal file
66
src/observable/map/BaseObservableMap.js
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import MapOperator from "./operators/MapOperator.js";
|
||||||
|
import SortOperator from "./operators/SortOperator.js";
|
||||||
|
|
||||||
|
export default class BaseObservableMap {
|
||||||
|
constructor() {
|
||||||
|
this._handlers = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitReset() {
|
||||||
|
for(let h of this._handlers) {
|
||||||
|
h.onReset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we need batch events, mostly on index based collection though?
|
||||||
|
// maybe we should get started without?
|
||||||
|
emitAdd(key, value) {
|
||||||
|
for(let h of this._handlers) {
|
||||||
|
h.onAdd(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitChange(key, value, ...params) {
|
||||||
|
for(let h of this._handlers) {
|
||||||
|
h.onChange(key, value, ...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitRemove(key, value) {
|
||||||
|
for(let h of this._handlers) {
|
||||||
|
h.onRemove(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(handler) {
|
||||||
|
this._handlers.add(handler);
|
||||||
|
if (this._handlers.length === 1) {
|
||||||
|
this.onSubscribeFirst();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (handler) {
|
||||||
|
this._handlers.delete(this._handler);
|
||||||
|
if (this._handlers.length === 0) {
|
||||||
|
this.onUnsubscribeLast();
|
||||||
|
}
|
||||||
|
handler = null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
map(mapper, updater) {
|
||||||
|
return new MapOperator(this, mapper, updater);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(comparator) {
|
||||||
|
return new SortOperator(this, comparator);
|
||||||
|
}
|
||||||
|
}
|
22
src/observable/map/Operator.js
Normal file
22
src/observable/map/Operator.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// import BaseObservableMap from "./BaseObservableMap.js";
|
||||||
|
|
||||||
|
export default class Operator /* extends BaseObservableMap */ {
|
||||||
|
constructor(source) {
|
||||||
|
// super();
|
||||||
|
this._source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
this._sourceSubscription = this._source.subscribe(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
this._sourceSubscription();
|
||||||
|
this._sourceSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(key, value) {}
|
||||||
|
onAdd(key, value) {}
|
||||||
|
onChange(key, value, params) {}
|
||||||
|
onReset() {}
|
||||||
|
}
|
55
src/observable/map/operators/FilterOperator.js
Normal file
55
src/observable/map/operators/FilterOperator.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import Operator from "../Operator.js";
|
||||||
|
|
||||||
|
export default class FilterOperator extends Operator {
|
||||||
|
constructor(source, mapper, updater) {
|
||||||
|
super(source);
|
||||||
|
this._mapper = mapper;
|
||||||
|
this._updater = updater;
|
||||||
|
this._mappedValues = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(key, value) {
|
||||||
|
const mappedValue = this._mapper(value);
|
||||||
|
this._mappedValues.set(key, mappedValue);
|
||||||
|
this.emitAdd(key, mappedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(key, _value) {
|
||||||
|
const mappedValue = this._mappedValues.get(key);
|
||||||
|
if (this._mappedValues.delete(key)) {
|
||||||
|
this.emitRemove(key, mappedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(key, value, params) {
|
||||||
|
const mappedValue = this._mappedValues.get(key);
|
||||||
|
if (mappedValue !== undefined) {
|
||||||
|
const newParams = this._updater(value, params);
|
||||||
|
if (newParams !== undefined) {
|
||||||
|
this.emitChange(key, mappedValue, newParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
for (let [key, value] of this._source) {
|
||||||
|
const mappedValue = this._mapper(value);
|
||||||
|
this._mappedValues.set(key, mappedValue);
|
||||||
|
}
|
||||||
|
super.onSubscribeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
super.onUnsubscribeLast();
|
||||||
|
this._mappedValues.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset() {
|
||||||
|
this._mappedValues.clear();
|
||||||
|
this.emitReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this._mappedValues.entries()[Symbol.iterator];
|
||||||
|
}
|
||||||
|
}
|
55
src/observable/map/operators/MapOperator.js
Normal file
55
src/observable/map/operators/MapOperator.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import Operator from "../Operator.js";
|
||||||
|
|
||||||
|
export default class MapOperator extends Operator {
|
||||||
|
constructor(source, mapper, updater) {
|
||||||
|
super(source);
|
||||||
|
this._mapper = mapper;
|
||||||
|
this._updater = updater;
|
||||||
|
this._mappedValues = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(key, value) {
|
||||||
|
const mappedValue = this._mapper(value);
|
||||||
|
this._mappedValues.set(key, mappedValue);
|
||||||
|
this.emitAdd(key, mappedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(key, _value) {
|
||||||
|
const mappedValue = this._mappedValues.get(key);
|
||||||
|
if (this._mappedValues.delete(key)) {
|
||||||
|
this.emitRemove(key, mappedValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(key, value, params) {
|
||||||
|
const mappedValue = this._mappedValues.get(key);
|
||||||
|
if (mappedValue !== undefined) {
|
||||||
|
const newParams = this._updater(value, params);
|
||||||
|
if (newParams !== undefined) {
|
||||||
|
this.emitChange(key, mappedValue, newParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
for (let [key, value] of this._source) {
|
||||||
|
const mappedValue = this._mapper(value);
|
||||||
|
this._mappedValues.set(key, mappedValue);
|
||||||
|
}
|
||||||
|
super.onSubscribeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
super.onUnsubscribeLast();
|
||||||
|
this._mappedValues.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset() {
|
||||||
|
this._mappedValues.clear();
|
||||||
|
this.emitReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this._mappedValues.entries()[Symbol.iterator];
|
||||||
|
}
|
||||||
|
}
|
119
src/observable/map/operators/SortOperator.js
Normal file
119
src/observable/map/operators/SortOperator.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import Operator from "../Operator.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Based off baseSortedIndex function in Lodash <https://lodash.com/>
|
||||||
|
* Copyright JS Foundation and other contributors <https://js.foundation/>
|
||||||
|
* Released under MIT license <https://lodash.com/license>
|
||||||
|
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
|
||||||
|
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
||||||
|
*/
|
||||||
|
function sortedIndex(array, value, comparator) {
|
||||||
|
let low = 0;
|
||||||
|
let high = array.length;
|
||||||
|
|
||||||
|
while (low < high) {
|
||||||
|
let mid = (low + high) >>> 1;
|
||||||
|
let cmpResult = comparator(value, array[mid]);
|
||||||
|
|
||||||
|
if (cmpResult > 0) {
|
||||||
|
low = mid + 1;
|
||||||
|
} else if (cmpResult < 0) {
|
||||||
|
high = mid;
|
||||||
|
} else {
|
||||||
|
low = high = mid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return high;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this should not inherit from an BaseObservableMap, as it's a list
|
||||||
|
export default class SortOperator extends Operator {
|
||||||
|
constructor(sourceMap, comparator) {
|
||||||
|
super(sourceMap);
|
||||||
|
this._comparator = comparator;
|
||||||
|
this._sortedValues = [];
|
||||||
|
this._keyIndex = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(key, value) {
|
||||||
|
const idx = sortedIndex(this._sortedValues, value, this._comparator);
|
||||||
|
this._sortedValues.splice(idx, 0, value);
|
||||||
|
this._keyIndex.set(key, idx);
|
||||||
|
this.emitAdd(idx, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(key, _value) {
|
||||||
|
const idx = sortedIndex(this._sortedValues, value, this._comparator);
|
||||||
|
this._sortedValues.splice(idx, 0, value);
|
||||||
|
this._keyIndex.set(key, idx);
|
||||||
|
this.emitAdd(idx, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(key, value, params) {
|
||||||
|
// index could have moved if other items got added in the meantime
|
||||||
|
const oldIdx = this._keyIndex.get(key);
|
||||||
|
this._sortedValues.splice(oldIdx, 1);
|
||||||
|
const idx = sortedIndex(this._sortedValues, value, this._comparator);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
this._sortedValues = new Array(this._source.size);
|
||||||
|
let i = 0;
|
||||||
|
for (let [key, value] of this._source) {
|
||||||
|
this._sortedValues[i] = value;
|
||||||
|
this._keyIndex.set(key, i);
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
this._sortedValues.sort(this._comparator);
|
||||||
|
super.onSubscribeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
super.onUnsubscribeLast();
|
||||||
|
this._sortedValues = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset() {
|
||||||
|
this._sortedValues = [];
|
||||||
|
this.emitReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
get length() {
|
||||||
|
return this._source.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this._sortedValues;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#ifdef TESTS
|
||||||
|
export function tests() {
|
||||||
|
return {
|
||||||
|
test_sortIndex(assert) {
|
||||||
|
let idx = sortedIndex([1, 5, 6, 8], 0, (a, b) => a - b);
|
||||||
|
assert.equal(idx, 0);
|
||||||
|
idx = sortedIndex([1, 5, 6, 8], 3, (a, b) => a - b);
|
||||||
|
assert.equal(idx, 1);
|
||||||
|
idx = sortedIndex([1, 5, 6, 8], 8, (a, b) => a - b);
|
||||||
|
assert.equal(idx, 3);
|
||||||
|
},
|
||||||
|
|
||||||
|
test_sortIndex_reverse(assert) {
|
||||||
|
let idx = sortedIndex([8, 6, 5, 1], 6, (a, b) => b - a);
|
||||||
|
assert.equal(idx, 1);
|
||||||
|
},
|
||||||
|
|
||||||
|
test_sortIndex_likeArraySort(assert) {
|
||||||
|
const a = [5, 1, 8, 2];
|
||||||
|
const cmp = (a, b) => a - b;
|
||||||
|
a.sort(cmp);
|
||||||
|
assert.deepEqual(a, [1, 2, 5, 8]);
|
||||||
|
let idx = sortedIndex(a, 2, cmp);
|
||||||
|
assert.equal(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endif
|
|
@ -1,6 +1,4 @@
|
||||||
import EventEmitter from "./event-emitter.js";
|
class ObservableMap {
|
||||||
|
|
||||||
class LiveMap {
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this._handlers = new Set();
|
this._handlers = new Set();
|
||||||
}
|
}
|
||||||
|
@ -30,43 +28,46 @@ class LiveMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(handler) {
|
subscribe(handler) {
|
||||||
this._handlers.add(handler);
|
this._handlers.add(handler);
|
||||||
|
if (this._handlers.length === 1) {
|
||||||
|
this.onSubscribeFirst();
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
if (handler) {
|
if (handler) {
|
||||||
this._handlers.delete(this._handler);
|
this._handlers.delete(this._handler);
|
||||||
|
if (this._handlers.length === 0) {
|
||||||
|
this.onUnsubscribeLast();
|
||||||
|
}
|
||||||
handler = null;
|
handler = null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Operator extends LiveMap {
|
class Operator extends ObservableMap {
|
||||||
constructor(source) {
|
constructor(source) {
|
||||||
super();
|
super();
|
||||||
this._source = source;
|
this._source = source;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(handler) {
|
onSubscribeFirst() {
|
||||||
this.onSubscribe(this._source);
|
this._sourceSubscription = this._source.subscribe(this);
|
||||||
let subscription = super.subscribe(handler);
|
}
|
||||||
let sourceSubscription = this._source.subscribe(this);
|
|
||||||
return () => {
|
|
||||||
sourceSubscription = sourceSubscription && sourceSubscription();
|
|
||||||
subscription = subscription && subscription();
|
|
||||||
// this.onUnsubscribe(); ?
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubscribe() {
|
onUnsubscribeLast() {
|
||||||
|
this._sourceSubscription();
|
||||||
}
|
this._sourceSubscription = null;
|
||||||
|
}
|
||||||
|
|
||||||
onRemove(key, value) {}
|
onRemove(key, value) {}
|
||||||
onAdd(key, value) {}
|
onAdd(key, value) {}
|
||||||
|
@ -74,7 +75,7 @@ class Operator extends LiveMap {
|
||||||
onReset() {}
|
onReset() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class LiveMapCollection extends LiveMap {
|
export default class ObservableMapCollection extends ObservableMap {
|
||||||
constructor(initialValues) {
|
constructor(initialValues) {
|
||||||
super();
|
super();
|
||||||
this._values = new Map(initialValues);
|
this._values = new Map(initialValues);
|
||||||
|
@ -124,7 +125,7 @@ export default class LiveMapCollection extends LiveMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LiveMapOperator extends Operator {
|
class ObservableMapOperator extends Operator {
|
||||||
constructor(source, mapper, updater) {
|
constructor(source, mapper, updater) {
|
||||||
super(source);
|
super(source);
|
||||||
this._mapper = mapper;
|
this._mapper = mapper;
|
||||||
|
@ -132,13 +133,6 @@ class LiveMapOperator extends Operator {
|
||||||
this._mappedValues = new Map();
|
this._mappedValues = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribe(source) {
|
|
||||||
for (let [key, value] of source) {
|
|
||||||
const mappedValue = this._mapper(value);
|
|
||||||
this._mappedValues.set(key, mappedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(key, value) {
|
onAdd(key, value) {
|
||||||
const mappedValue = this._mapper(value);
|
const mappedValue = this._mapper(value);
|
||||||
this._mappedValues.set(key, mappedValue);
|
this._mappedValues.set(key, mappedValue);
|
||||||
|
@ -162,6 +156,19 @@ class LiveMapOperator extends Operator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
for (let [key, value] of this._source) {
|
||||||
|
const mappedValue = this._mapper(value);
|
||||||
|
this._mappedValues.set(key, mappedValue);
|
||||||
|
}
|
||||||
|
super.onSubscribeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
super.onUnsubscribeLast();
|
||||||
|
this._mappedValues.clear();
|
||||||
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset() {
|
||||||
this._mappedValues.clear();
|
this._mappedValues.clear();
|
||||||
this.emitReset();
|
this.emitReset();
|
||||||
|
@ -172,12 +179,12 @@ class LiveMapOperator extends Operator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilterOperator extends LiveMapOperator {
|
class FilterOperator extends ObservableMapOperator {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SortSet {
|
class SortSet {
|
||||||
constructor(liveMap) {
|
constructor(ObservableMap) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,4 +195,4 @@ export function tests() {
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
78
src/ui/web/ListView.js
Normal file
78
src/ui/web/ListView.js
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import * as html from "./html.js";
|
||||||
|
|
||||||
|
class UIView {
|
||||||
|
mount(initialValue) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ListView {
|
||||||
|
constructor(collection, childCreator) {
|
||||||
|
this._collection = collection;
|
||||||
|
this._root = null;
|
||||||
|
this._subscription = null;
|
||||||
|
this._childCreator = childCreator;
|
||||||
|
this._childInstances = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDOMNode() {
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
mount() {
|
||||||
|
this._subscription = this._collection.subscribe(this);
|
||||||
|
this._root = html.ul({className: "ListView"});
|
||||||
|
this._childInstances = new Array(this._collection.length);
|
||||||
|
for (let item of this._collection) {
|
||||||
|
const child = this._childCreator(item);
|
||||||
|
this._childInstances.push(child);
|
||||||
|
const childDomNode = child.mount();
|
||||||
|
this._root.appendChild(childDomNode);
|
||||||
|
}
|
||||||
|
return this._root;
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount() {
|
||||||
|
this._subscription = this._subscription();
|
||||||
|
for (let child of this._childInstances) {
|
||||||
|
child.unmount();
|
||||||
|
}
|
||||||
|
this._childInstances = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(i, value) {
|
||||||
|
const child = this._childCreator(value);
|
||||||
|
const childDomNode = child.mount();
|
||||||
|
this._childInstances.splice(i, 0, child);
|
||||||
|
const isLast = i === this._collection.length - 1;
|
||||||
|
if (isLast) {
|
||||||
|
this._root.appendChild(childDomNode);
|
||||||
|
} else {
|
||||||
|
const nextDomNode = this._childInstances[i + 1].getDOMNode();
|
||||||
|
this._root.insertBefore(childDomNode, nextDomNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(i, _value) {
|
||||||
|
const [child] = this._childInstances.splice(i, 1);
|
||||||
|
child.getDOMNode().remove();
|
||||||
|
child.unmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMove(fromIdx, toIdx, value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(i, value) {
|
||||||
|
this._childInstances[i].update(value);
|
||||||
|
}
|
||||||
|
}
|
43
src/ui/web/html.js
Normal file
43
src/ui/web/html.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
export function setAttribute(el, name, value) {
|
||||||
|
el.setAttribute(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function el(elementName, attrs, children) {
|
||||||
|
const e = document.createElement(elementName);
|
||||||
|
if (typeof attrs === "object") {
|
||||||
|
for (let [name, value] of Object.entries(attrs)) {
|
||||||
|
setAttribute(e, name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
// TODO: use fragment here?
|
||||||
|
for (let c of children) {
|
||||||
|
e.appendChild(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function text(str) {
|
||||||
|
return document.createTextNode(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ol(... params) { return el("ol", ... params); }
|
||||||
|
export function ul(... params) { return el("ul", ... params); }
|
||||||
|
export function li(... params) { return el("li", ... params); }
|
||||||
|
export function div(... params) { return el("div", ... params); }
|
||||||
|
export function h1(... params) { return el("h1", ... params); }
|
||||||
|
export function h2(... params) { return el("h2", ... params); }
|
||||||
|
export function h3(... params) { return el("h3", ... params); }
|
||||||
|
export function h4(... params) { return el("h4", ... params); }
|
||||||
|
export function h5(... params) { return el("h5", ... params); }
|
||||||
|
export function h6(... params) { return el("h6", ... params); }
|
||||||
|
export function p(... params) { return el("p", ... params); }
|
||||||
|
export function strong(... params) { return el("strong", ... params); }
|
||||||
|
export function em(... params) { return el("em", ... params); }
|
||||||
|
export function span(... params) { return el("span", ... params); }
|
||||||
|
export function img(... params) { return el("img", ... params); }
|
||||||
|
export function section(... params) { return el("section", ... params); }
|
||||||
|
export function main(... params) { return el("main", ... params); }
|
||||||
|
export function article(... params) { return el("article", ... params); }
|
||||||
|
export function aside(... params) { return el("aside", ... params); }
|
Reference in a new issue