@texuli/core 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/portal/index.d.ts +10 -1
- package/dist/portal/index.js +44 -32
- package/dist/portal/scope.d.ts +58 -0
- package/dist/portal/scope.js +118 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ when you need Texuli's URL policy to sanitize links and media from agents
|
|
|
12
12
|
or users. It has no runtime dependencies and imports no UI framework.
|
|
13
13
|
|
|
14
14
|
> **Status: 0.x release.** Texuli is in development. The packages are
|
|
15
|
-
> published to npm as `0.
|
|
16
|
-
> stable release). Under 0.x, a new minor (`0.
|
|
17
|
-
> changes, so pin the exact version or use `^0.
|
|
15
|
+
> published to npm as `0.9.0`, a pre-1.0 release (1.0.0 will be the first
|
|
16
|
+
> stable release). Under 0.x, a new minor (`0.10.0`) may contain breaking
|
|
17
|
+
> changes, so pin the exact version or use `^0.9.0`, which accepts only 0.9.x.
|
|
18
18
|
|
|
19
19
|
## Install
|
|
20
20
|
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { computeInertTargets, createInertRegistry, getInertRegistry, INERT_REGIS
|
|
|
10
10
|
export { ariaKeyShortcuts, createShortcutMap, detectPlatform, flipArrowForDirection, formatShortcut, getNavigationIntent, hasCommandModifier, isActivationKey, isComposing, isTextEntryTarget, type KeyboardEventLike, type MatchOptions, matchesShortcut, type NavigationIntent, type NavigationOptions, normalizeKey, type ParsedShortcut, type PlatformSource, parseShortcut, type ResolvedShortcut, resolveShortcut, type ShortcutBinding, type ShortcutMap, type ShortcutPlatform, type ShortcutStyle, } from './keyboard/index.js';
|
|
11
11
|
export { type BindLayerStackDocumentOptions, bindLayerStackDocument, createLayerStack, decideEscape, getLayerStack, isPointerOutsideTopLayer, LAYER_STACK_GLOBAL_KEY, type LayerEntry, type LayerId, type LayerRegistration, type LayerStack, type LayerStackDocumentBinding, type LayerStackGlobal, layerReceivesEscape, layerReceivesOutsideInteraction, type RegisterLayerInput, resetLayerStack, } from './layer-stack/index.js';
|
|
12
12
|
export { createOverlay, OVERLAY_MODE_ATTR, OVERLAY_Z_BASE, type OverlayController, type OverlayDismissEvent, type OverlayDismissReason, type OverlayMode, type OverlayOptions, type OverlayPhase, } from './overlay/index.js';
|
|
13
|
-
export { createPortal, type PortalController, type PortalOptions, type PortalTarget, } from './portal/index.js';
|
|
13
|
+
export { createPortal, createScopedPortal, createScopedPortalRoot, findPortalScope, PORTAL_ROOT_ATTRIBUTE, type PortalController, type PortalOptions, type PortalScope, type PortalTarget, SCOPE_ATTRIBUTE, SCOPE_SELECTOR, type ScopedPortalOptions, type ScopedPortalRoot, } from './portal/index.js';
|
|
14
14
|
export { type ApplyPositionOptions, type AutoUpdateOptions, applyPosition, autoUpdate, canUseCssAnchor, clearPosition, computeDomPosition, cssAnchorDeclarations, type DomPositionOptions, type FloatingPosition, getContainerRect, getScrollAncestors, getViewportRect, intersectRects, layoutBox, measureRect, observeMove, observeResize, type PositionFloatingOptions, type PositionReference, type PositionStrategy, positionFloating, readDirection, snapToDevicePixel, supportsCssAnchorPositioning, type VirtualElement, } from './position-dom/index.js';
|
|
15
15
|
export { type AnyPlacement, type ArrowPosition, arrowPosition, availableSpace, type ComputePositionOptions, type ComputePositionResult, computePosition, flipFallbacks, insetRect, isLogicalPlacement, isPlacement, type LogicalPlacement, listPlacements, mirrorPlacementForRtl, overflowOf, type Placement, type PositionOffset, place, type Rect, rect, rectBottom, rectRight, resolveLogicalPlacement, resolvePlacementForDirection, type SideOverflow, shiftIntoBoundary, sizeToAvailable, totalOverflow, } from './position-math/index.js';
|
|
16
16
|
export { type CreatePresenceOptions, createPresence, hasPendingMotion, PRESENCE_STATE_ATTR, type PresenceController, type PresenceHooks, type PresenceSnapshot, type PresenceState, parseCssTimeList, prefersReducedMotion, REDUCED_MOTION_QUERY, } from './presence/presence.js';
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { computeInertTargets, createInertRegistry, getInertRegistry, INERT_REGIS
|
|
|
10
10
|
export { ariaKeyShortcuts, createShortcutMap, detectPlatform, flipArrowForDirection, formatShortcut, getNavigationIntent, hasCommandModifier, isActivationKey, isComposing, isTextEntryTarget, matchesShortcut, normalizeKey, parseShortcut, resolveShortcut, } from "./keyboard/index.js";
|
|
11
11
|
export { bindLayerStackDocument, createLayerStack, decideEscape, getLayerStack, isPointerOutsideTopLayer, LAYER_STACK_GLOBAL_KEY, layerReceivesEscape, layerReceivesOutsideInteraction, resetLayerStack, } from "./layer-stack/index.js";
|
|
12
12
|
export { createOverlay, OVERLAY_MODE_ATTR, OVERLAY_Z_BASE, } from "./overlay/index.js";
|
|
13
|
-
export { createPortal, } from "./portal/index.js";
|
|
13
|
+
export { createPortal, createScopedPortal, createScopedPortalRoot, findPortalScope, PORTAL_ROOT_ATTRIBUTE, SCOPE_ATTRIBUTE, SCOPE_SELECTOR, } from "./portal/index.js";
|
|
14
14
|
export { applyPosition, autoUpdate, canUseCssAnchor, clearPosition, computeDomPosition, cssAnchorDeclarations, getContainerRect, getScrollAncestors, getViewportRect, intersectRects, layoutBox, measureRect, observeMove, observeResize, positionFloating, readDirection, snapToDevicePixel, supportsCssAnchorPositioning, } from "./position-dom/index.js";
|
|
15
15
|
export { arrowPosition, availableSpace, computePosition, flipFallbacks, insetRect, isLogicalPlacement, isPlacement, listPlacements, mirrorPlacementForRtl, overflowOf, place, rect, rectBottom, rectRight, resolveLogicalPlacement, resolvePlacementForDirection, shiftIntoBoundary, sizeToAvailable, totalOverflow, } from "./position-math/index.js";
|
|
16
16
|
export { createPresence, hasPendingMotion, PRESENCE_STATE_ATTR, parseCssTimeList, prefersReducedMotion, REDUCED_MOTION_QUERY, } from "./presence/presence.js";
|
package/dist/portal/index.d.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
export { createScopedPortal, createScopedPortalRoot, findPortalScope, PORTAL_ROOT_ATTRIBUTE, type PortalScope, SCOPE_ATTRIBUTE, SCOPE_SELECTOR, type ScopedPortalOptions, type ScopedPortalRoot, } from './scope.js';
|
|
2
|
+
/** A DOM element that can receive a portal, or `null` for the default (the document body). */
|
|
2
3
|
export type PortalTarget = Element | null | undefined;
|
|
3
4
|
/** Initial target for a portal. Omitting it sends the source to `body`. */
|
|
4
5
|
export type PortalOptions = {
|
|
5
6
|
target?: PortalTarget;
|
|
7
|
+
/**
|
|
8
|
+
* Where an omitted or `null` target goes instead of `body`, asked on every
|
|
9
|
+
* placement with the source's original parent (`null` falls back to body).
|
|
10
|
+
* `createScopedPortal` uses it to follow a token scope.
|
|
11
|
+
*/
|
|
12
|
+
fallback?: (origin: Element | null) => Element | null;
|
|
13
|
+
/** Called after every placement and at destroy, with the source's parent. */
|
|
14
|
+
settled?: (parent: Node | null) => void;
|
|
6
15
|
};
|
|
7
16
|
/** Controls the target of one portaled element and restores its original place. */
|
|
8
17
|
export type PortalController = {
|
package/dist/portal/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { createScopedPortal, createScopedPortalRoot, findPortalScope, PORTAL_ROOT_ATTRIBUTE, SCOPE_ATTRIBUTE, SCOPE_SELECTOR, } from "./scope.js";
|
|
1
2
|
/**
|
|
2
3
|
* Move an existing element to `target` or its owner document's body. The same
|
|
3
4
|
* element is moved, rather than cloned, so its listeners, framework state and
|
|
@@ -35,44 +36,16 @@ export function createPortal(source, options = {}) {
|
|
|
35
36
|
setTarget(target) {
|
|
36
37
|
if (disposed || !source || !ownerDocument)
|
|
37
38
|
return false;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
if (destination.ownerDocument !== ownerDocument ||
|
|
44
|
-
destination === source ||
|
|
45
|
-
source.contains(destination)) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
if (originMarker?.parentNode === destination && originalParent === destination) {
|
|
49
|
-
restoreToOrigin();
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
if (source.parentNode === destination)
|
|
53
|
-
return true;
|
|
54
|
-
const createdMarker = originMarker === null && originalParent !== null;
|
|
55
|
-
if (createdMarker && originalParent) {
|
|
56
|
-
originMarker = ownerDocument.createComment('texuli-portal-origin');
|
|
57
|
-
originalParent.insertBefore(originMarker, source);
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
destination.append(source);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
if (createdMarker && originMarker?.parentNode) {
|
|
64
|
-
originMarker.parentNode.removeChild(originMarker);
|
|
65
|
-
originMarker = null;
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
return true;
|
|
39
|
+
const moved = place(target);
|
|
40
|
+
options.settled?.(source.parentNode);
|
|
41
|
+
return moved;
|
|
70
42
|
},
|
|
71
43
|
destroy() {
|
|
72
44
|
if (disposed)
|
|
73
45
|
return;
|
|
74
46
|
disposed = true;
|
|
75
47
|
restoreToOrigin();
|
|
48
|
+
options.settled?.(source?.parentNode ?? null);
|
|
76
49
|
},
|
|
77
50
|
get currentTarget() {
|
|
78
51
|
return getCurrentTarget();
|
|
@@ -81,6 +54,45 @@ export function createPortal(source, options = {}) {
|
|
|
81
54
|
return source !== null && source !== undefined && source.parentNode !== originalParent;
|
|
82
55
|
},
|
|
83
56
|
};
|
|
57
|
+
function place(target) {
|
|
58
|
+
if (!source || !ownerDocument)
|
|
59
|
+
return false;
|
|
60
|
+
const origin = originMarker?.parentNode ?? originalParent;
|
|
61
|
+
const destination = target ??
|
|
62
|
+
options.fallback?.(origin?.nodeType === 1 ? origin : null) ??
|
|
63
|
+
ownerDocument.body;
|
|
64
|
+
if (!destination) {
|
|
65
|
+
restoreToOrigin();
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
if (destination.ownerDocument !== ownerDocument ||
|
|
69
|
+
destination === source ||
|
|
70
|
+
source.contains(destination)) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if (originMarker?.parentNode === destination && originalParent === destination) {
|
|
74
|
+
restoreToOrigin();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (source.parentNode === destination)
|
|
78
|
+
return true;
|
|
79
|
+
const createdMarker = originMarker === null && originalParent !== null;
|
|
80
|
+
if (createdMarker && originalParent) {
|
|
81
|
+
originMarker = ownerDocument.createComment('texuli-portal-origin');
|
|
82
|
+
originalParent.insertBefore(originMarker, source);
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
destination.append(source);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
if (createdMarker && originMarker?.parentNode) {
|
|
89
|
+
originMarker.parentNode.removeChild(originMarker);
|
|
90
|
+
originMarker = null;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
84
96
|
if (source)
|
|
85
97
|
controller.setTarget(options.target);
|
|
86
98
|
return controller;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token scopes for portals (ADR-023). A theme or design system applied to an
|
|
3
|
+
* element (`applyTheme(el)`) marks it `[data-tx-scope]` and writes its
|
|
4
|
+
* `--tx-*` custom properties inline. Content portaled to `body` would leave
|
|
5
|
+
* that subtree and lose the scope, so a scoped portal mounts it in a scoped
|
|
6
|
+
* portal root instead: a box-less (`display: contents`) body child that
|
|
7
|
+
* mirrors the scope's `data-tx-*` attributes and inline custom properties,
|
|
8
|
+
* kept in sync while it lives.
|
|
9
|
+
*/
|
|
10
|
+
import { type PortalController, type PortalOptions } from './index.js';
|
|
11
|
+
/** The attribute that marks an element as a token scope. */
|
|
12
|
+
export declare const SCOPE_ATTRIBUTE = "data-tx-scope";
|
|
13
|
+
/** The attribute on a scoped portal root. */
|
|
14
|
+
export declare const PORTAL_ROOT_ATTRIBUTE = "data-tx-portal-root";
|
|
15
|
+
/** What counts as a scope: the marker, or a theme attribute (scopes written before the marker). */
|
|
16
|
+
export declare const SCOPE_SELECTOR = "[data-tx-scope], [data-tx-theme]";
|
|
17
|
+
/**
|
|
18
|
+
* The nearest token scope of `from` (itself included): the closest
|
|
19
|
+
* `[data-tx-scope]` or `[data-tx-theme]` element. `null` without one, and
|
|
20
|
+
* when that scope already holds `body` (a page-wide theme on `<html>` reaches
|
|
21
|
+
* body portals as it is). Inside a scoped portal root, the scope it mirrors.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findPortalScope(from: Element | null | undefined): Element | null;
|
|
24
|
+
/** A body child that carries one scope's tokens to the content portaled into it. */
|
|
25
|
+
export type ScopedPortalRoot = {
|
|
26
|
+
/** The root element; portal content into it. */
|
|
27
|
+
readonly element: HTMLElement;
|
|
28
|
+
/** The scope it mirrors. */
|
|
29
|
+
readonly scope: Element;
|
|
30
|
+
/** Copy the scope again (the observer does this on every change). */
|
|
31
|
+
sync(): void;
|
|
32
|
+
/** Stop observing and remove the root; idempotent. */
|
|
33
|
+
destroy(): void;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Create a scoped portal root for `scope` at the end of its document's body.
|
|
37
|
+
* Returns `null` when the document has no body (SSR, a detached document).
|
|
38
|
+
* Without `MutationObserver` it mirrors once; call `sync()` after changes.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createScopedPortalRoot(scope: Element): ScopedPortalRoot | null;
|
|
41
|
+
/**
|
|
42
|
+
* The token scope a scoped portal follows, resolved on every placement:
|
|
43
|
+
* `'auto'` (default) finds the nearest scope around the source's original
|
|
44
|
+
* position; an element names one; a function returns one; `null` opts out
|
|
45
|
+
* (always `body`).
|
|
46
|
+
*/
|
|
47
|
+
export type PortalScope = Element | null | 'auto' | (() => Element | null);
|
|
48
|
+
export type ScopedPortalOptions = Omit<PortalOptions, 'fallback' | 'settled'> & {
|
|
49
|
+
scope?: PortalScope;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* `createPortal` that follows a token scope: an omitted or `null` target is
|
|
53
|
+
* the scoped portal root of the scope (created on demand, removed once the
|
|
54
|
+
* source leaves it), or `body` without a scope. An explicit target wins. The
|
|
55
|
+
* scope is resolved on every placement, so a theme applied after creation is
|
|
56
|
+
* followed by the next `setTarget`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createScopedPortal(source: Element | null | undefined, options?: ScopedPortalOptions): PortalController;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token scopes for portals (ADR-023). A theme or design system applied to an
|
|
3
|
+
* element (`applyTheme(el)`) marks it `[data-tx-scope]` and writes its
|
|
4
|
+
* `--tx-*` custom properties inline. Content portaled to `body` would leave
|
|
5
|
+
* that subtree and lose the scope, so a scoped portal mounts it in a scoped
|
|
6
|
+
* portal root instead: a box-less (`display: contents`) body child that
|
|
7
|
+
* mirrors the scope's `data-tx-*` attributes and inline custom properties,
|
|
8
|
+
* kept in sync while it lives.
|
|
9
|
+
*/
|
|
10
|
+
import { createPortal } from "./index.js";
|
|
11
|
+
/** The attribute that marks an element as a token scope. */
|
|
12
|
+
export const SCOPE_ATTRIBUTE = 'data-tx-scope';
|
|
13
|
+
/** The attribute on a scoped portal root. */
|
|
14
|
+
export const PORTAL_ROOT_ATTRIBUTE = 'data-tx-portal-root';
|
|
15
|
+
/** What counts as a scope: the marker, or a theme attribute (scopes written before the marker). */
|
|
16
|
+
export const SCOPE_SELECTOR = '[data-tx-scope], [data-tx-theme]';
|
|
17
|
+
/** Each live root, back to the scope it mirrors (a nested overlay mirrors the scope, not the root). */
|
|
18
|
+
const rootScopes = /* @__PURE__ */ new WeakMap();
|
|
19
|
+
/** `data-tx-*` attributes, except the portal and overlay markers. */
|
|
20
|
+
const mirrored = (name) => name.startsWith('data-tx-') && !/^data-tx-(portal|overlay)/.test(name);
|
|
21
|
+
/**
|
|
22
|
+
* The nearest token scope of `from` (itself included): the closest
|
|
23
|
+
* `[data-tx-scope]` or `[data-tx-theme]` element. `null` without one, and
|
|
24
|
+
* when that scope already holds `body` (a page-wide theme on `<html>` reaches
|
|
25
|
+
* body portals as it is). Inside a scoped portal root, the scope it mirrors.
|
|
26
|
+
*/
|
|
27
|
+
export function findPortalScope(from) {
|
|
28
|
+
const scope = from?.closest(SCOPE_SELECTOR) ?? null;
|
|
29
|
+
if (!scope)
|
|
30
|
+
return null;
|
|
31
|
+
const body = scope.ownerDocument.body;
|
|
32
|
+
return rootScopes.get(scope) ?? (body && scope.contains(body) ? null : scope);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Create a scoped portal root for `scope` at the end of its document's body.
|
|
36
|
+
* Returns `null` when the document has no body (SSR, a detached document).
|
|
37
|
+
* Without `MutationObserver` it mirrors once; call `sync()` after changes.
|
|
38
|
+
*/
|
|
39
|
+
export function createScopedPortalRoot(scope) {
|
|
40
|
+
const doc = scope.ownerDocument;
|
|
41
|
+
if (!doc.body)
|
|
42
|
+
return null;
|
|
43
|
+
const element = doc.createElement('div');
|
|
44
|
+
const style = element.style;
|
|
45
|
+
element.setAttribute(PORTAL_ROOT_ATTRIBUTE, '');
|
|
46
|
+
style.setProperty('display', 'contents', 'important');
|
|
47
|
+
let properties = [];
|
|
48
|
+
const sync = () => {
|
|
49
|
+
for (const { name } of Array.from(element.attributes)) {
|
|
50
|
+
if (mirrored(name) && !scope.hasAttribute(name))
|
|
51
|
+
element.removeAttribute(name);
|
|
52
|
+
}
|
|
53
|
+
for (const { name, value } of Array.from(scope.attributes)) {
|
|
54
|
+
if (mirrored(name))
|
|
55
|
+
element.setAttribute(name, value);
|
|
56
|
+
}
|
|
57
|
+
const inline = scope.style;
|
|
58
|
+
const next = Array.from(inline ?? []).filter((name) => name.startsWith('--'));
|
|
59
|
+
for (const name of properties)
|
|
60
|
+
if (!next.includes(name))
|
|
61
|
+
style.removeProperty(name);
|
|
62
|
+
for (const name of next) {
|
|
63
|
+
style.setProperty(name, inline.getPropertyValue(name), inline.getPropertyPriority(name));
|
|
64
|
+
}
|
|
65
|
+
properties = next;
|
|
66
|
+
};
|
|
67
|
+
sync();
|
|
68
|
+
doc.body.append(element);
|
|
69
|
+
rootScopes.set(element, scope);
|
|
70
|
+
const Observer = doc.defaultView?.MutationObserver;
|
|
71
|
+
const observer = Observer ? new Observer(sync) : null;
|
|
72
|
+
observer?.observe(scope, { attributes: true });
|
|
73
|
+
return {
|
|
74
|
+
element,
|
|
75
|
+
scope,
|
|
76
|
+
sync,
|
|
77
|
+
destroy() {
|
|
78
|
+
observer?.disconnect();
|
|
79
|
+
rootScopes.delete(element);
|
|
80
|
+
element.remove();
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* `createPortal` that follows a token scope: an omitted or `null` target is
|
|
86
|
+
* the scoped portal root of the scope (created on demand, removed once the
|
|
87
|
+
* source leaves it), or `body` without a scope. An explicit target wins. The
|
|
88
|
+
* scope is resolved on every placement, so a theme applied after creation is
|
|
89
|
+
* followed by the next `setTarget`.
|
|
90
|
+
*/
|
|
91
|
+
export function createScopedPortal(source, options = {}) {
|
|
92
|
+
const { scope = 'auto' } = options;
|
|
93
|
+
const roots = [];
|
|
94
|
+
return createPortal(source, {
|
|
95
|
+
target: options.target,
|
|
96
|
+
fallback(origin) {
|
|
97
|
+
const found = scope === 'auto' ? findPortalScope(origin) : typeof scope === 'function' ? scope() : scope;
|
|
98
|
+
if (!found)
|
|
99
|
+
return null;
|
|
100
|
+
let root = roots.find((entry) => entry.scope === found);
|
|
101
|
+
if (!root) {
|
|
102
|
+
root = createScopedPortalRoot(found) ?? undefined;
|
|
103
|
+
if (root)
|
|
104
|
+
roots.push(root);
|
|
105
|
+
}
|
|
106
|
+
return root?.element ?? null;
|
|
107
|
+
},
|
|
108
|
+
settled(parent) {
|
|
109
|
+
for (let index = roots.length - 1; index >= 0; index -= 1) {
|
|
110
|
+
const root = roots[index];
|
|
111
|
+
if (root.element === parent)
|
|
112
|
+
continue;
|
|
113
|
+
root.destroy();
|
|
114
|
+
roots.splice(index, 1);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|