Files
oikos/web/node_modules/svelte-splitpanes/dist/Splitpanes.svelte
dtoro d4d99a7473
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
feat: Phase 1 — extract the client (web SPA + desktop) to dtoro/oikos-web
Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.

Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
  (updateURL repointed to oikos-web releases), compose/, own CI (web +
  desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
  version-tagged images, prune-to-3), own webhook receiver on :9798 +
  launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
  stopped+removed, oikos-web project brought up on 8091; outer Caddy
  untouched (targets the published port) — serving + Authentik flow +
  /wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
  workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
  targets, the compose web service, oikos-web from deploy.sh's fallback
  prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
  point at the new repo; mbse + mascot design docs carry a path note.

Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.

Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
2026-08-15 22:27:52 +02:00

979 lines
39 KiB
Svelte

<script context="module" lang="ts">export const KEY = {};
// Constants for better maintainability
const DOUBLE_CLICK_TIMEOUT = 500;
const DRAG_FINISH_DELAY = 100;
const SIZE_THRESHOLD = 0.1; // Minimum size threshold for calculations
const FULL_SIZE = 100; // Full percentage size
/**
* the third argument for event bundler
* @see https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
*/
const thirdEventArg = (() => {
let result = false;
try {
const arg = Object.defineProperty({}, 'passive', {
get() {
result = { passive: true };
return true;
}
});
// @ts-expect-error no overload matches
window.addEventListener('testpassive', arg, arg);
// @ts-expect-error no overload matches
window.remove('testpassive', arg, arg);
}
catch (_e) {
/* */
}
return result;
})();
/**
* Type guard to check if an event is a TouchEvent.
*/
// function isTouchEvent(event: MouseEvent | TouchEvent): event is TouchEvent {
// return 'touches' in event;
// }
</script>
<script lang="ts">import { onMount, onDestroy, setContext, createEventDispatcher, tick, afterUpdate } from 'svelte';
import { writable } from 'svelte/store';
import GatheringRound from './internal/GatheringRound.svelte';
import { browser } from './internal/env.js';
import { getDimensionName } from './internal/utils/sizing.js';
import { elementRectWithoutBorder, getGlobalMousePosition, positionDiff, getElementRect } from './internal/utils/position.js';
import { forEachPartial, sumPartial } from './internal/utils/array.js';
import { calcComputedStyle } from './internal/utils/styling.js';
// PROPS ----------------
//@ts-expect-error undefined not assigned to string
export let id = undefined;
// horiz or verti?
export let horizontal = false;
// when true, moving a splitter can push other panes
export let pushOtherPanes = true;
// open/close on double click
export let dblClickSplitter = true;
// true if RTL
export let rtl = 'auto';
// true to display the first splitter
export let firstSplitter = false;
// css style
export let style = null;
// the splitter theme to use
export let theme = 'default-theme';
// css class
let clazz = '';
export { clazz as class };
// VARIABLES ----------------
//used to bubble events up
const dispatch = createEventDispatcher();
// the splitpane component
let container;
// true when component is ready, prevents emitting console warnings on hot reloading.
let isReady = false;
// true when pane reset is awaiting for the next tick, to avoid double call to pane reset.
let isAwaitingPaneReset = false;
// true after the initial timeout 0 waiting, prevents CSS transitions until then.
let isAfterInitialTimeoutZero = false;
// true when mouse is down
let isMouseDown = false;
// true when a splitter is being dragged
let isDragging = false;
// that's the splitter than is being dragged
let activeSplitter = -1;
// that's well the clicked splitter!
let clickedSplitter = -1;
// used to detect double clicks
let timeoutId;
// panes per insertion order (pane.index is the order index)
let panes = new Array();
// passed to the children via the context - writable to ensure proper reactivity
const isHorizontal = writable(horizontal);
const showFirstSplitter = writable(firstSplitter);
// tells the key of the very first pane, or undefined if not recieved yet
const veryFirstPaneKey = writable(undefined);
let activeSplitterElement;
let activeSplitterDrag;
let ssrPaneDefinedSizeSum = 0;
let ssrPaneUndefinedSizeCount = 0;
// Cache for drag operations to avoid repeated style calculations
let cachedContainerStyle = null;
let cachedContainerRect = null;
let cachedIsRTL = false;
// Cache for splitter size calculations during drag
// const cachedSplitterSizes: {
// activeSplitterSize: number;
// totalBefore: number;
// totalAfter: number;
// totalSplitter: number;
// } | null = null;
// Request animation frame ID for drag updates
// let rafId: number | null = null;
// let pendingDragData: { tdrag: number; containerSize: number } | null = null;
// Snapshot for verifyAndUpdatePanesOrder optimization
let lastChildrenSnapshot = [];
// REACTIVE ----------------
$: $isHorizontal = horizontal;
$: $showFirstSplitter = firstSplitter;
// used to complete rendering service side (SSR mode)
function ssrRegisterPaneSize(size) {
if (size === null) {
++ssrPaneUndefinedSizeCount;
}
else {
ssrPaneDefinedSizeSum += size;
}
}
const onPaneInit = _key => {
if ($veryFirstPaneKey === undefined) {
$veryFirstPaneKey = _key;
}
return {
undefinedPaneInitSize: browser
? 0
: (FULL_SIZE - ssrPaneDefinedSizeSum) / ssrPaneUndefinedSizeCount
};
};
setContext(KEY, {
showFirstSplitter,
veryFirstPaneKey,
isHorizontal,
ssrRegisterPaneSize: browser ? undefined : ssrRegisterPaneSize,
onPaneInit,
clientOnly: browser
? {
onPaneAdd,
onPaneRemove
}
: undefined
});
/**
* Calculates the index of a pane element within its parent container.
* Optimized to avoid array allocation by iterating through siblings directly.
*
* @param paneElement - The HTML element of the pane
* @returns The zero-based index of the pane among all pane elements
*/
function calculatePaneIndex(paneElement) {
let index = -1;
let currentNode = paneElement.parentNode?.firstChild;
while (currentNode) {
if (currentNode.nodeType === Node.ELEMENT_NODE &&
currentNode.classList.contains('splitpanes__pane')) {
index++;
}
if (currentNode === paneElement)
break;
currentNode = currentNode.nextSibling;
}
return index;
}
function onPaneAdd(pane) {
// 1. Add pane to array at the same index it was inserted in the <splitpanes> tag.
const index = calculatePaneIndex(pane.element);
if (index === 0) {
// Need to update the first pane key, because the first pane can be changed in runtime.
$veryFirstPaneKey = pane.key;
}
//inserts pane at proper array index
panes.splice(index, 0, pane);
// reindex panes
for (let i = 0; i < panes.length; i++) {
panes[i].index = i;
}
if (isReady) {
// 2. tick and resize the panes.
tickAndResetPaneSizes().then(() => {
// 3. Set the pane as ready
pane.isReady = true;
// 4. Fire `pane-add` event.
dispatch('pane-add', {
index,
panes: prepareSizeEvent()
});
});
}
const paneForward = (cb, includingFirst = true) => (value) => {
if (includingFirst || pane.index > 0) {
cb(value, pane);
}
};
return {
onSplitterDown: paneForward(onMouseDown, false),
onSplitterClick: paneForward(onSplitterClick, false),
onSplitterDblClick: paneForward(onSplitterDblClick),
onPaneClick: paneForward(onPaneClick),
reportGivenSizeChange: paneForward(reportGivenSizeChange)
};
}
async function onPaneRemove(key) {
// 1. Remove the pane from array and redo indexes.
const index = panes.findIndex(p => p.key === key);
// race condition - typically happens when the dev server restarts
if (index >= 0) {
const removed = panes.splice(index, 1)[0];
// reindex panes
for (let i = 0; i < panes.length; i++) {
panes[i].index = i;
}
if (index === 0) {
$veryFirstPaneKey = panes.length > 0 ? panes[0].key : undefined;
}
if (isReady) {
// 3. tick and resize the panes.
await tickAndResetPaneSizes();
// 4. Fire `pane-remove` event.
dispatch('pane-remove', {
removed,
panes: prepareSizeEvent()
});
}
}
}
// called by sub-panes
function onPaneClick(_event, pane) {
dispatch('pane-click', pane);
}
function reportGivenSizeChange(newGivenSize, pane) {
pane.setSz(newGivenSize);
tickAndResetPaneSizes();
}
onMount(() => {
verifyAndUpdatePanesOrder();
resetPaneSizes();
for (let i = 0; i < panes.length; i++) {
panes[i].isReady = true;
}
isReady = true;
dispatch('ready');
setTimeout(() => {
isAfterInitialTimeoutZero = true;
}, 0);
});
if (browser) {
onDestroy(() => {
if (isReady) {
// this is to solve an edge case:
// when the user starts dragging and the component is destroyed, leaving behind hanging events
unbindEvents();
}
// Prevent emitting console warnings on hot reloading.
isReady = false;
});
}
afterUpdate(() => {
verifyAndUpdatePanesOrder();
});
// Tells in the current DOM state if we are in RTL direction or not.
function isRTL(containerComputedStyle) {
if (rtl === 'auto') {
// the try catch is to support old browser, flag is preset to false
try {
return (containerComputedStyle ?? calcComputedStyle(container)).direction === 'rtl';
}
catch (_err) {
// We want application to not crush, but don't care about the message
}
}
// otherwise
return rtl === true;
}
function bindEvents() {
document.body.style.cursor = $isHorizontal ? 'col-resize' : 'row-resize';
document.addEventListener('mousemove', onMouseMove, thirdEventArg);
document.addEventListener('mouseup', onMouseUp);
if ('ontouchstart' in window) {
document.addEventListener('touchmove', onMouseMove, thirdEventArg);
document.addEventListener('touchend', onMouseUp);
}
}
function unbindEvents() {
document.body.style.cursor = '';
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
if ('ontouchstart' in window) {
document.removeEventListener('touchmove', onMouseMove);
document.removeEventListener('touchend', onMouseUp);
}
}
const isSplitterElement = (node) => node.nodeType === Node.ELEMENT_NODE &&
node.classList.contains('splitpanes__splitter');
function getOrientedDiff(drag, elementSize, isRTL) {
let tdrag = drag[horizontal ? 'top' : 'left'];
if (isRTL && !horizontal)
tdrag = elementSize - tdrag;
return tdrag;
}
const getCurrentDimensionName = () => getDimensionName(horizontal);
function onMouseDown(event, splitterPane) {
isMouseDown = true;
activeSplitter = splitterPane.index;
splitterPane.setSplitterActive(true);
const paneElement = splitterPane.element;
let activeSplitterNode = paneElement;
while (activeSplitterNode) {
activeSplitterNode = activeSplitterNode.previousSibling;
if (activeSplitterNode && isSplitterElement(activeSplitterNode)) {
break;
}
}
if (activeSplitterNode === null) {
console.error("Splitpane Error: Active splitter wasn't found!");
return; // Don't bind move event on error
}
activeSplitterElement = activeSplitterNode;
const globalMousePosition = getGlobalMousePosition(event);
const splitterRect = getElementRect(activeSplitterElement);
// Cache container style and dimensions for the drag operation
cachedContainerStyle = calcComputedStyle(container);
cachedContainerRect = elementRectWithoutBorder(container, cachedContainerStyle);
cachedIsRTL = isRTL(cachedContainerStyle);
activeSplitterDrag = getOrientedDiff(positionDiff(globalMousePosition, splitterRect), splitterRect[getCurrentDimensionName()], cachedIsRTL);
bindEvents();
}
function onMouseMove(event) {
if (isMouseDown && cachedContainerRect) {
isDragging = true;
const globalMousePosition = getGlobalMousePosition(event);
// Use cached values to avoid repeated expensive DOM calculations
const containerSizeWithoutBorder = cachedContainerRect[getCurrentDimensionName()];
const currentMouseDrag = positionDiff(globalMousePosition, cachedContainerRect);
const tdrag = getOrientedDiff(currentMouseDrag, containerSizeWithoutBorder, cachedIsRTL);
calculatePanesSize(tdrag, containerSizeWithoutBorder);
dispatch('resize', prepareSizeEvent());
}
}
function onMouseUp() {
if (isDragging) {
dispatch('resized', prepareSizeEvent());
}
isMouseDown = false;
const pane = panes[activeSplitter];
pane.setSplitterActive(false);
// Clear caches after drag operation
cachedContainerStyle = null;
cachedContainerRect = null;
// Keep dragging flag until click event is finished (click happens immediately after mouseup)
// in order to prevent emitting `splitter-click` event if splitter was dragged.
setTimeout(() => {
isDragging = false;
unbindEvents();
}, DRAG_FINISH_DELAY);
}
// If touch device, detect double tap manually (2 taps separated by less than 500ms).
function onSplitterClick(event, splitterPane) {
if ('ontouchstart' in window) {
const splitterIndex = splitterPane.index;
// Detect splitter double taps if the option is on.
if (dblClickSplitter) {
if (clickedSplitter === splitterIndex) {
if (timeoutId)
clearTimeout(timeoutId);
timeoutId = null;
onSplitterDblClick(event, splitterPane);
clickedSplitter = -1; // Reset for the next tap check.
}
else {
clickedSplitter = splitterIndex;
timeoutId = setTimeout(() => {
clickedSplitter = -1;
}, DOUBLE_CLICK_TIMEOUT);
}
}
}
if (!isDragging)
dispatch('splitter-click', splitterPane);
}
// On splitter dbl click or dbl tap maximize this pane.
function onSplitterDblClick(_event, splitterPane) {
if (dblClickSplitter) {
const splitterIndex = splitterPane.index;
let totalMinSizes = 0;
for (let i = 0; i < panes.length; i++) {
const pane = panes[i];
if (i !== splitterIndex) {
totalMinSizes += pane.min();
}
}
const maxExtendedSize = Math.min(Math.max(0, FULL_SIZE - totalMinSizes), splitterPane.max());
const totalMaxExtendedPlusMinSizes = totalMinSizes + maxExtendedSize;
if (totalMaxExtendedPlusMinSizes >= FULL_SIZE) {
// put everything to the minimum, and in the splitterPane put the rest of the size
for (let i = 0; i < panes.length; i++) {
const pane = panes[i];
if (pane !== splitterPane) {
pane.setSz(pane.min());
}
else {
pane.setSz(FULL_SIZE - totalMinSizes);
}
}
}
else {
// notice that in this case, we can conclude that `panes.length >= 2`
// put splitterPane to the maximum (the actual one), and the normal panes to the minimum,
// and give the spare to left pane (or to the right pane, if the splitterPane is the first pane)
// if this spare size is beyond the pane maximum, need to pass it along to the other panes
let leftSpare = FULL_SIZE - totalMaxExtendedPlusMinSizes;
splitterPane.setSz(maxExtendedSize);
const giveBest = (pane) => {
const min = pane.min();
const max = pane.max();
const szExtra = Math.min(Math.max(0, leftSpare), max - min);
pane.setSz(min + szExtra);
leftSpare -= szExtra;
};
// go backward and give the most size as we can
for (let i = splitterIndex - 1; i >= 0; i--)
giveBest(panes[i]);
// go forward and give the most size as we can
for (let i = splitterIndex + 1; i < panes.length; i++)
giveBest(panes[i]);
// at the end of the process, we must have that `leftSpare` is 0
if (leftSpare !== 0) {
console.warn('Splitpanes: there is a left spare size after computation of splitter double click, which means there are issues on the size constains of the panes.');
}
}
dispatch('pane-maximize', splitterPane);
dispatch('resized', prepareSizeEvent());
}
// onMouseUp might not be called on the second click, so update the mouse state.
// TODO: Should also check and unbind events, but better IMO to not bind&unbind on every click, so ignored for now.
isMouseDown = false;
}
const prepareSizeEvent = () => panes.map(pane => ({
min: pane.min(),
max: pane.max(),
size: pane.sz(),
snap: pane.snap()
}));
/**
* Returns the drag percentage of the splitter relative to the 2 parts it's inbetween, meaning the ratio between
* the size that all the panes before the splitter consumes (ignoring other splitters size) and the total size of the container.
*/
function getCurrentDragPercentage(tdrag, containerSizeWithoutBorder) {
// Here we want the splitter size **including the borders**.
// We need to use `Element.getBoundingClientRect()` and not `Element.clientWidth` and `Element.clientHeight`,
// bacause the latter round the number of pixels to integer, and additionally, they don't include the borders.
const splitterSize = (node) => getElementRect(node)[getCurrentDimensionName()];
const activeSplitterSize = splitterSize(activeSplitterElement);
let splittersTotalSizeBefore = 0;
let currentBeforeNode = activeSplitterElement.previousSibling;
while (currentBeforeNode) {
if (isSplitterElement(currentBeforeNode)) {
splittersTotalSizeBefore += splitterSize(currentBeforeNode);
}
currentBeforeNode = currentBeforeNode.previousSibling;
}
let splittersTotalSizeAfter = 0;
let currentAfterNode = activeSplitterElement.nextSibling;
while (currentAfterNode) {
if (isSplitterElement(currentAfterNode)) {
splittersTotalSizeAfter += splitterSize(currentAfterNode);
}
currentAfterNode = currentAfterNode.nextSibling;
}
const totalSplitterBefore = splittersTotalSizeBefore + activeSplitterDrag;
const totalSplitter = splittersTotalSizeBefore + activeSplitterSize + splittersTotalSizeAfter;
// An explanation to the mathematical computation:
//
// Let's start with the case of only two panes. If we mark the first pane size in prec
// (thinking about it as a number between 0 to 1) as `x`, we'll get that the size of the left pane in pixels will be:
// `x*containerSizeWithoutBorder - x*totalSplitter = x*(containerSizeWithoutBorder - totalSplitter)`
// Since we want that the total size in pixels before the user mouse pointer will be `tdrag`, and we need to add the
// size of the splitter itself that is before the mouse pointer, we get the equation:
// `x*(containerSizeWithoutBorder - totalSplitter) + activeSplitterDrag = tdrag`
//
// Now in the general case when we have many panes before the splitter, mark their precentages
// (again, thinking about it as a number between 0 to 1) by x1,x2,...,xn we'll get the equation:
// `(x1 + ... + xn)*(containerSizeWithoutBorder - totalSplitter) + totalSplitterBefore = tdrag`
// And solving it yeild the answer:
// `x1 + ... + xn = (tdrag - totalSplitterBefore) / (containerSizeWithoutBorder - totalSplitter)`
return (((tdrag - totalSplitterBefore) / (containerSizeWithoutBorder - totalSplitter)) * FULL_SIZE);
}
/**
* Called when slitters are moving to adjust pane sizes
*/
function calculatePanesSize(tdrag, containerSizeWithoutBorder) {
let paneBeforeIndex = activeSplitter - 1;
let paneBefore = panes[paneBeforeIndex];
let paneAfterIndex = activeSplitter;
let paneAfter = panes[paneAfterIndex];
let sums = {
prevPanesSize: sumPrevPanesSize(paneBeforeIndex),
nextPanesSize: sumNextPanesSize(paneAfterIndex),
prevReachedMinPanes: 0,
nextReachedMinPanes: 0
};
// If not pushing other panes, panes to resize are right before and right after splitter.
const minDrag = 0 + (pushOtherPanes ? 0 : sums.prevPanesSize);
const maxDrag = 100 - (pushOtherPanes ? 0 : sums.nextPanesSize);
// Calculate drag percentage
const mouseDragPercentage = Math.max(Math.min(getCurrentDragPercentage(tdrag, containerSizeWithoutBorder), maxDrag), minDrag);
// Handle snap
const paneBeforeSnap = sums.prevPanesSize + paneBefore.min() + paneBefore.snap();
const paneAfterSnap = 100 - (sums.nextPanesSize + paneAfter.min() + paneAfter.snap());
let dragPercentage = mouseDragPercentage;
let snapped = false;
if (mouseDragPercentage <= paneBeforeSnap) {
if (mouseDragPercentage > sums.prevPanesSize + paneBefore.min()) {
dragPercentage = Math.max(paneBefore.min() + sums.prevPanesSize, 100 - (paneAfter.max() + sums.nextPanesSize));
snapped = true;
}
}
else if (mouseDragPercentage >= paneAfterSnap) {
if (mouseDragPercentage < 100 - sums.nextPanesSize - paneAfter.min()) {
dragPercentage = Math.min(100 - (paneAfter.min() + sums.nextPanesSize), paneBefore.max() + sums.prevPanesSize);
snapped = true;
}
}
const paneBeforeMaxReached = paneBefore.max() < 100 && dragPercentage >= paneBefore.max() + sums.prevPanesSize;
const paneAfterMaxReached = paneAfter.max() < 100 && dragPercentage <= 100 - (paneAfter.max() + sums.nextPanesSize);
// Prevent dragging beyond pane max.
if (paneBeforeMaxReached || paneAfterMaxReached) {
if (paneBeforeMaxReached) {
paneBefore.setSz(paneBefore.max());
paneAfter.setSz(Math.max(100 - paneBefore.max() - sums.prevPanesSize - sums.nextPanesSize, 0));
}
else {
paneBefore.setSz(Math.max(100 - paneAfter.max() - sums.prevPanesSize - sums.nextPanesSize, 0));
paneAfter.setSz(paneAfter.max());
}
}
else {
// When pushOtherPanes = true, find the closest expanded pane on each side of the splitter.
// TODO: Bug: This should work also when removing `!snapped` condition, but it's not!
// To reproduce, reload the example page and see the example "Min & max with snap".
// It gets wrongly pushed when try to snap on the initial dragging of the first splitter to the right.
if (pushOtherPanes && !snapped) {
const vars = doPushOtherPanes(sums, dragPercentage);
if (!vars) {
// setAllPaneDimensions();
return; // Prevent other calculation.
}
({ sums, paneBeforeIndex, paneAfterIndex } = vars);
paneBefore = panes[paneBeforeIndex];
paneAfter = panes[paneAfterIndex];
}
if (typeof paneBeforeIndex === 'number') {
paneBefore.setSz(Math.min(Math.max(dragPercentage - sums.prevPanesSize - sums.prevReachedMinPanes, paneBefore.min()), paneBefore.max()));
}
if (typeof paneAfterIndex === 'number') {
paneAfter.setSz(Math.min(Math.max(100 - dragPercentage - sums.nextPanesSize - sums.nextReachedMinPanes, paneAfter.min()), paneAfter.max()));
}
}
}
function doPushOtherPanes(sums, dragPercentage) {
const splitterIndex = activeSplitter - 1;
let paneBeforeIndex = splitterIndex;
let paneAfterIndex = splitterIndex + 1;
// Pushing Down.
// Going smaller than the current pane min size: take the previous expanded pane.
if (dragPercentage < sums.prevPanesSize + panes[paneBeforeIndex].min()) {
paneBeforeIndex = findPrevExpandedPane(splitterIndex)?.index;
sums.prevReachedMinPanes = 0;
if (paneBeforeIndex === undefined) {
// If nothing else to push down, cancel dragging.
sums.prevPanesSize = 0;
panes[0].setSz(panes[0].min());
forEachPartial(panes, 1, splitterIndex + 1, (pane) => {
pane.setSz(pane.min());
sums.prevReachedMinPanes += pane.min();
});
panes[paneAfterIndex].setSz(100 - sums.prevReachedMinPanes - panes[0].min() - sums.prevPanesSize - sums.nextPanesSize);
return null;
}
else {
// If pushing a n-2 or less pane, from splitter, then make sure all in between is at min size.
if (paneBeforeIndex < splitterIndex) {
forEachPartial(panes, paneBeforeIndex + 1, splitterIndex + 1, (pane) => {
pane.setSz(pane.min());
sums.prevReachedMinPanes += pane.min();
});
}
sums.prevPanesSize = sumPrevPanesSize(paneBeforeIndex);
}
}
// Pushing Up.
// Pushing up beyond min size is reached: take the next expanded pane.
if (dragPercentage > 100 - sums.nextPanesSize - panes[paneAfterIndex].min()) {
paneAfterIndex = findNextExpandedPane(splitterIndex)?.index;
if (paneBeforeIndex === undefined)
console.log('Yep undefined paneAfterIndex');
sums.nextReachedMinPanes = 0;
if (paneAfterIndex === undefined) {
// If nothing else to push up, cancel dragging.
sums.nextPanesSize = 0;
const panesCount = panes.length;
panes[panesCount - 1].setSz(panes[panesCount - 1].min());
forEachPartial(panes, splitterIndex + 1, panesCount - 1, pane => {
pane.setSz(pane.min());
sums.nextReachedMinPanes += pane.min();
});
panes[paneBeforeIndex].setSz(100 -
sums.prevPanesSize -
sums.nextReachedMinPanes -
panes[panesCount - 1].min() -
sums.nextPanesSize);
return null;
}
else {
// If pushing a n+2 or more pane, from splitter, then make sure all in between is at min size.
if (paneAfterIndex > splitterIndex + 1) {
forEachPartial(panes, splitterIndex + 1, paneAfterIndex, (pane) => {
pane.setSz(pane.min());
sums.nextReachedMinPanes += pane.min();
});
}
sums.nextPanesSize = sumNextPanesSize(paneAfterIndex);
}
}
return { sums, paneBeforeIndex, paneAfterIndex };
}
const getSizeOfPane = (pane) => pane.sz();
const sumPrevPanesSize = (splitterIndex) => sumPartial(panes, 0, splitterIndex, getSizeOfPane);
const sumNextPanesSize = (splitterIndex) => sumPartial(panes, splitterIndex + 1, panes.length, getSizeOfPane);
// Return the previous pane from siblings which has a size (width for vert or height for horz) of more than 0.
const findPrevExpandedPane = (splitterIndex) => [...panes].reverse().find(p => p.index < splitterIndex && p.sz() > p.min());
// Return the next pane from siblings which has a size (width for vert or height for horz) of more than 0.
const findNextExpandedPane = (splitterIndex) => panes.find(p => p.index > splitterIndex + 1 && p.sz() > p.min());
async function tickAndResetPaneSizes() {
isAwaitingPaneReset = true;
await tick();
if (isAwaitingPaneReset) {
resetPaneSizes();
isAwaitingPaneReset = false;
}
}
/**
*
* @param addedPane
* @param removedPane
*/
function resetPaneSizes() {
equalize();
if (isReady)
dispatch('resized', prepareSizeEvent());
}
function equalize() {
// Escape the function on the edge case that there is not even a single pane
if (panes.length === 0) {
return;
}
// otherwise
const panesCount = panes.length;
let leftToAllocate = 100;
let definedSizesCount = 0;
let undefinedSizesNotReadyCount = 0;
let undefinedSizesSum = 0;
const ungrowable = [];
const unshrinkable = [];
for (let i = 0; i < panesCount; i++) {
const pane = panes[i];
const sz = pane.sz();
if (pane.givenSize === null) {
if (pane.isReady) {
undefinedSizesSum += sz;
if (sz >= pane.max())
ungrowable.push(pane);
if (sz <= pane.min())
unshrinkable.push(pane);
}
else {
undefinedSizesNotReadyCount += 1;
}
}
else {
// if the size is defined, we don't modify its size at all
leftToAllocate -= sz;
definedSizesCount++;
ungrowable.push(pane);
unshrinkable.push(pane);
}
}
const undefinedSizesCount = panesCount - definedSizesCount;
const undefinedSizesReadyCount = undefinedSizesCount - undefinedSizesNotReadyCount;
// the proportion of the newly added panes
let undefinedSizesNotReadySz;
let undefinedScaleFactor;
if (undefinedSizesReadyCount > 0) {
// if has undefined sizes panes that are ready:
undefinedSizesNotReadySz = undefinedSizesSum / undefinedSizesReadyCount;
if (undefinedSizesNotReadySz > SIZE_THRESHOLD && leftToAllocate > SIZE_THRESHOLD) {
undefinedSizesSum += undefinedSizesNotReadyCount * undefinedSizesNotReadySz;
undefinedScaleFactor = leftToAllocate / undefinedSizesSum;
}
else {
// when the size of the ready undefined panes shares are negligible, need to set the not ready
// undefined one to size 0, for being "proportional" to negligible sizes
undefinedSizesNotReadySz = 0;
undefinedScaleFactor = 1;
}
}
else {
// otherwise, divide the space of the undefined sizes panes equally:
undefinedSizesNotReadySz = leftToAllocate / undefinedSizesCount;
undefinedScaleFactor = 1;
}
// whenever `leftToAllocate` or `undefinedSizesSum` aren't negligible, need to adjact the sizes
if (leftToAllocate + undefinedSizesSum > SIZE_THRESHOLD) {
leftToAllocate = 100; // reset the space calculation
for (let i = 0; i < panesCount; i++) {
const pane = panes[i];
if (!(typeof pane.givenSize === 'number')) {
// add the proportion of the newly added pane if has undefined size
const currentSz = pane.isReady ? pane.sz() : undefinedSizesNotReadySz;
const sz = Math.max(Math.min(currentSz * undefinedScaleFactor, pane.max()), pane.min());
pane.setSz(sz);
}
leftToAllocate -= pane.sz();
}
// since we multiply by scaling, there might be left space that is needed to be saturated
if (Math.abs(leftToAllocate) > SIZE_THRESHOLD) {
leftToAllocate = readjustSizes(leftToAllocate, ungrowable, unshrinkable);
}
}
if (!isFinite(leftToAllocate)) {
console.warn('Splitpanes: Internal error, sizes might be NaN as a result.');
}
else if (Math.abs(leftToAllocate) > SIZE_THRESHOLD) {
console.warn('Splitpanes: Could not resize panes correctly due to their constraints.');
}
}
// Second loop to adjust sizes now that we know more about the panes constraints.
function readjustSizes(leftToAllocate, ungrowable, unshrinkable) {
const panesCount = panes.length;
const panesSizableCount = panesCount - (leftToAllocate > 0 ? ungrowable.length : unshrinkable.length);
if (panesSizableCount <= 0) {
return leftToAllocate;
}
const equalSpaceToAllocate = leftToAllocate / panesSizableCount;
if (panes.length === 1) {
panes[0].setSz(100);
leftToAllocate = 0;
}
else
for (let i = 0; i < panes.length; i++) {
const pane = panes[i];
const sz = pane.sz();
if (leftToAllocate > 0 && !ungrowable.includes(pane)) {
// Need to diff the size before and after to get the exact allocated space.
const newPaneSize = Math.max(Math.min(sz + equalSpaceToAllocate, pane.max()), pane.min());
const allocated = newPaneSize - sz;
leftToAllocate -= allocated;
pane.setSz(newPaneSize);
}
else if (!unshrinkable.includes(pane)) {
// Need to diff the size before and after to get the exact allocated space.
const newPaneSize = Math.max(Math.min(sz + equalSpaceToAllocate, pane.max()), pane.min());
const allocated = newPaneSize - sz;
leftToAllocate -= allocated;
pane.setSz(newPaneSize);
}
}
return leftToAllocate;
}
/**
* Checks that <Splitpanes> is composed of <Pane>, and verify that the panes are still in the right order,
* and if not update the internal order.
*
* Optimized with early exit guards to avoid unnecessary DOM iterations.
*/
function verifyAndUpdatePanesOrder() {
if (!container) {
return;
}
const { children } = container;
// Quick check: if length hasn't changed and first/last elements are same, skip
if (children.length === lastChildrenSnapshot.length &&
children.length > 0 &&
children[0] === lastChildrenSnapshot[0] &&
children[children.length - 1] === lastChildrenSnapshot[children.length - 1]) {
return; // No changes detected
}
let currentPaneIndex = 0;
let needReorder = false;
for (let i = 0; i < children.length; i++) {
const child = children.item(i);
const isPane = child.classList.contains('splitpanes__pane');
const isSplitter = child.classList.contains('splitpanes__splitter');
// Node is not a Pane or a splitter: remove it.
if (!isPane && !isSplitter) {
child.parentNode?.removeChild(child); // el.remove() doesn't work on IE11.
console.warn('Splitpanes: Only <Pane> elements are allowed at the root of <Splitpanes>. One of your DOM nodes was removed.');
lastChildrenSnapshot = Array.from(children);
return;
}
else if (isPane) {
if (!needReorder && panes[currentPaneIndex]?.element !== child) {
needReorder = true;
}
currentPaneIndex++;
}
}
if (needReorder) {
const newPanes = [];
for (let i = 0; i < children.length; i++) {
const child = children.item(i);
const isPane = child?.classList.contains('splitpanes__pane');
if (isPane) {
const pane = panes.find(pane => pane.element === child);
if (pane) {
pane.index = newPanes.length;
newPanes.push(pane);
}
else {
console.warn("Splitpanes: Internal error - found a <Pane> elements which isn't tracked.");
}
}
}
panes = newPanes;
$veryFirstPaneKey = panes.length > 0 ? panes[0].key : undefined;
}
// Update snapshot after verification
lastChildrenSnapshot = Array.from(children);
}
</script>
<div
{id}
bind:this={container}
class={`splitpanes ${theme || ''} ${clazz || ''}`}
class:splitpanes--horizontal={horizontal}
class:splitpanes--vertical={!horizontal}
class:splitpanes--dragging={isMouseDown || isDragging}
class:splitpanes--freeze={!isAfterInitialTimeoutZero}
{style}>
{#if !browser}
<GatheringRound><slot /></GatheringRound>
{/if}
<slot />
</div>
<style global>:global(.splitpanes.default-theme) :global(.splitpanes__pane) {
background-color: #f2f2f2;
}
:global(.splitpanes.default-theme) :global(.splitpanes__splitter) {
background-color: #fff;
box-sizing: border-box;
position: relative;
flex-shrink: 0;
}
:global(.splitpanes.default-theme) :global(.splitpanes__splitter:before), :global(.splitpanes.default-theme) :global(.splitpanes__splitter:after) {
content: "";
position: absolute;
top: 50%;
left: 50%;
background-color: rgba(0, 0, 0, 0.15);
transition: background-color 0.3s;
}
:global(.splitpanes.default-theme) :global(.splitpanes__splitter:hover:before), :global(.splitpanes.default-theme) :global(.splitpanes__splitter:hover:after) {
background-color: rgba(0, 0, 0, 0.25);
}
:global(.splitpanes.default-theme) :global(.splitpanes__splitter:first-child) {
cursor: auto;
}
:global(.default-theme.splitpanes) :global(.splitpanes) :global(.splitpanes__splitter) {
z-index: 1;
}
:global(.default-theme.splitpanes--vertical) > :global(.splitpanes__splitter),
:global(.default-theme) :global(.splitpanes--vertical) > :global(.splitpanes__splitter) {
width: 7px;
border-left: 1px solid #eee;
cursor: col-resize;
}
:global(.default-theme.splitpanes--vertical) > :global(.splitpanes__splitter:before), :global(.default-theme.splitpanes--vertical) > :global(.splitpanes__splitter:after), :global(.default-theme) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:before), :global(.default-theme) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:after) {
transform: translateY(-50%);
width: 1px;
height: 30px;
}
:global(.default-theme.splitpanes--vertical) > :global(.splitpanes__splitter:before),
:global(.default-theme) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:before) {
margin-left: -2px;
}
:global(.default-theme.splitpanes--vertical) > :global(.splitpanes__splitter:after),
:global(.default-theme) :global(.splitpanes--vertical) > :global(.splitpanes__splitter:after) {
margin-left: 1px;
}
:global(.default-theme.splitpanes--horizontal) > :global(.splitpanes__splitter),
:global(.default-theme) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter) {
height: 7px;
border-top: 1px solid #eee;
cursor: row-resize;
}
:global(.default-theme.splitpanes--horizontal) > :global(.splitpanes__splitter:before), :global(.default-theme.splitpanes--horizontal) > :global(.splitpanes__splitter:after), :global(.default-theme) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:before), :global(.default-theme) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:after) {
transform: translateX(-50%);
width: 30px;
height: 1px;
}
:global(.default-theme.splitpanes--horizontal) > :global(.splitpanes__splitter:before),
:global(.default-theme) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:before) {
margin-top: -2px;
}
:global(.default-theme.splitpanes--horizontal) > :global(.splitpanes__splitter:after),
:global(.default-theme) :global(.splitpanes--horizontal) > :global(.splitpanes__splitter:after) {
margin-top: 1px;
}
:global(div.splitpanes--horizontal.splitpanes--dragging) {
cursor: row-resize;
}
:global(div.splitpanes--vertical.splitpanes--dragging) {
cursor: col-resize;
}
:global(.splitpanes) {
display: flex;
width: 100%;
height: 100%;
}
:global(.splitpanes--vertical) {
flex-direction: row;
}
:global(.splitpanes--horizontal) {
flex-direction: column;
}
:global(.splitpanes--dragging) :global(*) {
user-select: none;
}
:global(.splitpanes__pane) {
width: 100%;
height: 100%;
overflow: hidden;
}
:global(.splitpanes--vertical) :global(.splitpanes__pane) {
transition: width 0.2s ease-out;
}
:global(.splitpanes--horizontal) :global(.splitpanes__pane) {
transition: height 0.2s ease-out;
}
:global(.splitpanes__pane) {
/** Add also a direct child selector, for dealing with specifity of nested splitpanes transition.
This issue was happening in the examples on nested splitpanes, vertical inside horizontal.
I think it's better to keep also the previous CSS selector for (potential) old browser compatibility.
*/
}
:global(.splitpanes--vertical) > :global(.splitpanes__pane) {
transition: width 0.2s ease-out;
}
:global(.splitpanes--horizontal) > :global(.splitpanes__pane) {
transition: height 0.2s ease-out;
}
:global(.splitpanes--dragging) :global(.splitpanes__pane) {
transition: none;
pointer-events: none;
}
:global(.splitpanes--freeze) :global(.splitpanes__pane) {
transition: none;
}
:global(.splitpanes__splitter) {
touch-action: none;
}
:global(.splitpanes--vertical) > :global(.splitpanes__splitter) {
min-width: 1px;
}
:global(.splitpanes--horizontal) > :global(.splitpanes__splitter) {
min-height: 1px;
}</style>