ember-source
Version:
A JavaScript framework for creating ambitious web applications
406 lines (400 loc) • 11.7 kB
JavaScript
import { b as DOMOperations, d as CursorImpl, C as ConcreteBounds, e as dynamicAttribute, j as clear } from './dynamic-CuBsUXX8.js';
import { a as assert } from './assert-CUCJBR2C.js';
import { s as setLocalDebugType } from './debug-brand-B1TWjOCH.js';
import './debug-to-string-BsFOvUtQ.js';
import { S as StackImpl, e as expect } from './collections-B8me-ZlQ.js';
import '@embroider/macros';
import { destroy, registerDestructor } from '../@glimmer/destroyable/index.js';
class TreeConstruction extends DOMOperations {
createElementNS(namespace, tag) {
return this.document.createElementNS(namespace, tag);
}
setAttribute(element, name, value, namespace = null) {
if (namespace) {
element.setAttributeNS(namespace, name, value);
} else {
element.setAttribute(name, value);
}
}
}
const DOMTreeConstruction = TreeConstruction;
class First {
constructor(node) {
this.node = node;
}
firstNode() {
return this.node;
}
}
class Last {
constructor(node) {
this.node = node;
}
lastNode() {
return this.node;
}
}
class NewTreeBuilder {
dom;
updateOperations;
constructing = null;
operations = null;
env;
cursors = new StackImpl();
modifierStack = new StackImpl();
blockStack = new StackImpl();
static forInitialRender(env, cursor) {
return new this(env, cursor.element, cursor.nextSibling).initialize();
}
static resume(env, block) {
let parentNode = block.parentElement();
let nextSibling = block.reset(env);
let stack = new this(env, parentNode, nextSibling).initialize();
stack.pushBlock(block);
return stack;
}
constructor(env, parentNode, nextSibling) {
this.pushElement(parentNode, nextSibling);
this.env = env;
this.dom = env.getAppendOperations();
this.updateOperations = env.getDOM();
}
initialize() {
this.pushAppendingBlock();
return this;
}
debugBlocks() {
return this.blockStack.toArray();
}
get element() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
return this.cursors.current.element;
}
get nextSibling() {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
return this.cursors.current.nextSibling;
}
get hasBlocks() {
return this.blockStack.size > 0;
}
block() {
return expect(this.blockStack.current);
}
popElement() {
this.cursors.pop();
expect(this.cursors.current);
}
pushAppendingBlock() {
return this.pushBlock(new AppendingBlockImpl(this.element));
}
pushResettableBlock() {
return this.pushBlock(new ResettableBlockImpl(this.element));
}
pushBlockList(list) {
return this.pushBlock(new AppendingBlockList(this.element, list));
}
pushBlock(block, isRemote = false) {
let current = this.blockStack.current;
if (current !== null) {
if (!isRemote) {
current.didAppendBounds(block);
}
}
this.__openBlock();
this.blockStack.push(block);
return block;
}
popBlock() {
this.block().finalize(this);
this.__closeBlock();
return expect(this.blockStack.pop());
}
__openBlock() {}
__closeBlock() {}
// todo return seems unused
openElement(tag) {
let element = this.__openElement(tag);
this.constructing = element;
return element;
}
__openElement(tag) {
return this.dom.createElement(tag, this.element);
}
flushElement(modifiers) {
let parent = this.element;
let element = expect(this.constructing);
this.__flushElement(parent, element);
this.constructing = null;
this.operations = null;
this.pushModifiers(modifiers);
this.pushElement(element, null);
this.didOpenElement(element);
}
__flushElement(parent, constructing) {
this.dom.insertBefore(parent, constructing, this.nextSibling);
}
closeElement() {
this.willCloseElement();
this.popElement();
return this.popModifiers();
}
pushRemoteElement(element, guid, insertBefore) {
return this.__pushRemoteElement(element, guid, insertBefore);
}
__pushRemoteElement(element, _guid, insertBefore) {
this.pushElement(element, insertBefore);
if (insertBefore === undefined) {
while (element.lastChild) {
element.removeChild(element.lastChild);
}
}
let block = new RemoteBlock(element);
return this.pushBlock(block, true);
}
popRemoteElement() {
const block = this.popBlock();
this.popElement();
return block;
}
pushElement(element, nextSibling = null) {
this.cursors.push(new CursorImpl(element, nextSibling));
}
pushModifiers(modifiers) {
this.modifierStack.push(modifiers);
}
popModifiers() {
return this.modifierStack.pop();
}
didAppendBounds(bounds) {
this.block().didAppendBounds(bounds);
return bounds;
}
didAppendNode(node) {
this.block().didAppendNode(node);
return node;
}
didOpenElement(element) {
this.block().openElement(element);
return element;
}
willCloseElement() {
this.block().closeElement();
}
appendText(string) {
return this.didAppendNode(this.__appendText(string));
}
__appendText(text) {
let {
dom,
element,
nextSibling
} = this;
let node = dom.createTextNode(text);
dom.insertBefore(element, node, nextSibling);
return node;
}
__appendNode(node) {
this.dom.insertBefore(this.element, node, this.nextSibling);
return node;
}
__appendFragment(fragment) {
let first = fragment.firstChild;
if (first) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
let ret = new ConcreteBounds(this.element, first, fragment.lastChild);
this.dom.insertBefore(this.element, fragment, this.nextSibling);
return ret;
} else {
const comment = this.__appendComment('');
return new ConcreteBounds(this.element, comment, comment);
}
}
__appendHTML(html) {
return this.dom.insertHTMLBefore(this.element, this.nextSibling, html);
}
appendDynamicHTML(value) {
let bounds = this.trustedContent(value);
this.didAppendBounds(bounds);
}
appendDynamicText(value) {
let node = this.untrustedContent(value);
this.didAppendNode(node);
return node;
}
appendDynamicFragment(value) {
let bounds = this.__appendFragment(value);
this.didAppendBounds(bounds);
}
appendDynamicNode(value) {
let node = this.__appendNode(value);
let bounds = new ConcreteBounds(this.element, node, node);
this.didAppendBounds(bounds);
}
trustedContent(value) {
return this.__appendHTML(value);
}
untrustedContent(value) {
return this.__appendText(value);
}
appendComment(string) {
return this.didAppendNode(this.__appendComment(string));
}
__appendComment(string) {
let {
dom,
element,
nextSibling
} = this;
let node = dom.createComment(string);
dom.insertBefore(element, node, nextSibling);
return node;
}
__setAttribute(name, value, namespace) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
this.dom.setAttribute(this.constructing, name, value, namespace);
}
__setProperty(name, value) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
this.constructing[name] = value;
}
setStaticAttribute(name, value, namespace) {
this.__setAttribute(name, value, namespace);
}
setDynamicAttribute(name, value, trusting, namespace) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- @fixme
let element = this.constructing;
let attribute = dynamicAttribute(element, name, namespace, trusting);
attribute.set(this, value, this.env);
return attribute;
}
}
class AppendingBlockImpl {
first = null;
last = null;
nesting = 0;
constructor(parent) {
this.parent = parent;
setLocalDebugType('block:simple', this);
}
parentElement() {
return this.parent;
}
firstNode() {
let first = expect(this.first);
return first.firstNode();
}
lastNode() {
let last = expect(this.last);
return last.lastNode();
}
openElement(element) {
this.didAppendNode(element);
this.nesting++;
}
closeElement() {
this.nesting--;
}
didAppendNode(node) {
if (this.nesting !== 0) return;
if (!this.first) {
this.first = new First(node);
}
this.last = new Last(node);
}
didAppendBounds(bounds) {
if (this.nesting !== 0) return;
if (!this.first) {
this.first = bounds;
}
this.last = bounds;
}
finalize(stack) {
if (this.first === null) {
stack.appendComment('');
}
}
}
class RemoteBlock extends AppendingBlockImpl {
constructor(parent) {
super(parent);
setLocalDebugType('block:remote', this);
registerDestructor(this, () => {
// In general, you only need to clear the root of a hierarchy, and should never
// need to clear any child nodes. This is an important constraint that gives us
// a strong guarantee that clearing a subtree is a single DOM operation.
//
// Because remote blocks are not normally physically nested inside of the tree
// that they are logically nested inside, we manually clear remote blocks when
// a logical parent is cleared.
//
// HOWEVER, it is currently possible for a remote block to be physically nested
// inside of the block it is logically contained inside of. This happens when
// the remote block is appended to the end of the application's entire element.
//
// The problem with that scenario is that Glimmer believes that it owns more of
// the DOM than it actually does. The code is attempting to write past the end
// of the Glimmer-managed root, but Glimmer isn't aware of that.
//
// The correct solution to that problem is for Glimmer to be aware of the end
// of the bounds that it owns, and once we make that change, this check could
// be removed.
//
// For now, a more targeted fix is to check whether the node was already removed
// and avoid clearing the node if it was. In most cases this shouldn't happen,
// so this might hide bugs where the code clears nested nodes unnecessarily,
// so we should eventually try to do the correct fix.
if (this.parentElement() === this.firstNode().parentNode) {
clear(this);
}
});
}
}
class ResettableBlockImpl extends AppendingBlockImpl {
constructor(parent) {
super(parent);
setLocalDebugType('block:resettable', this);
}
reset() {
destroy(this);
let nextSibling = clear(this);
this.first = null;
this.last = null;
this.nesting = 0;
return nextSibling;
}
}
// FIXME: All the noops in here indicate a modelling problem
class AppendingBlockList {
constructor(parent, boundList) {
this.parent = parent;
this.boundList = boundList;
this.parent = parent;
this.boundList = boundList;
}
parentElement() {
return this.parent;
}
firstNode() {
let head = expect(this.boundList[0]);
return head.firstNode();
}
lastNode() {
let boundList = this.boundList;
let tail = expect(boundList[boundList.length - 1]);
return tail.lastNode();
}
openElement(_element) {
}
closeElement() {
}
didAppendNode(_node) {
}
didAppendBounds(_bounds) {}
finalize(_stack) {
assert(this.boundList.length > 0);
}
}
function clientBuilder(env, cursor) {
return NewTreeBuilder.forInitialRender(env, cursor);
}
export { DOMTreeConstruction as D, NewTreeBuilder as N, RemoteBlock as R, ResettableBlockImpl as a, clientBuilder as c };