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