@surface/custom-element
Version:
Provides support of directives and data binding on custom elements.
128 lines (127 loc) • 5.11 kB
JavaScript
import { CancellationTokenSource } from "@surface/core";
import { tryEvaluateExpression, tryEvaluatePattern, tryObserveByObservable } from "../common.js";
import TemplateProcessor from "../processors/template-processor.js";
import { scheduler } from "../singletons.js";
import TemplateBlock from "./template-block.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 LoopDirective {
constructor(template, descriptor, context) {
this.cache = new Cache();
this.cancellationTokenSource = new CancellationTokenSource();
this.templateBlock = new TemplateBlock();
this.disposed = false;
this.task = () => {
if (this.disposed) {
return;
}
const elements = tryEvaluateExpression(this.context.scope, this.descriptor.right, this.descriptor.rawExpression, this.descriptor.stackTrace);
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.templateBlock.setContent(this.tree, false);
};
void scheduler.enqueue(task, "high", this.cancellationTokenSource.token);
}
};
this.template = template;
this.descriptor = descriptor;
this.context = context;
this.tree = document.createDocumentFragment();
this.iterator = descriptor.operator == "in" ? this.forInIterator : this.forOfIterator;
const parent = this.template.parentNode;
this.templateBlock.insertAt(parent, template);
const listener = () => void scheduler.enqueue(this.task, "normal", this.cancellationTokenSource.token);
this.subscription = tryObserveByObservable(context.scope, descriptor, listener, true);
listener();
}
action(value, index) {
if (this.cache.hasChanges(index, value)) {
const directiveScope = tryEvaluatePattern(this.context.scope, this.descriptor.left, value, this.descriptor.rawExpression, this.descriptor.stackTrace);
const mergedScope = { $index: index, ...this.context.scope, ...directiveScope };
const content = this.template.content.cloneNode(true);
const context = {
directives: this.context.directives,
host: this.context.host,
parentNode: this.context.parentNode,
root: content,
scope: mergedScope,
templateDescriptor: this.descriptor.descriptor,
};
const disposable = TemplateProcessor.process(context);
const block = new TemplateBlock();
this.cache.add(value, block, disposable);
block.connect(this.tree);
block.setContent(content);
const count = index + 1;
if (Math.ceil(count / LoopDirective.maximumAmount) * LoopDirective.maximumAmount == count) {
this.cache.trim();
this.templateBlock.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.templateBlock.dispose();
this.disposed = true;
}
}
}
LoopDirective.maximumAmount = 1000;