Skip to content

Commit

Permalink
Merge pull request #2666 from exadel-inc/feat/drag-to-scroll
Browse files Browse the repository at this point in the history
feat(esl-drag-to-scroll): create mixin to enable drag-to-scroll functionality
  • Loading branch information
abarmina committed Sep 18, 2024
2 parents 6c68f10 + 3102ce6 commit 2a721cf
Show file tree
Hide file tree
Showing 13 changed files with 303 additions and 0 deletions.
1 change: 1 addition & 0 deletions .commitlintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ rules:
- esl-animate
- esl-base-element
- esl-carousel
- esl-drag-to-scroll
- esl-event-listener
- esl-footnotes
- esl-forms
Expand Down
1 change: 1 addition & 0 deletions site/site.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ rewriteRules:
"src/modules/esl-image/README.md": "/components/esl-image"
"src/modules/esl-image-utils/README.md": "/components/esl-image-utils"
"src/modules/esl-carousel/README.md": "/components/esl-carousel"
"src/modules/esl-drag-to-scroll/README.md": "/components/esl-drag-to-scroll"
2 changes: 2 additions & 0 deletions site/src/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
ESLTabs,
ESLTab,
ESLScrollbar,
ESLDragToScrollMixin,
ESLAlert,
ESLToggleableDispatcher,
ESLSelect,
Expand Down Expand Up @@ -102,6 +103,7 @@ ESLA11yGroup.register();
ESLTabs.register();

ESLScrollbar.register();
ESLDragToScrollMixin.register();

