Skip to content

Commit

Permalink
✨ feat: start working on #11
Browse files Browse the repository at this point in the history
  • Loading branch information
Tal500 committed Aug 2, 2022
1 parent 8e14778 commit 9b56746
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 24 deletions.
46 changes: 41 additions & 5 deletions src/lib/Pane.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script lang="ts">
import { getContext, onMount, onDestroy } from 'svelte';
import { writable } from 'svelte/store';
import type { Action } from 'svelte/action';
import { KEY } from './Splitpanes.svelte';
import type { IPane, SplitContext } from '.';
const { onPaneInit, onPaneAdd, onPaneRemove, onPaneClick, isHorizontal, showFirstSplitter, veryFirstPaneKey } =
const { onPaneInit, onPaneAdd, onPaneRemove, onPaneClick, isHorizontal, showFirstSplitter } =
getContext<SplitContext>(KEY);
// PROPS
Expand All @@ -22,6 +23,11 @@
const key = {};
let element: HTMLElement;
let sz: number = size == null ? 0 : size;
const sizeStore = writable({
size,
minSize,
maxSize
});
const isBrowser = typeof window !== 'undefined';
Expand All @@ -31,6 +37,14 @@
sz = size;
}
$: {
sizeStore.update((details) => {
details.minSize = minSize;
details.maxSize = maxSize;
return details;
});
}
$: dimension = $isHorizontal ? 'height' : 'width';
$: style =
Expand All @@ -42,7 +56,8 @@
.filter((value) => value !== undefined)
.join(' ') || undefined;
const { onSplitterDown, onSplitterClick, onSplitterDblClick } = onPaneInit(key);
const result = onPaneInit(key, sizeStore);
const { onSplitterDown, onSplitterClick, onSplitterDblClick, previousPaneSizeStore } = result;
function handleMouseClick(event: MouseEvent) {
onPaneClick(event, key);
Expand Down Expand Up @@ -80,6 +95,10 @@
if (size != null) {
size = sz;
}
sizeStore.update((details) => {
details.size = v;
return details;
});
},
min: () => minSize,
max: () => maxSize,
Expand All @@ -94,12 +113,29 @@
</script>

