馃悕馃悕馃悕
at main 340 lines 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 [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