ESLAlert.register();
ESLAlert.init({
Expand Down
1 change: 1 addition & 0 deletions site/static/assets/examples/drag-to-scroll.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions site/views/components/esl-drag-to-scroll.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
layout: content
title: ESL Drag to Scroll
seoTitle: ESL Drag to Scroll - custom mixin to enable drag-to-scroll functionality for the element
name: ESL Drag to Scroll
tags: [components, new]
aside:
source: src/modules/esl-drag-to-scroll
examples:
- esl-drag-to-scroll
---

{% mdRender 'src/modules/esl-drag-to-scroll/README.md', 'intro' %}
74 changes: 74 additions & 0 deletions site/views/examples/esl-drag-to-scroll.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
layout: content
title: ESL Drag to Scroll
seoTitle: Handle drag-to-scroll functionality for any scrollable content using ESL Mixin
name: Drag to Scroll
tags: [examples, playground]
icon: examples/drag-to-scroll.svg
aside:
components:
- esl-drag-to-scroll
---
<section class="row">
<div class="col-12">
<uip-root>
<script type="text/html"
label="Both (Default)"
uip-snippet
uip-snippet-js="js-snippet-scrollbar">
<section style="resize: both; overflow: hidden; max-width: 100%; height: 500px; max-height: 70vh;" class="border bg-white relative-block">
<div style="max-width: 100%; max-height: 100%;" class="p-4 esl-scrollable-content" esl-drag-to-scroll>
<div style="min-width: 50vw">
<h2>Horizontal scroll appears only if content is too long for the area (resize the section by dragging the lower right corner)</h2>
<!-- paragraph x10 -->
</div>
</div>
<esl-scrollbar class="scrollbar-blue" target="::prev(.esl-scrollable-content)"></esl-scrollbar>
<esl-scrollbar class="scrollbar-blue scrollbar-second" target="::prev(.esl-scrollable-content)" horizontal></esl-scrollbar>
</section>
</script>

<script type="text/html"
label="Vertical"
uip-snippet
uip-snippet-js="js-snippet-scrollbar">
<section class="border bg-white relative-block">
<div class="p-4 esl-scrollable-content" esl-drag-to-scroll="{axis: 'y'}">
<div style="height: 500px">
<h2>Simple vertical drag to scroll example</h2>
<!-- paragraph x25 -->
</div>
</div>
<esl-scrollbar class="scrollbar-blue" target="::prev"></esl-scrollbar>
</section>
</script>

<script type="text/html"
label="Horizontal"
uip-snippet
uip-snippet-js="js-snippet-scrollbar">
<section class="border bg-white relative-block">
<div class="p-4 esl-scrollable-content" esl-drag-to-scroll="{axis: 'x'}">
<div style="width: 3200px;">
<h2>Simple horizontal drag to scroll example</h2>
<!-- paragraph x4 -->
</div>
</div>
<esl-scrollbar class="scrollbar-blue" target="::prev" horizontal></esl-scrollbar>
</section>
</script>

<script id="js-snippet-scrollbar" type="text/plain">
import { ESLScrollbar, ESLDragToScrollMixin } from '@exadel/esl';
ESLScrollbar.register();
ESLDragToScrollMixin.register();
</script>

<uip-snippets class="uip-toolbar" dropdown-view="@xs"></uip-snippets>
<uip-preview></uip-preview>
<uip-editor label="Source code (HTML)" collapsible copy></uip-editor>
<uip-editor source="js" label="Source code (JS)" collapsible collapsed copy></uip-editor>
</uip-root>
</div>
</section>
1 change: 1 addition & 0 deletions src/modules/all.less
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import './esl-media/core.less';

@import './esl-scrollbar/core.less';
@import './esl-drag-to-scroll/core.less';

@import './esl-a11y-group/core.less';
@import './esl-toggleable/core.less';
Expand Down
1 change: 1 addition & 0 deletions src/modules/all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export * from './esl-open-state/core';

// Scrollbar
export * from './esl-scrollbar/core';
export * from './esl-drag-to-scroll/core';

// Alert
export * from './esl-alert/core';
Expand Down
54 changes: 54 additions & 0 deletions src/modules/esl-drag-to-scroll/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# [ESL](../../../) Drag to Scroll

Version: *1.0.0*

Authors: *Anna Barmina*, *Alexey Stsefanovich (ala'n)*

<a name="intro"></a>

## ESL Drag To Scroll Mixin

`ESLDragToScrollMixin` (`esl-drag-to-scroll`) is a custom attribute that enables drag-to-scroll functionality for any scrollable container element.
This mixin enhances user experience by allowing intuitive drag-based scrolling, making it easier to navigate through content.

### Configuration
The mixin uses a primary attribute, `esl-drag-to-scroll`, with optional configuration passed as a JSON attribute value.

**Configuration options:**
- `axis` (string) - determines the scrolling axis to control. Possible values:
- `'both'` - both horizontal and vertical scrolling (by default);
- `'x'` - horizontal scrolling only;
- `'y'` - vertical scrolling only.
- `cls` (string) - class to apply to the element during dragging to indicate the drag state.
By default, the class `dragging` is applied.
- `tolerance` (number) - a minimum distance to move before the drag action starts.
By default, the value is 10.
- `selection` (boolean) - Determines whether the text should prevent the drag-scroll action. The default value is true.

**Default Configuration"**
The default configuration for the mixin is as follows:
```json
{
"axis": "both",
"cls": "dragging",
"tolerance": 10,
"selection": true
}
```

### Usage

To use the mixin, apply the `esl-drag-to-scroll` attribute to your scrollable container element.
```html
<div class="esl-scrollable-content" esl-drag-to-scroll>
<!-- Content here -->
</div>
```

You can also provide **custom configuration** through the JSON attribute.
For example, to enable horizontal scrolling only, disable text selection, and use a custom class `is-dragging` during the drag.
```html
<div class="esl-scrollable-content" esl-drag-to-scroll="{axis: 'x', cls: 'is-dragging', selection: false}">
<!-- Content here -->
</div>
```
1 change: 1 addition & 0 deletions src/modules/esl-drag-to-scroll/core.less
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import './core/esl-drag-to-scroll.less';
1 change: 1 addition & 0 deletions src/modules/esl-drag-to-scroll/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './core/esl-drag-to-scroll.mixin';
9 changes: 9 additions & 0 deletions src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[esl-drag-to-scroll] {
cursor: grab;

&.dragging {
cursor: grabbing;
user-select: none;
scroll-behavior: auto !important;
}
}
144 changes: 144 additions & 0 deletions src/modules/esl-drag-to-scroll/core/esl-drag-to-scroll.mixin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import {ESLMixinElement} from '../../esl-mixin-element/core';
import {listen, memoize} from '../../esl-utils/decorators';
import {ExportNs} from '../../esl-utils/environment/export-ns';

import type {Point} from '../../esl-utils/dom/point';
import {evaluate} from '../../esl-utils/misc/format';

/**
* ESLDragToScrollConfig - configuration options for the ESLDragToScrollMixin
*/
export interface ESLDragToScrollConfig {
/** Determines the scrolling axis. Options are 'x', 'y', or 'both' (default) */
axis: 'x' | 'y' | 'both';
/** Class name to apply during dragging. Defaults to 'dragging' */
cls: string;
/** Min distance in pixels to activate dragging mode. Defaults to 10 */
tolerance: number;
/** Prevent dragging if text is selected or not. Defaults to true */
selection: boolean;
}

/**
* ESLDragToScrollMixin - mixin to enable drag-to-scroll functionality for any scrollable container element
* @author Anna Barmina, Alexey Stsefanovich (ala'n)
*
* Use example:
* ```
* <div class="esl-scrollable-content" esl-drag-to-scroll>
* <!-- Content here -->
* </div>
* ```
*/
@ExportNs('DragToScrollMixin')
export class ESLDragToScrollMixin extends ESLMixinElement {
public static override is = 'esl-drag-to-scroll';

/** Default configuration object */
public static DEFAULT_CONFIG: ESLDragToScrollConfig = {
axis: 'both',
cls: 'dragging',
tolerance: 10,
selection: true
};

/** Initial pointer event when dragging starts */
private startEvent: PointerEvent;

private startScrollLeft: number = 0;
private startScrollTop: number = 0;

private _isDragging: boolean = false;

/** Flag indicating whether dragging is in progress */
public get isDragging(): boolean {
return this._isDragging;
}
private set isDragging(value: boolean) {
this._isDragging = value;
this.$$cls(this.config.cls, value);
}

/**
* Mixin configuration (merged with default)
* @see ESLDragToScrollConfig
*/
@memoize()
public get config(): ESLDragToScrollConfig {
const config = evaluate(this.$$attr(ESLDragToScrollMixin.is)!, {});
return {...ESLDragToScrollMixin.DEFAULT_CONFIG, ...config};
}
public set config(value: ESLDragToScrollConfig | string) {
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
this.$$attr(ESLDragToScrollMixin.is, serialized);
}

/** Flag indicating whether text is selected */
public get hasSelection(): boolean {
const selection = document.getSelection();
if (!selection || !this.config.selection) return false;
// Prevents draggable state if the text is selected
return !selection.isCollapsed && this.$host.contains(selection.anchorNode);
}

protected override attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
memoize.clear(this, 'config');
}

/** @returns the offset of the pointer event relative to the start event */
public getEventOffset(event: PointerEvent): Point {
if (!this.startEvent || event.type === 'pointercancel') return {x: 0, y: 0};
const x = this.startEvent.clientX - event.clientX;
const y = this.startEvent.clientY - event.clientY;
return {x, y};
}

/** Scrolls the host element by the specified offset */
public scrollBy(offset: Point): void {
if (this.config.axis === 'x' || this.config.axis === 'both') {
this.$host.scrollLeft = this.startScrollLeft + offset.x;
}

if (this.config.axis === 'y' || this.config.axis === 'both') {
this.$host.scrollTop = this.startScrollTop + offset.y;
}
}

/** Handles the pointerdown event to start dragging */
@listen('pointerdown')
private onPointerDown(event: PointerEvent): void {
this.startEvent = event;
this.startScrollLeft = this.$host.scrollLeft;
this.startScrollTop = this.$host.scrollTop;

this.$$on({group: 'pointer'});
}

/** Handles the pointermove event to perform scrolling while dragging */
@listen({auto: false, event: 'pointermove', group: 'pointer'})
private onPointerMove(event: PointerEvent): void {
const offset = this.getEventOffset(event);

if (!this.isDragging) {
// Stop tracking if there was a selection before dragging started
if (this.hasSelection) return this.onPointerUp(event);
// Does not start dragging mode if offset have not reached tolerance
if (Math.abs(Math.max(Math.abs(offset.x), Math.abs(offset.y))) < this.config.tolerance) return;
this.isDragging = true;
}

this.$host.setPointerCapture(event.pointerId);
this.scrollBy(offset);
}

/** Handles the pointerup and pointercancel events to stop dragging */
@listen({auto: false, event: 'pointerup pointercancel', group: 'pointer'})
private onPointerUp(event: PointerEvent): void {
this.$$off({group: 'pointer'});
if (this.$host.hasPointerCapture(event.pointerId)) {
this.$host.releasePointerCapture(event.pointerId);
}
this.scrollBy(this.getEventOffset(event));
this.isDragging = false;
}
}

0 comments on commit 2a721cf

Please sign in to comment.