Skip to content

Commit

Permalink
Scroll lock plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
PuruVJ committed Dec 20, 2024
1 parent 011df16 commit a981783
Show file tree
Hide file tree
Showing 7 changed files with 224 additions and 44 deletions.
7 changes: 4 additions & 3 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"singleQuote": true,
"printWidth": 100,
"useTabs": true
"singleQuote": true,
"printWidth": 100,
"useTabs": true,
"trailingComma": "all"
}
16 changes: 8 additions & 8 deletions docs/src/data/sizes.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{
"react": {
"size": 2.14,
"version": "2.2.0"
},
"svelte": {
"size": 2.96,
"size": 3.64,
"version": "2.2.0"
},
"vanilla": {
"size": 1.95,
"react": {
"size": 2.28,
"version": "2.2.0"
},
"solid": {
"size": 1.93,
"size": 2.07,
"version": "2.2.0"
},
"vue": {
"size": 1.98,
"version": "2.2.0"
},
"vanilla": {
"size": 2.1,
"version": "2.2.0"
}
}
5 changes: 5 additions & 0 deletions docs/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export function elements_overlap(el1: HTMLElement, el2: HTMLElement) {
export function wait_for(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}

// Add scroll helpers
export function isWindow(container: HTMLElement | Window): container is Window {
return container === window;
}
32 changes: 16 additions & 16 deletions packages/core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import type { Plugin, PluginContext } from './plugins.ts';
import { listen } from './utils.ts';

type DeepMutable<T> = T extends object
? {
-readonly [P in keyof T]: T[P] extends readonly any[]
? DeepMutable<T[P]>
: T[P] extends object
? keyof T[P] extends never
? T[P]
: DeepMutable<T[P]>
: T[P];
}
? keyof T[P] extends never
? T[P]
: DeepMutable<T[P]>
: T[P];
}
: T;

interface DraggableInstance {
Expand All @@ -25,8 +26,8 @@ interface DraggableInstance {

export function createDraggable({
plugins: initial_plugins = [],
delegateTargetFn = () => document.body,
}: { plugins?: Plugin[]; delegateTargetFn?: () => HTMLElement } = {}) {
delegate: delegateTargetFn = () => document.body,
}: { plugins?: Plugin[]; delegate?: () => HTMLElement } = {}) {
const instances = new WeakMap<HTMLElement, DraggableInstance>();
let listeners_initialized = false;
let active_node: HTMLElement | null = null;
Expand All @@ -36,16 +37,16 @@ export function createDraggable({

const delegateTarget = delegateTargetFn();

delegateTarget.addEventListener('pointerdown', handle_pointer_down, {
listen(delegateTarget, 'pointerdown', handle_pointer_down, {
passive: true,
capture: false,
});
delegateTarget.addEventListener('pointermove', handle_pointer_move, {
listen(delegateTarget, 'pointermove', handle_pointer_move, {
passive: false,
capture: false,
});
delegateTarget.addEventListener('pointerup', handle_pointer_up, {
passive: false,
listen(delegateTarget, 'pointerup', handle_pointer_up, {
passive: true,
capture: false,
});

Expand Down Expand Up @@ -132,8 +133,7 @@ export function createDraggable({
if (!instance.ctx.isInteracting) return;

if (instance.ctx.isDragging) {
// Listen for click handler and cancel it
active_node.addEventListener('click', (e) => e.stopPropagation(), {
listen(active_node, 'click', (e) => e.stopPropagation(), {
once: true,
signal: instance.controller.signal,
capture: true,
Expand Down Expand Up @@ -298,7 +298,7 @@ export function createDraggable({

// Clean up old instance if different
if (old_plugin && old_plugin !== new_plugin) {
old_plugin.cleanup?.();
old_plugin.cleanup?.(instance.ctx, instance.states.get(old_plugin.name));
instance.states.delete(old_plugin.name);
}

Expand Down Expand Up @@ -340,7 +340,7 @@ export function createDraggable({
);

for (const plugin of removed_plugins) {
plugin.cleanup?.();
plugin.cleanup?.(instance.ctx, instance.states.get(plugin.name));
instance.states.delete(plugin.name);
has_changes = true;
}
Expand Down Expand Up @@ -377,7 +377,7 @@ export function createDraggable({
}

for (const plugin of instance.plugins) {
plugin.cleanup?.();
plugin.cleanup?.(instance.ctx, instance.states.get(plugin.name));
}

instances.delete(node);
Expand Down
170 changes: 155 additions & 15 deletions packages/core/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { get_node_style, set_node_dataset, set_node_key_style } from './utils.ts';

export interface PluginContext {
delta: {
x: number;
Expand Down Expand Up @@ -91,7 +93,7 @@ export interface Plugin<PrivateState = any> {
dragEnd?: (context: PluginContext, state: PrivateState, event: PointerEvent) => void;

// Cleanup when draggable is destroyed
cleanup?: () => void;
cleanup?: (context: PluginContext, state: PrivateState) => void;
}

/**
Expand Down Expand Up @@ -140,9 +142,9 @@ export const stateMarker = unstable_definePlugin(() => {
cancelable: false,

setup(ctx) {
ctx.rootNode.dataset.neodrag = '';
ctx.rootNode.dataset.neodragState = 'idle';
ctx.rootNode.dataset.neodragCount = '0';
set_node_dataset(ctx.rootNode, 'neodrag', '');
set_node_dataset(ctx.rootNode, 'neodragState', 'idle');
set_node_dataset(ctx.rootNode, 'neodragCount', '0');

return {
count: 0,
Expand All @@ -151,13 +153,13 @@ export const stateMarker = unstable_definePlugin(() => {

dragStart(ctx) {
ctx.effect(() => {
ctx.rootNode.dataset.neodragState = 'dragging';
set_node_dataset(ctx.rootNode, 'neodragState', 'dragging');
});
},

dragEnd(ctx, state) {
ctx.rootNode.dataset.neodragState = 'idle';
ctx.rootNode.dataset.neodragCount = (++state.count).toString();
set_node_dataset(ctx.rootNode, 'neodragState', 'idle');
set_node_dataset(ctx.rootNode, 'neodragCount', ++state.count);
},
};
});
Expand Down Expand Up @@ -189,15 +191,15 @@ export const applyUserSelectHack = unstable_definePlugin((value: boolean = true)
dragStart(ctx, state) {
ctx.effect(() => {
if (value) {
state.body_user_select_val = document.body.style.userSelect;
document.body.style.userSelect = 'none';
state.body_user_select_val = get_node_style(document.body, 'userSelect');
set_node_key_style(document.body, 'userSelect', 'none');
}
});
},

dragEnd(_, state) {
if (value) {
document.body.style.userSelect = state.body_user_select_val;
set_node_key_style(document.body, 'userSelect', state.body_user_select_val);
}
},
};
Expand Down Expand Up @@ -251,7 +253,11 @@ export const transform = unstable_definePlugin(
rootNode: ctx.rootNode,
});
} else {
ctx.rootNode.style.translate = `${ctx.offset.x}px ${ctx.offset.y}px`;
set_node_key_style(
ctx.rootNode,
'transform',
`translate(${ctx.offset.x}px, ${ctx.offset.y}px)`,
);
}
}
},
Expand All @@ -266,7 +272,11 @@ export const transform = unstable_definePlugin(
});
}

ctx.rootNode.style.translate = `${ctx.offset.x}px ${ctx.offset.y}px`;
set_node_key_style(
ctx.rootNode,
'transform',
`translate(${ctx.offset.x}px, ${ctx.offset.y}px)`,
);
});
},
};
Expand Down Expand Up @@ -298,6 +308,8 @@ export const BoundsFrom = {
};
},

// selector,

parent(padding?: {
top?: number;
left?: number;
Expand Down Expand Up @@ -669,15 +681,143 @@ export const touchAction = unstable_definePlugin((mode: TouchActionMode = 'manip
liveUpdate: true,

setup(ctx) {
const original_touch_action = ctx.rootNode.style.touchAction;
ctx.rootNode.style.touchAction = mode;
const original_touch_action = get_node_style(ctx.rootNode, 'touchAction');
set_node_key_style(ctx.rootNode, 'touchAction', mode);

return { original_touch_action };
},

dragEnd(ctx, state) {
// Restore original touch-action
ctx.rootNode.style.touchAction = state.original_touch_action || 'auto';
set_node_key_style(ctx.rootNode, 'touchAction', state.original_touch_action || 'auto');
},
};
});

// Scroll-lock plugin that prevents scrolling while dragging
export const scrollLock = unstable_definePlugin(
(
options: {
lockAxis?: 'x' | 'y' | 'both'; // Which axes to lock scrolling on
container?: HTMLElement | (() => HTMLElement); // Custom container to lock
allowScrollbar?: boolean; // Whether to allow scrollbar interaction
} = {},
) => {
return {
name: 'neodrag:scrollLock',

setup() {
const defaults = {
lockAxis: 'both',
container: window,
allowScrollbar: false,
};

const config = { ...defaults, ...options };

return {
config,
originalStyles: new Map<
HTMLElement,
{
userSelect: string;
touchAction: string;
overflow: string;
}
>(),
containerRect: null as DOMRect | null,
lastContainerCheck: 0,
};
},

dragStart(ctx, state) {
const container =
typeof state.config.container === 'function'
? state.config.container()
: state.config.container;

// Reset cache
state.containerRect = null;
state.lastContainerCheck = 0;

ctx.effect(() => {
// Store original styles
if (container instanceof HTMLElement) {
state.originalStyles.set(container, {
userSelect: get_node_style(container, 'userSelect'),
touchAction: get_node_style(container, 'touchAction'),
overflow: get_node_style(container, 'overflow'),
});

// Apply scroll locking styles
set_node_key_style(container, 'userSelect', 'none');

if (!state.config.allowScrollbar) {
set_node_key_style(container, 'overflow', 'hidden');
}

if (state.config.lockAxis === 'x' || state.config.lockAxis === 'both') {
set_node_key_style(container, 'touchAction', 'pan-y');
} else if (state.config.lockAxis === 'y') {
set_node_key_style(container, 'touchAction', 'pan-x');
}
} else {
// For window, we need to lock the body
const body = document.body;
state.originalStyles.set(body, {
userSelect: get_node_style(body, 'userSelect'),
touchAction: get_node_style(body, 'touchAction'),
overflow: get_node_style(body, 'overflow'),
});

set_node_key_style(body, 'userSelect', 'none');

if (!state.config.allowScrollbar) {
set_node_key_style(body, 'overflow', 'hidden');
}

if (state.config.lockAxis === 'x' || state.config.lockAxis === 'both') {
set_node_key_style(body, 'touchAction', 'pan-y');
} else if (state.config.lockAxis === 'y') {
set_node_key_style(body, 'touchAction', 'pan-x');
}
}
});
},

dragEnd(ctx, state) {
const container =
typeof state.config.container === 'function'
? state.config.container()
: state.config.container;

ctx.effect(() => {
const target = container instanceof HTMLElement ? container : document.body;
const originalStyles = state.originalStyles.get(target);

if (originalStyles) {
set_node_key_style(target, 'userSelect', originalStyles.userSelect);
set_node_key_style(target, 'touchAction', originalStyles.touchAction);
set_node_key_style(target, 'overflow', originalStyles.overflow);
}

state.originalStyles.delete(target);
});

// Clear cache
state.containerRect = null;
state.lastContainerCheck = 0;
},

cleanup(_, state) {
// Restore any remaining original styles
for (const [element, styles] of state.originalStyles) {
set_node_key_style(element, 'userSelect', styles.userSelect);
set_node_key_style(element, 'touchAction', styles.touchAction);
set_node_key_style(element, 'overflow', styles.overflow);
}
state.originalStyles.clear();
},
};
},
);
Loading

0 comments on commit a981783

Please sign in to comment.