<!-- Splitter -->
<!-- TODO: Support aria role="separator" and make this a focusable separtor. Sources:
<!-- TODO: Make this a focusable separtor, and check if it is non-focusable (when minSize=maxSize).
Need also:
* Let the user control about aria values (such as aria-valuetext), and if it is focusable.default.render
* Let the user decide which pane he wish to leave without any seperator description, and describe the rest of them.
* Verify it's working in SSR.
* Keybinding
Sources:
* https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/roles/separator_role
* https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/
-->
{#if $veryFirstPaneKey !== key || $showFirstSplitter}
<div use:splitterAction class="splitpanes__splitter" />
{#if $previousPaneSizeStore || $showFirstSplitter}
<div
use:splitterAction
class="splitpanes__splitter"
role={$previousPaneSizeStore ? 'separator' : undefined}
aria-valuenow={$previousPaneSizeStore?.size ?? undefined}
aria-valuemin={$previousPaneSizeStore && $previousPaneSizeStore.minSize > 0
? $previousPaneSizeStore.minSize
: undefined}
aria-valuemax={$previousPaneSizeStore && $previousPaneSizeStore.maxSize < 100
? $previousPaneSizeStore.maxSize
: undefined}
/>
{/if}

<!-- Pane -->
Expand Down
57 changes: 41 additions & 16 deletions src/lib/Splitpanes.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

<script lang="ts">
import { onMount, onDestroy, setContext, createEventDispatcher, tick } from 'svelte';
import { writable } from 'svelte/store';
import type { IPane, IPaneSizingEvent, SplitContext, PaneInitFunction } from '.';
import { writable, type Readable } from 'svelte/store';
import type { SizeDetails, IPane, IPaneSizingEvent, SplitContext, PaneInitFunction } from '.';
import { switchableStore } from './internal/store';
// TYPE DECLARATIONS ----------------
Expand Down Expand Up @@ -99,7 +100,11 @@
let isHorizontal = writable<boolean>(horizontal);
const showFirstSplitter = writable<boolean>(firstSplitter);
// tells the key of the very first pane, or undefined if not recieved yet
const veryFirstPaneKey = writable<any>(undefined);
let lastPreviousPanesSizeStoreOnInit: Readable<SizeDetails> | undefined = undefined;
const previousPanesSizeStoresData = new Map<
any,
{ receivedStore: Readable<SizeDetails>; update: (store: Readable<SizeDetails> | undefined) => void }
>();
let activeSplitterElement: HTMLElement | null = null;
let activeSplitterDrag: number | null = null;
let startingTDrag: number | null = null;
Expand All @@ -115,10 +120,11 @@
});
}
const onPaneInit: PaneInitFunction = (key: any) => {
if ($veryFirstPaneKey === undefined) {
$veryFirstPaneKey = key;
}
const onPaneInit: PaneInitFunction = (key: any, sizeStore: Readable<SizeDetails>) => {
const { store, update } = switchableStore(lastPreviousPanesSizeStoreOnInit, undefined);
lastPreviousPanesSizeStoreOnInit = sizeStore;
previousPanesSizeStoresData.set(key, { receivedStore: sizeStore, update });
return {
onSplitterDown: (e) => {
Expand All @@ -137,13 +143,13 @@
if (dblClickSplitter) {
onSplitterDblClick(e, indexOfPane(key));
}
}
},
previousPaneSizeStore: store
};
};
setContext<SplitContext>(KEY, {
showFirstSplitter,
veryFirstPaneKey,
isHorizontal,
onPaneInit,
onPaneAdd,
Expand All @@ -159,11 +165,6 @@
return el === 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);
Expand All @@ -172,6 +173,20 @@
panes[i].index = i;
}
// Update previous size stores both both this pane and for the next one
const currentData = previousPanesSizeStoresData.get(pane.key);
const previousSizeStoreNow =
index === 0 ? undefined : previousPanesSizeStoresData.get(panes[index - 1].key).receivedStore;
currentData.update(previousSizeStoreNow);
if (index < panes.length - 1) {
// If isn't not the last one
const currentSizeStore = currentData.receivedStore;
const nextKey = panes[index + 1].key;
previousPanesSizeStoresData.get(nextKey).update(currentSizeStore);
}
if (isReady) {
await tick();
Expand All @@ -189,6 +204,7 @@
async function onPaneRemove(key: any) {
// 1. Remove the pane from array and redo indexes.
const index = panes.findIndex((p) => p.key === key);
const isNotLast = index < panes.length - 1;
// race condition - typically happens when the dev server restarts
if (index >= 0) {
Expand All @@ -199,8 +215,17 @@
panes[i].index = i;
}
if (index === 0) {
$veryFirstPaneKey = panes.length > 0 ? panes[0].key : undefined;
// Update previous size stores both both this pane and for the next one
previousPanesSizeStoresData.get(key).update(undefined);
if (isNotLast) {
// If not the last one
const prevIndex = index - 1;
const prevSizeStoreNow =
prevIndex === 0 ? undefined : previousPanesSizeStoresData.get(panes[prevIndex].key).receivedStore;
const nextKey = panes[index].key;
previousPanesSizeStoresData.get(nextKey).update(prevSizeStoreNow);
}
if (isReady) {
Expand Down
24 changes: 21 additions & 3 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,34 @@ import type { Readable } from 'svelte/store';
export { default as Splitpanes } from './Splitpanes.svelte';
export { default as Pane } from './Pane.svelte';

export type PaneInitFunction = (key: any) => {
export interface SizeDetails {
/** The current size of a pane.
*
* If number, it's the real pane size value.
* If null, it means that the pane size is unknown yet.
*/
size: number | null;
minSize: number;
maxSize: number;
}

export type PaneInitFunction = (
key: any,
sizeStore: Readable<SizeDetails>
) => {
onSplitterDown: (_event: TouchEvent | MouseEvent) => void;
onSplitterClick: (event: MouseEvent) => void;
onSplitterDblClick: (_event: MouseEvent) => void;
/** A store that tells what is the previous pane size.
*
* If undefined, it means that you are the first pane.
* Otherwise, it contains the details about the previous panel size details.
*/
previousPaneSizeStore: Readable<SizeDetails | undefined>;
};

// methods passed from splitpane to children panes
export interface SplitContext {
/** Tells the key of the very first pane, or undefined if not recieved yet. */
veryFirstPaneKey: Readable<any>;
isHorizontal: Readable<boolean>;
showFirstSplitter: Readable<boolean>;
onPaneInit: PaneInitFunction;
Expand Down
51 changes: 51 additions & 0 deletions src/lib/internal/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { readable, type Readable } from 'svelte/store';

export interface SwitchableStoreResult<T> {
store: Readable<T>;
update: (store?: Readable<T>) => void;
}

export function switchableStore<T extends K, K>(
initialStore?: Readable<T> | undefined,
unsetValue: K = undefined
): SwitchableStoreResult<T> {
let currentStore = initialStore;
let _set: ((value: T) => void) | undefined = undefined;
let unsubscribe: (() => void) | undefined = undefined;

function makeSubscription(store: Readable<T> | undefined, set: (value: T) => void) {
if (store) {
return store.subscribe((val) => {
set(val);
});
} else {
set(unsetValue as T);
return undefined;
}
}

const store = readable<T>(undefined as T, (set) => {
unsubscribe = makeSubscription(currentStore, set);
_set = set;

return () => {
if (unsubscribe) {
unsubscribe();
}
unsubscribe = undefined;
_set = undefined;
};
});

const update = (store?: Readable<T>) => {
currentStore = store;
if (_set) {
if (unsubscribe) {
unsubscribe();
}
unsubscribe = makeSubscription(currentStore, _set);
}
};

return { store, update };
}

0 comments on commit 9b56746

Please sign in to comment.