moved to root
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
||||
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
||||
* THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
WI.View = class View extends WI.Object
|
||||
{
|
||||
constructor(element)
|
||||
{
|
||||
super();
|
||||
|
||||
this._element = element || document.createElement("div");
|
||||
this._element.__view = this;
|
||||
this._parentView = null;
|
||||
this._subviews = [];
|
||||
this._dirty = false;
|
||||
this._dirtyDescendantsCount = 0;
|
||||
this._isAttachedToRoot = false;
|
||||
this._layoutReason = null;
|
||||
this._didInitialLayout = false;
|
||||
}
|
||||
|
||||
// Static
|
||||
|
||||
static fromElement(element)
|
||||
{
|
||||
if (!element || !(element instanceof HTMLElement))
|
||||
return null;
|
||||
|
||||
if (element.__view instanceof WI.View)
|
||||
return element.__view;
|
||||
return null;
|
||||
}
|
||||
|
||||
static rootView()
|
||||
{
|
||||
if (!WI.View._rootView) {
|
||||
// Since the root view is attached by definition, it does not go through the
|
||||
// normal view attachment process. Simply mark it as attached.
|
||||
WI.View._rootView = new WI.View(document.body);
|
||||
WI.View._rootView._isAttachedToRoot = true;
|
||||
}
|
||||
|
||||
return WI.View._rootView;
|
||||
}
|
||||
|
||||
// Public
|
||||
|
||||
get element() { return this._element; }
|
||||
get layoutPending() { return this._dirty; }
|
||||
get parentView() { return this._parentView; }
|
||||
get subviews() { return this._subviews; }
|
||||
get isAttached() { return this._isAttachedToRoot; }
|
||||
|
||||
isDescendantOf(view)
|
||||
{
|
||||
let parentView = this._parentView;
|
||||
while (parentView) {
|
||||
if (parentView === view)
|
||||
return true;
|
||||
parentView = parentView.parentView;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
addSubview(view)
|
||||
{
|
||||
this.insertSubviewBefore(view, null);
|
||||
}
|
||||
|
||||
insertSubviewBefore(view, referenceView)
|
||||
{
|
||||
console.assert(view instanceof WI.View);
|
||||
console.assert(!referenceView || referenceView instanceof WI.View);
|
||||
console.assert(view !== WI.View._rootView, "Root view cannot be a subview.");
|
||||
|
||||
console.assert(!view.parentView, view);
|
||||
|
||||
if (this._subviews.includes(view)) {
|
||||
console.assert(false, "Cannot add view that is already a subview.", view);
|
||||
return;
|
||||
}
|
||||
|
||||
const beforeIndex = referenceView ? this._subviews.indexOf(referenceView) : this._subviews.length;
|
||||
if (beforeIndex === -1) {
|
||||
console.assert(false, "Cannot insert view. Invalid reference view.", referenceView);
|
||||
return;
|
||||
}
|
||||
|
||||
this._subviews.insertAtIndex(view, beforeIndex);
|
||||
|
||||
console.assert(!view.element.parentNode || this._element.contains(view.element.parentNode), "Subview DOM element must be a descendant of the parent view element.");
|
||||
if (!view.element.parentNode)
|
||||
this._element.insertBefore(view.element, referenceView ? referenceView.element : null);
|
||||
|
||||
view._didMoveToParent(this);
|
||||
}
|
||||
|
||||
removeSubview(view)
|
||||
{
|
||||
console.assert(view instanceof WI.View);
|
||||
console.assert(this._element.contains(view.element), "Subview DOM element must be a child of the parent view element.");
|
||||
|
||||
let index = this._subviews.lastIndexOf(view);
|
||||
if (index === -1) {
|
||||
console.assert(false, "Cannot remove view which isn't a subview.", view);
|
||||
return;
|
||||
}
|
||||
|
||||
view._didMoveToParent(null);
|
||||
|
||||
this._subviews.splice(index, 1);
|
||||
view.element.remove();
|
||||
}
|
||||
|
||||
removeAllSubviews()
|
||||
{
|
||||
for (let subview of this._subviews)
|
||||
subview._didMoveToParent(null);
|
||||
|
||||
this._subviews = [];
|
||||
this._element.removeChildren();
|
||||
}
|
||||
|
||||
replaceSubview(oldView, newView)
|
||||
{
|
||||
console.assert(oldView !== newView, "Cannot replace subview with itself.");
|
||||
if (oldView === newView)
|
||||
return;
|
||||
|
||||
this.insertSubviewBefore(newView, oldView);
|
||||
this.removeSubview(oldView);
|
||||
}
|
||||
|
||||
updateLayout(layoutReason)
|
||||
{
|
||||
this._setLayoutReason(layoutReason);
|
||||
this._layoutSubtree();
|
||||
this._parentView?.didLayoutSubtree();
|
||||
}
|
||||
|
||||
updateLayoutIfNeeded(layoutReason)
|
||||
{
|
||||
if (!this._dirty && this._didInitialLayout)
|
||||
return;
|
||||
|
||||
this.updateLayout(layoutReason);
|
||||
}
|
||||
|
||||
needsLayout(layoutReason)
|
||||
{
|
||||
this._setLayoutReason(layoutReason);
|
||||
|
||||
WI.View._scheduleLayoutForView(this);
|
||||
}
|
||||
|
||||
// Protected
|
||||
|
||||
get layoutReason() { return this._layoutReason; }
|
||||
get didInitialLayout() { return this._didInitialLayout; }
|
||||
|
||||
attached()
|
||||
{
|
||||
// Implemented by subclasses.
|
||||
}
|
||||
|
||||
detached()
|
||||
{
|
||||
// Implemented by subclasses.
|
||||
}
|
||||
|
||||
initialLayout()
|
||||
{
|
||||
// Implemented by subclasses.
|
||||
|
||||
// Called once when the view is shown for the first time.
|
||||
// Views with complex DOM subtrees should create UI elements in
|
||||
// initialLayout rather than at construction time.
|
||||
}
|
||||
|
||||
layout()
|
||||
{
|
||||
// Implemented by subclasses.
|
||||
|
||||
// Not responsible for recursing to child views.
|
||||
// Should not be called directly; use updateLayout() instead.
|
||||
}
|
||||
|
||||
didLayoutSubtree()
|
||||
{
|
||||
// Implemented by subclasses.
|
||||
|
||||
// Called after the view and its entire subtree have finished layout.
|
||||
// Also called on the immediate parent view that did not layout because it wasn't dirty.
|
||||
}
|
||||
|
||||
sizeDidChange()
|
||||
{
|
||||
// Implemented by subclasses.
|
||||
|
||||
// Called after initialLayout, and before layout.
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
_setDirty(dirty)
|
||||
{
|
||||
if (this._dirty === dirty)
|
||||
return;
|
||||
|
||||
this._dirty = dirty;
|
||||
|
||||
for (let parentView = this.parentView; parentView; parentView = parentView.parentView) {
|
||||
parentView._dirtyDescendantsCount += this._dirty ? 1 : -1;
|
||||
console.assert(parentView._dirtyDescendantsCount >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
_didMoveToParent(parentView)
|
||||
{
|
||||
if (this._parentView === parentView)
|
||||
return;
|
||||
|
||||
let dirtyDescendantsCount = this._dirtyDescendantsCount;
|
||||
if (this._dirty)
|
||||
++dirtyDescendantsCount;
|
||||
|
||||
if (dirtyDescendantsCount) {
|
||||
for (let view = this.parentView; view; view = view.parentView) {
|
||||
view._dirtyDescendantsCount -= dirtyDescendantsCount;
|
||||
console.assert(view._dirtyDescendantsCount >= 0);
|
||||
}
|
||||
}
|
||||
|
||||
this._parentView = parentView;
|
||||
let isAttachedToRoot = this.isDescendantOf(WI.View._rootView);
|
||||
|
||||
let views = [this];
|
||||
for (let i = 0; i < views.length; ++i) {
|
||||
let view = views[i];
|
||||
views.pushAll(view.subviews);
|
||||
|
||||
view._dirty = false;
|
||||
view._dirtyDescendantsCount = 0;
|
||||
|
||||
if (view._isAttachedToRoot === isAttachedToRoot)
|
||||
continue;
|
||||
|
||||
view._isAttachedToRoot = isAttachedToRoot;
|
||||
if (view._isAttachedToRoot)
|
||||
view.attached();
|
||||
else
|
||||
view.detached();
|
||||
}
|
||||
|
||||
if (isAttachedToRoot)
|
||||
WI.View._scheduleLayoutForView(this);
|
||||
}
|
||||
|
||||
_layoutSubtree()
|
||||
{
|
||||
this._setDirty(false);
|
||||
let isInitialLayout = !this._didInitialLayout;
|
||||
if (isInitialLayout) {
|
||||
console.assert(WI.setReentrantCheck(this, "initialLayout"), "ERROR: calling `initialLayout` while already in it", this);
|
||||
this.initialLayout();
|
||||
this._didInitialLayout = true;
|
||||
}
|
||||
|
||||
if (this._layoutReason === WI.View.LayoutReason.Resize || isInitialLayout) {
|
||||
console.assert(WI.setReentrantCheck(this, "sizeDidChange"), "ERROR: calling `sizeDidChange` while already in it", this);
|
||||
this.sizeDidChange();
|
||||
console.assert(WI.clearReentrantCheck(this, "sizeDidChange"), "ERROR: missing return from `sizeDidChange`", this);
|
||||
}
|
||||
|
||||
let savedLayoutReason = this._layoutReason;
|
||||
if (isInitialLayout) {
|
||||
// The initial layout should always be treated as dirty.
|
||||
this._setLayoutReason();
|
||||
}
|
||||
|
||||
console.assert(WI.setReentrantCheck(this, "layout"), "ERROR: calling `layout` while already in it", this);
|
||||
this.layout();
|
||||
console.assert(WI.clearReentrantCheck(this, "layout"), "ERROR: missing return from `layout`", this);
|
||||
|
||||
// Ensure that the initial layout override doesn't affects to subviews.
|
||||
this._layoutReason = savedLayoutReason;
|
||||
|
||||
if (WI.settings.debugEnableLayoutFlashing.value)
|
||||
this._drawLayoutFlashingOutline(isInitialLayout);
|
||||
|
||||
for (let view of this._subviews) {
|
||||
view._setLayoutReason(this._layoutReason);
|
||||
view._layoutSubtree();
|
||||
}
|
||||
|
||||
this._layoutReason = null;
|
||||
|
||||
console.assert(WI.setReentrantCheck(this, "didLayoutSubtree"), "ERROR: calling `didLayoutSubtree` while already in it", this);
|
||||
this.didLayoutSubtree();
|
||||
console.assert(WI.clearReentrantCheck(this, "didLayoutSubtree"), "ERROR: missing return from `didLayoutSubtree`", this);
|
||||
}
|
||||
|
||||
_setLayoutReason(layoutReason)
|
||||
{
|
||||
this._layoutReason = layoutReason || WI.View.LayoutReason.Dirty;
|
||||
}
|
||||
|
||||
_drawLayoutFlashingOutline(isInitialLayout)
|
||||
{
|
||||
if (this._layoutFlashingTimeout)
|
||||
clearTimeout(this._layoutFlashingTimeout);
|
||||
else
|
||||
this._layoutFlashingPreviousOutline = this._element.style.outline;
|
||||
|
||||
let hue = isInitialLayout ? 20 : 40;
|
||||
this._element.style.outline = `1px solid hsla(${hue}, 100%, 51%, 0.8)`;
|
||||
|
||||
this._layoutFlashingTimeout = setTimeout(() => {
|
||||
if (this._element)
|
||||
this._element.style.outline = this._layoutFlashingPreviousOutline;
|
||||
|
||||
this._layoutFlashingTimeout = undefined;
|
||||
this._layoutFlashingPreviousOutline = null;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Layout controller logic
|
||||
|
||||
static _scheduleLayoutForView(view)
|
||||
{
|
||||
view._setDirty(true);
|
||||
|
||||
if (!view._isAttachedToRoot)
|
||||
return;
|
||||
|
||||
if (WI.View._scheduledLayoutUpdateIdentifier)
|
||||
return;
|
||||
|
||||
WI.View._scheduledLayoutUpdateIdentifier = requestAnimationFrame(WI.View._visitViewTreeForLayout);
|
||||
}
|
||||
|
||||
static _visitViewTreeForLayout()
|
||||
{
|
||||
console.assert(WI.View._rootView, "Cannot layout view tree without a root.");
|
||||
|
||||
WI.View._scheduledLayoutUpdateIdentifier = undefined;
|
||||
|
||||
let views = [WI.View._rootView];
|
||||
let cleanViews = new Set;
|
||||
for (let i = 0; i < views.length; ++i) {
|
||||
let view = views[i];
|
||||
|
||||
if (cleanViews.has(view)) {
|
||||
console.assert(WI.setReentrantCheck(view, "didLayoutSubtree"), "ERROR: calling `didLayoutSubtree` while already in it", view);
|
||||
view.didLayoutSubtree();
|
||||
console.assert(WI.clearReentrantCheck(view, "didLayoutSubtree"), "ERROR: missing return from `didLayoutSubtree`", view);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (view.layoutPending) {
|
||||
view._layoutSubtree();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (view._dirtyDescendantsCount) {
|
||||
cleanViews.add(view);
|
||||
|
||||
let hasDirtySubview = false;
|
||||
for (let subview of view.subviews) {
|
||||
hasDirtySubview ||= subview.layoutPending;
|
||||
views.push(subview);
|
||||
}
|
||||
|
||||
// Add again the parent view to the list we're iterating over, right after its subviews.
|
||||
// By the time it is encountered again, all its subviews will have done _layoutSubtree() and then we can call didLayoutSubtree() on it.
|
||||
if (hasDirtySubview)
|
||||
views.push(view);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
WI.View.LayoutReason = {
|
||||
Dirty: Symbol("layout-reason-dirty"),
|
||||
Resize: Symbol("layout-reason-resize")
|
||||
};
|
||||
|
||||
WI.View._rootView = null;
|
||||
WI.View._scheduledLayoutUpdateIdentifier = undefined;
|
||||
Reference in New Issue
Block a user