ember-source
Version:
A JavaScript framework for creating ambitious web applications
777 lines (764 loc) • 22.9 kB
JavaScript
import { A as DOMOperations, B as isSafeString, F as normalizeStringValue, N as NS_SVG, b as CursorImpl, C as ConcreteBounds, i as clear } from './on-CrTl7JQU.js';
import { a as assert } from './assert-CUCJBR2C.js';
import { s as setLocalDebugType } from './debug-brand-B1TWjOCH.js';
import { S as StackImpl, e as expect } from './collections-GpG8lT2g.js';
import { registerDestructor, destroy } from '../@glimmer/destroyable/index.js';
import './fragment-EpVz5Xuc.js';
import { a as castToBrowser } from './simple-cast-DCvJLSin.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;
/*
* @method normalizeProperty
* @param element {HTMLElement}
* @param slotName {String}
* @returns {Object} { name, type }
*/
function normalizeProperty(element, slotName) {
let type, normalized;
if (slotName in element) {
normalized = slotName;
type = 'prop';
} else {
let lower = slotName.toLowerCase();
if (lower in element) {
type = 'prop';
normalized = lower;
} else {
type = 'attr';
normalized = slotName;
}
}
if (type === 'prop' && (normalized.toLowerCase() === 'style' || preferAttr(element.tagName, normalized))) {
type = 'attr';
}
return {
normalized,
type
};
}
// Properties that MUST be set as attributes because the DOM properties
// are read-only or have type mismatches with the HTML attributes.
//
// element.form is a read-only DOM property on all form-associated elements
// that returns the owning HTMLFormElement (or null). The HTML `form` attribute
// (set via setAttribute) associates the element with a form by ID.
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/form
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLSelectElement/form
const ATTR_OVERRIDES = {
INPUT: {
form: true,
// HTMLElement.autocorrect is a boolean DOM property, but the HTML attribute
// uses "on"/"off" strings. Setting `element.autocorrect = "off"` coerces to
// `true` (truthy string). Must use setAttribute for correct "on"/"off" behavior.
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autocorrect
autocorrect: true,
// HTMLInputElement.list is a read-only DOM property that returns the associated
// HTMLDataListElement (or null). Must use setAttribute to set the datalist ID.
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/list
list: true
},
SELECT: {
form: true
},
OPTION: {
form: true
},
TEXTAREA: {
form: true
},
LABEL: {
form: true
},
FIELDSET: {
form: true
},
LEGEND: {
form: true
},
OBJECT: {
form: true
},
OUTPUT: {
form: true
},
BUTTON: {
form: true
}
};
function preferAttr(tagName, propName) {
let tag = ATTR_OVERRIDES[tagName.toUpperCase()];
return !!(tag && tag[propName.toLowerCase()]);
}
const badProtocols = ['javascript:', 'vbscript:'];
const badTags = ['A', 'BODY', 'LINK', 'IMG', 'IFRAME', 'BASE', 'FORM'];
const badTagsForDataURI = ['EMBED'];
const badAttributes = ['href', 'src', 'background', 'action'];
const badAttributesForDataURI = ['src'];
function has(array, item) {
return array.indexOf(item) !== -1;
}
function checkURI(tagName, attribute) {
return (tagName === null || has(badTags, tagName)) && has(badAttributes, attribute);
}
function checkDataURI(tagName, attribute) {
if (tagName === null) return false;
return has(badTagsForDataURI, tagName) && has(badAttributesForDataURI, attribute);
}
function requiresSanitization(tagName, attribute) {
return checkURI(tagName, attribute) || checkDataURI(tagName, attribute);
}
function findProtocolForURL() {
const weirdURL = URL;
if (typeof weirdURL === 'object' && weirdURL !== null &&
// this is super annoying, TS thinks that URL **must** be a function so `URL.parse` check
// thinks it is `never` without this `as unknown as any`
typeof weirdURL.parse === 'function') {
// In Ember-land the `fastboot` package sets the `URL` global to `require('url')`
// ultimately, this should be changed (so that we can either rely on the natural `URL` global
// that exists) but for now we have to detect the specific `FastBoot` case first
//
// a future version of `fastboot` will detect if this legacy URL setup is required (by
// inspecting Ember version) and if new enough, it will avoid shadowing the `URL` global
// constructor with `require('url')`.
let nodeURL = weirdURL;
return url => {
let protocol = null;
if (typeof url === 'string') {
protocol = nodeURL.parse(url).protocol;
}
return protocol === null ? ':' : protocol;
};
} else if (typeof weirdURL === 'function') {
return _url => {
try {
let url = new weirdURL(_url);
return url.protocol;
} catch {
// any non-fully qualified url string will trigger an error (because there is no
// baseURI that we can provide; in that case we **know** that the protocol is
// "safe" because it isn't specifically one of the `badProtocols` listed above
// (and those protocols can never be the default baseURI)
return ':';
}
};
} else {
throw new Error(`/runtime needs a valid "globalThis.URL"`);
}
}
let _protocolForUrlImplementation;
function protocolForUrl(url) {
if (!_protocolForUrlImplementation) {
_protocolForUrlImplementation = findProtocolForURL();
}
return _protocolForUrlImplementation(url);
}
function sanitizeAttributeValue(element, attribute, value) {
if (value === null || value === undefined) {
return value;
}
if (isSafeString(value)) {
return value.toHTML();
}
const tagName = element.tagName.toUpperCase();
let str = normalizeStringValue(value);
if (checkURI(tagName, attribute)) {
let protocol = protocolForUrl(str);
if (has(badProtocols, protocol)) {
return `unsafe:${str}`;
}
}
if (checkDataURI(tagName, attribute)) {
return `unsafe:${str}`;
}
return str;
}
function dynamicAttribute(element, attr, namespace, isTrusting = false) {
const {
tagName,
namespaceURI
} = element;
const attribute = {
element,
name: attr,
namespace
};
if (namespaceURI === NS_SVG) {
return buildDynamicAttribute(tagName, attr, attribute);
}
const {
type,
normalized
} = normalizeProperty(element, attr);
if (type === 'attr') {
return buildDynamicAttribute(tagName, normalized, attribute);
} else {
return buildDynamicProperty(tagName, normalized, attribute);
}
}
function buildDynamicAttribute(tagName, name, attribute) {
if (requiresSanitization(tagName, name)) {
return new SafeDynamicAttribute(attribute);
} else {
return new SimpleDynamicAttribute(attribute);
}
}
function buildDynamicProperty(tagName, name, attribute) {
if (requiresSanitization(tagName, name)) {
return new SafeDynamicProperty(name, attribute);
}
if (isUserInputValue(tagName, name)) {
return new InputValueDynamicAttribute(name, attribute);
}
if (isOptionSelected(tagName, name)) {
return new OptionSelectedDynamicAttribute(name, attribute);
}
return new DefaultDynamicProperty(name, attribute);
}
class DynamicAttribute {
constructor(attribute) {
this.attribute = attribute;
}
}
class SimpleDynamicAttribute extends DynamicAttribute {
set(dom, value, _env) {
const normalizedValue = normalizeValue(value);
if (normalizedValue !== null) {
const {
name,
namespace
} = this.attribute;
dom.__setAttribute(name, normalizedValue, namespace);
}
}
update(value, _env) {
const normalizedValue = normalizeValue(value);
const {
element,
name
} = this.attribute;
if (normalizedValue === null) {
element.removeAttribute(name);
} else {
element.setAttribute(name, normalizedValue);
}
}
}
class DefaultDynamicProperty extends DynamicAttribute {
constructor(normalizedName, attribute) {
super(attribute);
this.normalizedName = normalizedName;
}
value;
set(dom, value, _env) {
if (value !== null && value !== undefined) {
this.value = value;
dom.__setProperty(this.normalizedName, value);
}
}
update(value, _env) {
const {
element
} = this.attribute;
if (this.value !== value) {
element[this.normalizedName] = this.value = value;
if (value === null || value === undefined) {
this.removeAttribute();
}
}
}
removeAttribute() {
// TODO this sucks but to preserve properties first and to meet current
// semantics we must do this.
const {
element,
namespace
} = this.attribute;
if (namespace) {
element.removeAttributeNS(namespace, this.normalizedName);
} else {
element.removeAttribute(this.normalizedName);
}
}
}
class SafeDynamicProperty extends DefaultDynamicProperty {
set(dom, value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.set(dom, sanitized, env);
}
update(value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.update(sanitized, env);
}
}
class SafeDynamicAttribute extends SimpleDynamicAttribute {
set(dom, value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.set(dom, sanitized, env);
}
update(value, env) {
const {
element,
name
} = this.attribute;
const sanitized = sanitizeAttributeValue(element, name, value);
super.update(sanitized, env);
}
}
class InputValueDynamicAttribute extends DefaultDynamicProperty {
set(dom, value) {
const normalized = normalizeStringValue(value);
dom.__setProperty('value', normalized);
// GH#19219: Browsers don't reflect `input.value = ''` as a value attribute when
// type is later changed to "radio"/"checkbox". Explicitly set the attribute for <input>.
// Not needed for <textarea> (no value attribute).
if (value === '' && this.attribute.element.tagName === 'INPUT') {
dom.__setAttribute('value', '', null);
}
}
update(value) {
const input = castToBrowser(this.attribute.element);
const currentValue = input.value;
const normalizedValue = normalizeStringValue(value);
if (currentValue !== normalizedValue) {
input.value = normalizedValue;
}
}
}
class OptionSelectedDynamicAttribute extends DefaultDynamicProperty {
set(dom, value) {
if (value !== null && value !== undefined && value !== false) {
dom.__setProperty('selected', true);
}
}
update(value) {
const option = castToBrowser(this.attribute.element);
if (value) {
option.selected = true;
} else {
option.selected = false;
}
}
}
function isOptionSelected(tagName, attribute) {
return tagName === 'OPTION' && attribute === 'selected';
}
function isUserInputValue(tagName, attribute) {
return (tagName === 'INPUT' || tagName === 'TEXTAREA') && attribute === 'value';
}
function normalizeValue(value) {
if (value === false || value === undefined || value === null || typeof value.toString === 'undefined') {
return null;
}
if (value === true) {
return '';
}
// onclick function etc in SSR
if (typeof value === 'function') {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-base-to-string -- @fixme
return String(value);
}
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, SimpleDynamicAttribute as S, DynamicAttribute as a, ResettableBlockImpl as b, clientBuilder as c, dynamicAttribute as d, normalizeProperty as n };