diff --git a/.build/markup-classes-baseline.txt b/.build/markup-classes-baseline.txt index af8f12fdb..3fcc4c5c1 100644 --- a/.build/markup-classes-baseline.txt +++ b/.build/markup-classes-baseline.txt @@ -36,6 +36,8 @@ sort-score sort-status sort-tags sort-type +sortable-fixed +sortable-handle table-tbody tblr-illustrations-computer-fix-a tblr-illustrations-computer-fix-b diff --git a/.changeset/sortable-demo-examples.md b/.changeset/sortable-demo-examples.md new file mode 100644 index 000000000..7fe9891ea --- /dev/null +++ b/.changeset/sortable-demo-examples.md @@ -0,0 +1,5 @@ +--- +"@tabler/preview": minor +--- + +Added grouped Sortable demos: drag handle, table rows, kanban board, clone, multi-drag and swap. diff --git a/.changeset/sortable-drag-styles.md b/.changeset/sortable-drag-styles.md new file mode 100644 index 000000000..ddd859b62 --- /dev/null +++ b/.changeset/sortable-drag-styles.md @@ -0,0 +1,5 @@ +--- +"@tabler/core": minor +--- + +Added SortableJS drag styles and turned on `forceFallback` by default so `data-sortable` lists drag a styled, tilted copy. diff --git a/core/js/src/sortable.ts b/core/js/src/sortable.ts index 298e10cd4..e6f793c03 100644 --- a/core/js/src/sortable.ts +++ b/core/js/src/sortable.ts @@ -22,7 +22,9 @@ const DATA_ATTRIBUTE = `data-${NAME}` const SELECTOR_DATA_SORTABLE = `[${DATA_ATTRIBUTE}]` -const Default: ComponentConfig = {} +const Default: ComponentConfig = { + forceFallback: true, +} const DefaultType: Record = {} @@ -32,6 +34,7 @@ const DefaultType: Record = {} * Wraps SortableJS (https://sortablejs.github.io/Sortable/), loaded separately * as `window.Sortable`. Without the plugin the component is inert. Options come * from the `data-sortable` attribute as JSON, or from the config object. + * `forceFallback` is on by default so the dragged copy can be styled with `.sortable-drag`. */ class Sortable extends BaseComponent { diff --git a/core/js/tests/unit/sortable.spec.ts b/core/js/tests/unit/sortable.spec.ts index 58cda1fad..4e8d44c1a 100644 --- a/core/js/tests/unit/sortable.spec.ts +++ b/core/js/tests/unit/sortable.spec.ts @@ -45,6 +45,19 @@ describe('Sortable', () => { expect(instance.sortable).toBe(plugin.mock.instances[0]) }) + it('should drag a styled copy by default', () => { + new Sortable(list()) + + expect(plugin.mock.calls[0][1]).toMatchObject({ forceFallback: true }) + }) + + it('should let data-sortable turn the styled copy off', () => { + fixtureEl.innerHTML = '' + new Sortable(list()) + + expect(plugin.mock.calls[0][1]).toMatchObject({ forceFallback: false }) + }) + it('should let the config object win over the attribute', () => { new Sortable(list(), { animation: 0, group: 'shared' }) diff --git a/core/package.json b/core/package.json index 07a3e8106..0cffa6819 100644 --- a/core/package.json +++ b/core/package.json @@ -82,19 +82,19 @@ "files": [ { "path": "./dist/css/tabler.css", - "maxSize": "87 kB" + "maxSize": "88 kB" }, { "path": "./dist/css/tabler.min.css", - "maxSize": "81 kB" + "maxSize": "82 kB" }, { "path": "./dist/css/tabler.rtl.css", - "maxSize": "87 kB" + "maxSize": "88 kB" }, { "path": "./dist/css/tabler.rtl.min.css", - "maxSize": "81 kB" + "maxSize": "82 kB" }, { "path": "./dist/css/tabler-flags.css", diff --git a/core/scss/_core.scss b/core/scss/_core.scss index 50af33123..ca30366c8 100644 --- a/core/scss/_core.scss +++ b/core/scss/_core.scss @@ -101,6 +101,7 @@ @forward 'ui/chat'; @forward 'ui/signature'; @forward 'ui/patterns'; +@forward 'ui/sortable'; @forward 'helpers/index'; diff --git a/core/scss/_variables.scss b/core/scss/_variables.scss index 55afcc26f..3a9e7e660 100644 --- a/core/scss/_variables.scss +++ b/core/scss/_variables.scss @@ -1837,6 +1837,18 @@ $tracking-border-radius: var(--border-radius) !default; $tracking-block-min-width: 0.25rem !default; // scss-docs-end tracking-variables +// Sortable +// scss-docs-start sortable-variables +$sortable-ghost-bg: var(--primary-lt) !default; +$sortable-ghost-border-color: var(--primary) !default; +$sortable-ghost-opacity: 0.6 !default; +$sortable-outline-width: 1px !default; +$sortable-selected-bg: var(--primary-lt) !default; +$sortable-drag-bg: var(--bg-surface) !default; +$sortable-drag-rotate: 1.5deg !default; +$sortable-drag-box-shadow: $box-shadow-lg !default; +// scss-docs-end sortable-variables + // Progress // scss-docs-start progress-variables $progress-height: 0.5rem !default; diff --git a/core/scss/ui/_sortable.scss b/core/scss/ui/_sortable.scss new file mode 100644 index 000000000..3fc9d342e --- /dev/null +++ b/core/scss/ui/_sortable.scss @@ -0,0 +1,73 @@ +// +// Sortable +// +@use '../config' as *; + +.sortable-ghost, +.sortable-swap-highlight, +.sortable-selected, +.sortable-drag { + --sortable-ghost-bg: #{$sortable-ghost-bg}; + --sortable-ghost-border-color: #{$sortable-ghost-border-color}; + --sortable-ghost-opacity: #{$sortable-ghost-opacity}; + --sortable-outline-width: #{$sortable-outline-width}; + --sortable-selected-bg: #{$sortable-selected-bg}; + --sortable-drag-bg: #{$sortable-drag-bg}; + --sortable-drag-rotate: #{$sortable-drag-rotate}; + --sortable-drag-box-shadow: #{$sortable-drag-box-shadow}; +} + +.sortable-ghost, +.sortable-swap-highlight { + background-color: var(--sortable-ghost-bg); + outline: var(--sortable-outline-width) dashed var(--sortable-ghost-border-color); + outline-offset: calc(var(--sortable-outline-width) * -1); +} + +.sortable-ghost { + opacity: var(--sortable-ghost-opacity); +} + +tr.sortable-ghost, +tr.sortable-swap-highlight, +tr.sortable-selected { + > * { + --table-bg-state: var(--sortable-ghost-bg); + } +} + +.sortable-selected { + background-color: var(--sortable-selected-bg); + outline: var(--sortable-outline-width) solid var(--sortable-ghost-border-color); + outline-offset: calc(var(--sortable-outline-width) * -1); +} + +.list-group-item { + &.sortable-ghost, + &.sortable-swap-highlight, + &.sortable-selected, + &.sortable-drag { + @include border-radius(var(--list-group-border-radius)); + } +} + +.sortable-chosen { + cursor: grabbing; +} + +.sortable-drag { + cursor: grabbing; + background-color: var(--sortable-drag-bg); + outline: var(--sortable-outline-width) solid var(--sortable-ghost-border-color); + outline-offset: calc(var(--sortable-outline-width) * -1); + box-shadow: var(--sortable-drag-box-shadow); + opacity: 1 !important; // stylelint-disable-line declaration-no-important + transition: none; + + /* rtl:ignore */ + rotate: calc(var(--sortable-drag-rotate) * var(--dir)); +} + +.card.sortable-drag { + box-shadow: var(--sortable-drag-box-shadow); +} diff --git a/preview/pages/sortable.astro b/preview/pages/sortable.astro index 5612baca5..fe88a3ef8 100644 --- a/preview/pages/sortable.astro +++ b/preview/pages/sortable.astro @@ -1,21 +1,65 @@ --- import DefaultLayout from '@shared/layouts/DefaultLayout.astro' +import Avatar from '@ui/Avatar.astro' import Card from '@ui/Card.astro' import CardHeader from '@ui/CardHeader.astro' import CardBody from '@ui/CardBody.astro' import CardTitle from '@ui/CardTitle.astro' import CardSubtitle from '@ui/CardSubtitle.astro' +import Icon from '@ui/Icon.astro' import ListGroup from '@ui/ListGroup.astro' import ListGroupItem from '@ui/ListGroupItem.astro' +import Signal from '@ui/Signal.astro' +import { people, personById } from '@shared/lib/people' +import tasks from '@data/tasks.json' + +interface Task { + name: string + assigned_to: number + priority: string +} + +interface Column { + name: string + tasks: Task[] +} + +const priorities: Record = { + High: { value: 3, color: 'red' }, + Medium: { value: 2, color: 'orange' }, + Low: { value: 1, color: 'blue' }, +} + +const board = (tasks.columns as Column[]).map((column) => ({ + name: column.name, + tasks: column.tasks.slice(0, 4), +})) + +const selectedTopics = ['Design', 'Development', 'Marketing'] +const availableTopics = ['Sales', 'Support', 'Analytics', 'Legal', 'Finance', 'Research', 'Security', 'Hiring'] + +const team = people.slice(0, 5) +const gallery = people.slice(5, 11) + +const sections = [ + { title: 'Basics', description: 'Add data-sortable with SortableJS options as JSON to any list to make its children draggable.' }, + { title: 'Handles and tables', description: 'Limit dragging to a grip icon, or reorder the rows of a table.' }, + { title: 'Boards', description: 'Connect several lists to build a kanban board, a pick list or a nested tree.' }, + { title: 'Selection and swapping', description: 'The bundled SortableJS build also ships the MultiDrag and Swap plugins.' }, +] --- - +
+
+

{sections[0]!.title}

+

{sections[0]!.description}

+
- +
- Basic usage + Basic usage Drag to reorder items within a single list.
@@ -30,11 +74,11 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
- +
- Two lists with drag and drop - Move items between two connected lists. + Two lists with drag and drop + Give lists the same group to move items between them.
@@ -57,5 +101,351 @@ import ListGroupItem from '@ui/ListGroupItem.astro'
+
+ + +
+ Horizontal lists + Set direction to horizontal for rows of tags, and drag them between the two rows. +
+
+ +
Selected
+
+ {selectedTopics.map((topic) => {topic})} +
+
Available
+
+ {availableTopics.map((topic) => {topic})} +
+
+
+
+
+ + +
+ Fixed items + Set filter to a selector to make matching items impossible to drag. +
+
+ + + + Inbox (pinned) + + Projects + Reports + Archive + + Trash (pinned) + + + +
+
+ +
+

{sections[1]!.title}

+

{sections[1]!.description}

+
+
+ + +
+ Drag handle + Set handle so only the grip starts a drag, and the rest of the row stays clickable. +
+
+ + + { + team.map((person) => ( + +
+
+ +
+
+ +
+
+ + {person.full_name} + +
{person.job_title}
+
+
+
+ )) + } +
+
+
+
+
+ + +
+ Sortable table + Put data-sortable on the tbody to reorder rows. +
+
+
+ + + + + + + + + + + { + team.map((person) => ( + + + + + + + )) + } + +
DragNameTitleCountry
+ + {person.full_name}{person.job_title}{person.country}
+
+
+
+
+ + +
+ Grid of avatars + Sortable also works on a wrapping layout, not just on vertical lists. +
+
+ +
+ { + gallery.map((person) => ( +
+
+ +
{person.first_name}
+
+
+ )) + } +
+
+
+
+
+ + +
+ Disabled list + Set disabled to true to lock a list, for example while the user has no edit rights. +
+
+ + + + Read-only item 1 + + + Read-only item 2 + + + Read-only item 3 + + + +
+
+ +
+

{sections[2]!.title}

+

{sections[2]!.description}

+
+
+ + +
+ Kanban board + Drag tasks between columns that share one group. Items are dragged as a tilted copy with a shadow. Set forceFallback to false to use the browser preview instead. +
+
+ +
+ { + board.map((column) => ( +
+

{column.name}

+
+ {column.tasks.map((task) => ( +
+
+
{task.name}
+
+ + + {task.priority} + + +
+
+
+ ))} +
+
+ )) + } +
+
+
+
+
+ + +
+ Clone from a list + Use pull: "clone" and put: false on the source so items are copied, not moved. +
+
+ +
+
+
Templates
+ + Heading + Paragraph + Image + Button + +
+
+
Page
+ + Paragraph + +
+
+
+
+
+
+ + +
+ One-way transfer + Set put: false on a list to make it a source only, and use it to build a pick list. +
+
+ +
+
+
Available
+ + Email alerts + SMS alerts + Weekly digest + Push notifications + +
+
+
Selected
+ + In-app messages + +
+
+
+
+
+
+ + +
+ Nested lists + Put a sortable list inside an item and give all lists one group to move items between levels. +
+
+ + + +
Design
+ + Wireframes + Prototype + +
+ +
Development
+ + Frontend + Backend + Tests + +
+ +
Launch
+ + Release notes + +
+
+
+
+
+ +
+

{sections[3]!.title}

+

{sections[3]!.description}

+
+
+ + +
+ Multiple selection + Set multiDrag to true. Hold Ctrl or ⌘ and click to select items, then drag them together. +
+
+ + + Invoice #1001 + Invoice #1002 + Invoice #1003 + Invoice #1004 + Invoice #1005 + + +
+
+
+ + +
+ Swap + Set swap to true to exchange two items instead of shifting the rest. +
+
+ + + First place + Second place + Third place + Fourth place + + +
+
diff --git a/screenshots/pages/sortable.astro b/screenshots/pages/sortable.astro new file mode 100644 index 000000000..1ecaa0b25 --- /dev/null +++ b/screenshots/pages/sortable.astro @@ -0,0 +1,87 @@ +--- +// Sortable on a kanban board, drawn in the middle of a drag. The card being +// moved has left the Upcoming column and shows twice, as SortableJS renders it: +// a dashed slot in In Progress where it will land, and the copy that follows the +// pointer, lifted with a shadow and an outline. Both carry the real state classes +// (`.sortable-ghost`, `.sortable-drag`), so the picture shows the shipped styles. +// The fake cursor rests on the floating card. +import ScreenshotLayout from '../layouts/ScreenshotLayout.astro' +import Avatar from '@ui/Avatar.astro' +import Card from '@ui/Card.astro' +import CardBody from '@ui/CardBody.astro' +import Signal from '@ui/Signal.astro' +import { personById } from '@shared/lib/people' +import tasks from '@data/tasks.json' + +interface Task { + name: string + assigned_to: number + priority: string +} + +interface Column { + name: string + tasks: Task[] +} + +const [upcoming, progress, completed] = tasks.columns as Column[] as [Column, Column, Column] +const priorities: Record = { + High: { value: 3, color: 'red' }, + Medium: { value: 2, color: 'orange' }, + Low: { value: 1, color: 'blue' }, +} + +const moving = upcoming.tasks[2]! + +const columns = [ + { name: upcoming.name, cards: [upcoming.tasks[0]!, upcoming.tasks[1]!] }, + { name: progress.name, cards: [progress.tasks[0]!, moving, progress.tasks[1]!] }, + { name: completed.name, cards: [completed.tasks[0]!, completed.tasks[1]!, completed.tasks[2]!] }, +] +--- + + + + +
+ { + columns.map((column) => ( +
+

{column.name}

+
+ {column.cards.map((task) => ( + + +
{task.name}
+
+ + + {task.priority} + + +
+
+
+ ))} +
+
+ )) + } +
+ +
+
+
{moving.name}
+
+ + + {moving.priority} + + +
+
+ +
+
+
+