馃悕馃悕馃悕
1
2$css(`
3 split- {
4 display: flex;
5 flex-direction: row;
6 height: 100%;
7 width: 100%;
8 overflow: visible;
9 padding: 0rem;
10 }
11
12 [data-theme-changed] > split- {
13 padding: 0.5rem;
14 }
15
16 split-[data-orientation=row] {
17 flex-direction: row;
18 }
19
20 split-[data-orientation=col] {
21 flex-direction: column;
22 }
23
24 split- > divider- {
25 background-color: var(--main-faded);
26 user-select: none;
27 -webkit-user-select: none;
28 overflow: visible;
29 touch-action: none;
30 -webkit-tap-highlight-color: transparent;
31 -webkit-touch-callout: none;
32 -webkit-user-drag: none;
33 }
34
35 split-[data-orientation=row] > divider- {
36 width: 1px;
37 cursor: col-resize;
38 }
39
40 split-[data-orientation=col] > divider- {
41 height: 1px;
42 cursor: row-resize;
43 }
44
45 split- > divider-::before {
46 content: "";
47 position: relative;
48 display: inline-block;
49 pointer-events: auto;
50 }
51
52 split-[data-orientation=row] > divider-::before {
53 top: 0;
54 left: calc(0px - var(--panel-margin));
55 width: calc(var(--panel-margin) * 2);
56 height: 100%;
57 }
58
59 split-[data-orientation=col] > divider-::before {
60 left: 0;
61 top: calc(0px - var(--panel-margin));
62 height: calc(var(--panel-margin) * 2);
63 width: 100%;
64 }
65
66 split-[data-orientation=row] > :first-child {
67 margin-right: var(--panel-margin);
68 width: calc(var(--current-portion) - 0.5px - var(--panel-margin));
69 }
70
71 split-[data-orientation=col] > :first-child {
72 margin-bottom: var(--panel-margin);
73 height: calc(var(--current-portion) - 0.5px - var(--panel-margin));
74 }
75
76 split-[data-orientation=row] > :not(divider-):not(:first-child):not(:last-child) {
77 margin-left: var(--panel-margin);
78 margin-right: var(--panel-margin);
79 width: calc(var(--current-portion) - 1px - 2 * var(--panel-margin));
80 }
81
82 split-[data-orientation=col] > :not(divider-):not(:first-child):not(:last-child) {
83 margin-top: var(--panel-margin);
84 margin-bottom: var(--panel-margin);
85 height: calc(var(--current-portion) - 1px - 2 * var(--panel-margin));
86 }
87
88 split-[data-orientation=row] > :last-child {
89 margin-left: var(--panel-margin);
90 width: calc(var(--current-portion) - 0.5px - var(--panel-margin));
91 }
92
93 split-[data-orientation=col] > :last-child {
94 margin-top: var(--panel-margin);
95 height: calc(var(--current-portion) - 0.5px - var(--panel-margin));
96 }
97
98 split- > portion- {
99 overflow: hidden;
100 position: relative;
101 }
102`);
103
104function focusableDescendent(element, reverse = false) {
105 const walker = document.createTreeWalker(element, NodeFilter.SHOW_ELEMENT,
106 (node) => {
107 if (node.$ && node.$.focusable) {
108 return NodeFilter.FILTER_ACCEPT;
109 }
110 return NodeFilter.FILTER_SKIP;
111 });
112 if (reverse) {
113 let lastNode = null;
114 while (walker.nextNode()) {
115 lastNode = walker.currentNode;
116 }
117 return lastNode;
118 }
119 return walker.nextNode();
120}
121
122const defaults = {
123 content: [$prepMod("layout/nothing"), $prepMod("layout/nothing")],
124 percents: "equal",
125 orientation: "row"
126};
127
128customElements.define("split-", class extends HTMLElement {});
129customElements.define("divider-", class extends HTMLElement {});
130customElements.define("portion-", class extends HTMLElement {});
131
132export async function main(target, settings) {
133 settings = { ... defaults, ... settings };
134
135 const content = settings.content;
136
137 var n = content.length;
138
139 const container = $element("split-");
140 container.dataset.orientation = settings.orientation;
141 var row = settings.orientation === "row";
142
143 const orientationToggle = [row ? "row->col" : "col->row", () => {
144 row = !row;
145 settings.orientation = row ? "row" : "col";
146 container.dataset.orientation = settings.orientation;
147 orientationToggle[0] = row ? "row->col" : "col->row";
148 }];
149
150
151 container.addEventListener("keydown", (e) => {
152 if (e.target.matches("input, textarea, [contenteditable=\"true\"]")) return;
153
154 if (e.key === (row ? "h" : "k")) {
155 const currentIndex = portions.findIndex(t => t.contains(document.activeElement));
156 if (currentIndex === 0) {
157 return;
158 }
159 const prevIndex = currentIndex - 1;
160 const prev = focusableDescendent(portions[prevIndex], true);
161 if (prev) prev.focus();
162 e.stopPropagation();
163 }
164 else if (e.key === (row ? "l" : "j")) {
165 const currentIndex = portions.findIndex(t => t.contains(document.activeElement));
166 if (currentIndex === portions.length - 1) {
167 return;
168 }
169 const nextIndex = currentIndex + 1;
170 const next = focusableDescendent(portions[nextIndex]);
171 if (next) next.focus();
172
173 e.stopPropagation();
174 }
175
176 });
177
178 const portions = [];
179 const splitters = [];
180
181 const minPercent = 2;
182
183 async function createDragHandler(splitter, i) {
184 function startDrag(e) {
185 if (e.pointerType === "mouse" && e.button !== 0) return;
186 if (e.target !== splitter) return;
187 e.preventDefault();
188 e.stopPropagation();
189 e.stopImmediatePropagation();
190
191 function resizeCallback(e) {
192 const containerRect = container.getBoundingClientRect();
193
194 let least = row ? containerRect.left : containerRect.top;
195 let extent = row ? containerRect.width : containerRect.height;
196
197 const relativePos = (row ? e.clientX : e.clientY) - least;
198 const percent = (relativePos / extent) * 100;
199
200 const priorExtent = parseFloat(portions[i].style.getPropertyValue("--current-portion"));
201 const posteriorExtent = parseFloat(portions[i + 1].style.getPropertyValue("--current-portion"));
202 const totalAdjacent = priorExtent + posteriorExtent;
203
204 let adjacentStart = 0;
205 for (let j = 0; j < i; j++) {
206 adjacentStart += parseFloat(portions[j].style.getPropertyValue("--current-portion"));
207 }
208
209 const adjacentPercent = Math.max(0, Math.min(100, percent - adjacentStart));
210 const priorRatio = Math.max(minPercent, Math.min(totalAdjacent - minPercent, adjacentPercent));
211 const posteriorRatio = totalAdjacent - priorRatio;
212
213 portions[i].style.setProperty("--current-portion", priorRatio + "%");
214 portions[i + 1].style.setProperty("--current-portion", posteriorRatio + "%");
215 }
216
217 function cleanup() {
218 document.removeEventListener("pointermove", resizeCallback);
219 document.removeEventListener("pointerup", cleanup);
220 document.removeEventListener("pointerleave", cleanup);
221 }
222
223 document.addEventListener("pointermove", resizeCallback, { passive: false });
224 document.addEventListener("pointerup", cleanup, { passive: false });
225 document.addEventListener("pointerleave", cleanup, { passive: false });
226 }
227
228 splitter.addEventListener("pointerdown", startDrag, { passive: false, capture: true });
229 }
230
231 function collapse(removedIndex, keptIndex) {
232 n = n - 1;
233
234 if (n === 1) {
235 container.parentNode.replaceChildren(...portions[keptIndex].childNodes);
236 return;
237 }
238
239 portions[removedIndex].remove();
240 const removedExtent = parseFloat(portions[removedIndex].style.getPropertyValue("--current-portion"));
241 const keptExtent = parseFloat(portions[keptIndex].style.getPropertyValue("--current-portion"));
242 portions[keptIndex].style.setProperty("--current-portion", `${removedExtent + keptExtent}%`);
243
244 for (let i = removedIndex + 1; i < n; i++) {
245 container.insertBefore(portions[i], splitters[i-1]);
246 }
247
248 portions.splice(removedIndex, 1);
249 splitters[n - 1].remove();
250 splitters.splice(n - 1, 1);
251 }
252
253 function tryCollapse(separatorIndex) {
254 const prior = portions[separatorIndex];
255 const posterior = portions[separatorIndex + 1];
256
257 const priorCollapse = ![...prior.childNodes].some(child => $actualize(child.$preventCollapse));
258 const posteriorCollapse = ![...posterior.childNodes].some(child => $actualize(child.$preventCollapse));
259
260 if (!(priorCollapse || posteriorCollapse)) return;
261
262 collapse(priorCollapse ? separatorIndex : separatorIndex + 1, priorCollapse ? separatorIndex + 1 : separatorIndex);
263 }
264
265 function collapseOptions(separatorIndex) {
266 return () => {
267 const prior = portions[separatorIndex];
268 const posterior = portions[separatorIndex + 1];
269
270 const priorCollapse = ![...prior.childNodes].some(child => $actualize(child.$preventCollapse));
271 const posteriorCollapse = ![...posterior.childNodes].some(child => $actualize(child.$preventCollapse));
272
273 if (!(priorCollapse || posteriorCollapse)) return;
274
275 if (priorCollapse && posteriorCollapse) {
276 return [
277 [`collapse ${row ? "left" : "top"}`, () => collapse(separatorIndex, separatorIndex + 1)],
278 [`collapse ${row ? "right" : "bottom"}`, () => collapse(separatorIndex + 1, separatorIndex)]
279 ];
280 }
281
282 return ["collapse", () => collapse(priorCollapse ? separatorIndex : separatorIndex + 1, priorCollapse ? separatorIndex + 1 : separatorIndex)];
283 }
284 }
285
286 for (let i = 0; i < n; i++) {
287 const portion = $element("portion-");
288 if (settings.percents === "equal") {
289 portion.style.setProperty("--current-portion", `${100/n}%`);
290 } else {
291 portion.style.setProperty("--current-portion", `${settings.percents[i]}%`);
292 }
293
294 portions.push(portion);
295
296 container.$with(portion);
297
298 if (i === n - 1) continue;
299
300 const splitter = $element("divider-");
301 //document.createElement("div");
302 //splitter.className = "splitter";
303
304 splitter.$contextMenu = {
305 items: [orientationToggle, collapseOptions(i)]
306 };
307
308 splitter.addEventListener("pointerdown", (e) => {
309 if (e.button !== 1) return;
310
311 tryCollapse(i);
312 });
313
314 splitters.push(splitter);
315 container.appendChild(splitter);
316
317 createDragHandler(splitter, i);
318 }
319
320 target.appendChild(container);
321
322 for (let i = 0; i < n; i++) {
323 if (content[i].$isInitializer) {
324 await content[i](portions[i]);
325 }
326 else {
327 portions[i].appendChild(content[i]);
328 }
329 }
330
331 container.$preventCollapse = () => {
332 return portions.some(target => [...target.childNodes].some(child => $actualize(child.$preventCollapse)));
333 };
334
335 return {
336 replace: true,
337 topmost: container
338 };
339}
340