@trunkjs/element-relocator 1.0.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 +52 -0
- package/index.d.ts +22 -0
- package/index.js +96 -0
- package/package.json +18 -0
- package/skills/element-relocator/SKILL.md +61 -0
- package/web-types.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @trunkjs/element-relocator
|
|
2
|
+
|
|
3
|
+
A small Custom Element for temporarily moving an existing DOM element to a responsive destination and restoring its exact original position afterwards.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<tj-responsive>
|
|
9
|
+
<header>
|
|
10
|
+
<nav id="navigation">...</nav>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<aside>
|
|
14
|
+
<tj-element-relocator
|
|
15
|
+
source="#navigation"
|
|
16
|
+
class="md:relocate"
|
|
17
|
+
></tj-element-relocator>
|
|
18
|
+
</aside>
|
|
19
|
+
</tj-responsive>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`tj-element-relocator` itself has no breakpoint logic. `@trunkjs/responsive` (or any other mechanism) controls whether the `relocate` class is present.
|
|
23
|
+
|
|
24
|
+
When `relocate` appears, the element selected by `source` is moved to the relocator. When it disappears, the source is restored to its exact original DOM position using an internal comment anchor.
|
|
25
|
+
|
|
26
|
+
## Placement
|
|
27
|
+
|
|
28
|
+
`placement` controls where the source is placed relative to `<tj-element-relocator>`:
|
|
29
|
+
|
|
30
|
+
- `inside` (default): source becomes a child of the relocator.
|
|
31
|
+
- `before`: source becomes the previous sibling of the relocator.
|
|
32
|
+
- `after`: source becomes the next sibling of the relocator.
|
|
33
|
+
|
|
34
|
+
Sibling placement is useful for Web Components and slots because the relocated source remains in the relocator's parent light DOM.
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<my-layout>
|
|
38
|
+
<tj-element-relocator
|
|
39
|
+
source="#actions"
|
|
40
|
+
placement="after"
|
|
41
|
+
class="md:relocate"
|
|
42
|
+
></tj-element-relocator>
|
|
43
|
+
|
|
44
|
+
<div id="actions" slot="toolbar">...</div>
|
|
45
|
+
</my-layout>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Class contract
|
|
49
|
+
|
|
50
|
+
The element observes changes to `class`, `source`, and `placement`.
|
|
51
|
+
|
|
52
|
+
The only supported plain class is `relocate`. Class names containing `:` are accepted as responsive expressions. Any other class name without `:` causes a `console.warn`, but does not prevent relocation.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type TjElementRelocatorPlacement = 'inside' | 'before' | 'after';
|
|
2
|
+
export declare class TjElementRelocatorElement extends HTMLElement {
|
|
3
|
+
static get observedAttributes(): string[];
|
|
4
|
+
private sourceElement;
|
|
5
|
+
private sourceAnchor;
|
|
6
|
+
connectedCallback(): void;
|
|
7
|
+
disconnectedCallback(): void;
|
|
8
|
+
attributeChangedCallback(): void;
|
|
9
|
+
private sync;
|
|
10
|
+
private relocate;
|
|
11
|
+
private matchesCurrentSource;
|
|
12
|
+
private placeSource;
|
|
13
|
+
private restore;
|
|
14
|
+
private get placement();
|
|
15
|
+
private warn;
|
|
16
|
+
private warnAboutUnsupportedClasses;
|
|
17
|
+
}
|
|
18
|
+
declare global {
|
|
19
|
+
interface HTMLElementTagNameMap {
|
|
20
|
+
'tj-element-relocator': TjElementRelocatorElement;
|
|
21
|
+
}
|
|
22
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const n = "relocate", o = "<tj-element-relocator> warning";
|
|
2
|
+
class c extends HTMLElement {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(...arguments), this.sourceElement = null, this.sourceAnchor = null;
|
|
5
|
+
}
|
|
6
|
+
static get observedAttributes() {
|
|
7
|
+
return ["class", "source", "placement"];
|
|
8
|
+
}
|
|
9
|
+
connectedCallback() {
|
|
10
|
+
this.sync();
|
|
11
|
+
}
|
|
12
|
+
disconnectedCallback() {
|
|
13
|
+
this.restore();
|
|
14
|
+
}
|
|
15
|
+
attributeChangedCallback() {
|
|
16
|
+
this.isConnected && this.sync();
|
|
17
|
+
}
|
|
18
|
+
sync() {
|
|
19
|
+
this.warnAboutUnsupportedClasses(), this.classList.contains(n) ? this.relocate() : this.restore();
|
|
20
|
+
}
|
|
21
|
+
relocate() {
|
|
22
|
+
var t;
|
|
23
|
+
const e = (t = this.getAttribute("source")) == null ? void 0 : t.trim();
|
|
24
|
+
if (!e) {
|
|
25
|
+
this.warn('Missing "source" selector.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (this.sourceElement && !this.matchesCurrentSource(e) && this.restore(), !this.sourceElement) {
|
|
29
|
+
let r;
|
|
30
|
+
try {
|
|
31
|
+
r = this.ownerDocument.querySelector(e);
|
|
32
|
+
} catch {
|
|
33
|
+
this.warn(`Invalid "source" selector: "${e}".`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!r) {
|
|
37
|
+
this.warn(`Source not found: "${e}".`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (r === this || r.contains(this)) {
|
|
41
|
+
this.warn("Source is the relocator or one of its ancestors.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const s = r.parentNode;
|
|
45
|
+
if (!s) {
|
|
46
|
+
this.warn(`Source is detached: "${e}".`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.sourceAnchor = this.ownerDocument.createComment("tj-element-relocator:source"), s.insertBefore(this.sourceAnchor, r), this.sourceElement = r;
|
|
50
|
+
}
|
|
51
|
+
this.placeSource();
|
|
52
|
+
}
|
|
53
|
+
matchesCurrentSource(e) {
|
|
54
|
+
var t;
|
|
55
|
+
try {
|
|
56
|
+
return ((t = this.sourceElement) == null ? void 0 : t.matches(e)) ?? !1;
|
|
57
|
+
} catch {
|
|
58
|
+
return !1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
placeSource() {
|
|
62
|
+
if (this.sourceElement)
|
|
63
|
+
switch (this.placement) {
|
|
64
|
+
case "before":
|
|
65
|
+
this.before(this.sourceElement);
|
|
66
|
+
break;
|
|
67
|
+
case "after":
|
|
68
|
+
this.after(this.sourceElement);
|
|
69
|
+
break;
|
|
70
|
+
case "inside":
|
|
71
|
+
this.append(this.sourceElement);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
restore() {
|
|
76
|
+
var e, t;
|
|
77
|
+
this.sourceElement && ((e = this.sourceAnchor) != null && e.parentNode) && this.sourceAnchor.parentNode.insertBefore(this.sourceElement, this.sourceAnchor.nextSibling), (t = this.sourceAnchor) == null || t.remove(), this.sourceAnchor = null, this.sourceElement = null;
|
|
78
|
+
}
|
|
79
|
+
get placement() {
|
|
80
|
+
const e = this.getAttribute("placement");
|
|
81
|
+
return e === "before" || e === "after" || e === "inside" ? e : (e && this.warn(`Unsupported placement: "${e}".`), "inside");
|
|
82
|
+
}
|
|
83
|
+
warn(e) {
|
|
84
|
+
console.warn(o, { reason: e, element: this });
|
|
85
|
+
}
|
|
86
|
+
warnAboutUnsupportedClasses() {
|
|
87
|
+
const e = Array.from(this.classList).filter(
|
|
88
|
+
(t) => t !== n && !t.includes(":")
|
|
89
|
+
);
|
|
90
|
+
e.length && this.warn(`Unsupported classes: ${e.join(", ")}.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
customElements.get("tj-element-relocator") || customElements.define("tj-element-relocator", c);
|
|
94
|
+
export {
|
|
95
|
+
c as TjElementRelocatorElement
|
|
96
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trunkjs/element-relocator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "./index.js",
|
|
5
|
+
"repository": {
|
|
6
|
+
"directory": "packages/element-relocator",
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/trunkjs/trunkjs-monorepo.git"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/jsdom": "^21.1.7",
|
|
12
|
+
"jsdom": "^26.0.0",
|
|
13
|
+
"vitest": "^3.2.4"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"web-types": "./web-types.json"
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: element-relocator
|
|
3
|
+
description: Use @trunkjs/element-relocator when an existing DOM element must move to another responsive layout position and later return to its exact original position, including Web Component slot and light-DOM sibling scenarios.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TrunkJS Element Relocator
|
|
7
|
+
|
|
8
|
+
Use `<tj-element-relocator>` to move an existing element identified by a CSS selector. The relocator does not implement breakpoints itself. Prefer `@trunkjs/responsive` to control when the plain `relocate` class is present.
|
|
9
|
+
|
|
10
|
+
## Basic usage
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<tj-responsive>
|
|
14
|
+
<header>
|
|
15
|
+
<nav id="navigation">...</nav>
|
|
16
|
+
</header>
|
|
17
|
+
|
|
18
|
+
<aside>
|
|
19
|
+
<tj-element-relocator
|
|
20
|
+
source="#navigation"
|
|
21
|
+
class="md:relocate"
|
|
22
|
+
></tj-element-relocator>
|
|
23
|
+
</aside>
|
|
24
|
+
</tj-responsive>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`source` identifies the element being moved. When the relocator has the plain `relocate` class, the source is moved. When `relocate` disappears, the source is restored to its exact original DOM position.
|
|
28
|
+
|
|
29
|
+
Do not implement separate resize listeners or breakpoint handling around the relocator when `@trunkjs/responsive` can express the condition.
|
|
30
|
+
|
|
31
|
+
## Placement
|
|
32
|
+
|
|
33
|
+
Use `placement` to choose where the source is inserted relative to the relocator:
|
|
34
|
+
|
|
35
|
+
- `inside` (default): the source becomes a child of `<tj-element-relocator>`.
|
|
36
|
+
- `before`: the source becomes its previous sibling.
|
|
37
|
+
- `after`: the source becomes its next sibling.
|
|
38
|
+
|
|
39
|
+
Prefer `inside` for ordinary DOM relocation. Use `before` or `after` when the source must remain in the surrounding component's light DOM, especially for slot assignment.
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<my-layout>
|
|
43
|
+
<tj-element-relocator
|
|
44
|
+
source="#actions"
|
|
45
|
+
placement="after"
|
|
46
|
+
class="md:relocate"
|
|
47
|
+
></tj-element-relocator>
|
|
48
|
+
|
|
49
|
+
<div id="actions" slot="toolbar">...</div>
|
|
50
|
+
</my-layout>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `slot` attribute remains on the source; sibling placement keeps it a direct light-DOM child of `<my-layout>`.
|
|
54
|
+
|
|
55
|
+
## Class contract
|
|
56
|
+
|
|
57
|
+
Treat the relocator's classes as control input, not as general styling classes.
|
|
58
|
+
|
|
59
|
+
The only supported plain class is `relocate`. Responsive expressions containing `:` are allowed, for example `md:relocate`. Do not add unrelated plain classes such as `hidden`, `toolbar`, or `mobile`; they produce a developer warning.
|
|
60
|
+
|
|
61
|
+
For implementation details and invariants, see [`ARCHITECTURE.md`](../../ARCHITECTURE.md).
|
package/web-types.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
|
|
3
|
+
"name": "@trunkjs/element-relocator",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"contributions": {
|
|
6
|
+
"html": {
|
|
7
|
+
"elements": [
|
|
8
|
+
{
|
|
9
|
+
"name": "tj-element-relocator",
|
|
10
|
+
"description": "Relocates a source element when the 'relocate' class is present and restores it to its original DOM position when the class is removed.",
|
|
11
|
+
"attributes": [
|
|
12
|
+
{
|
|
13
|
+
"name": "source",
|
|
14
|
+
"description": "CSS selector for the element to relocate."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"name": "placement",
|
|
18
|
+
"description": "Placement relative to the relocator.",
|
|
19
|
+
"value": { "type": "enum", "items": ["inside", "before", "after"] }
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|