@surface/custom-element
Version:
Provides support of directives and data binding on custom elements.
115 lines (114 loc) • 4.38 kB
JavaScript
import { CancellationTokenSource, DisposableMetadata } from "@surface/core";
import { scheduler } from "../../singletons.js";
import Block from "../block.js";
import observe from "../observe.js";
class Cache {
constructor() {
this.entries = [];
this.changed = false;
this.cursor = 0;
this.stored = 0;
}
trim() {
if (this.stored > 0) {
this.entries.splice(this.cursor, this.stored - this.cursor).forEach(x => (x[2].dispose(), x[1].dispose()));
this.stored = 0;
}
}
add(value, block, disposable) {
this.entries.push([value, block, disposable]);
}
hasChanges(index, value) {
if (this.changed || !Object.is(value, this.entries[index]?.[0])) {
if (!this.changed) {
this.changed = true;
this.cursor = index;
}
return true;
}
return false;
}
resize(size) {
if (!this.changed) {
this.cursor = size;
}
this.trim();
this.changed = false;
this.cursor = 0;
this.stored = size;
}
}
export default class LoopStatement {
constructor(context) {
this.context = context;
this.cache = new Cache();
this.cancellationTokenSource = new CancellationTokenSource();
this.disposed = false;
this.task = () => {
if (this.disposed) {
return;
}
const elements = this.context.right(this.context.scope);
if (elements[Symbol.iterator]().next().done) {
this.cache.resize(0);
}
else {
const size = this.iterator(elements, this.action);
const task = () => {
this.cache.resize(size);
this.context.block.setContent(this.tree, false);
};
void scheduler.enqueue(task, "high", this.cancellationTokenSource.token);
}
};
this.tree = document.createDocumentFragment();
this.iterator = this.context.operator == "in" ? this.forInIterator : this.forOfIterator;
const listener = () => void scheduler.enqueue(this.task, "normal", this.cancellationTokenSource.token);
this.subscription = observe(context.scope, context.observables, listener, true);
listener();
}
action(value, index) {
if (this.cache.hasChanges(index, value)) {
const directiveScope = this.context.left(this.context.scope, value);
const scope = { $index: index, ...this.context.scope, ...directiveScope };
const [content, activator] = this.context.factory();
const disposables = [activator(this.context.parent, this.context.host, scope, new Map()), DisposableMetadata.from(scope)];
const disposable = { dispose: () => disposables.splice(0).forEach(x => x.dispose()) };
const block = new Block();
this.cache.add(value, block, disposable);
block.connect(this.tree);
block.setContent(content);
const count = index + 1;
if (Math.ceil(count / LoopStatement.maximumAmount) * LoopStatement.maximumAmount == count) {
this.cache.trim();
this.context.block.setContent(this.tree, false);
}
}
}
forOfIterator(elements) {
let index = 0;
for (const element of elements) {
const current = index++;
void scheduler.enqueue(() => this.action(element, current), "high", this.cancellationTokenSource.token);
}
return index;
}
forInIterator(elements) {
let index = 0;
for (const element in elements) {
const current = index++;
void scheduler.enqueue(() => this.action(element, current), "high", this.cancellationTokenSource.token);
}
return index;
}
dispose() {
if (!this.disposed) {
this.cancellationTokenSource.cancel();
this.cache.resize(0);
this.subscription.unsubscribe();
this.context.block.dispose();
this.disposed = true;
}
}
}
LoopStatement.maximumAmount = 1000;