馃悕馃悕馃悕
at dev 11 kB view raw
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