Skip to content

Commit

Permalink
✨ Adds Sort Plugin (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
ekwoka authored Aug 25, 2024
1 parent 4b015e6 commit 2d0b191
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 1 deletion.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "alpinejs",
"version": "3.13.0",
"version": "3.14.1",
"type": "module",
"exports": {
".": {
Expand Down
1 change: 1 addition & 0 deletions packages/alpinets/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface XAttributes {
};
_x_hideChildren: ElementWithXAttributes[];
_x_inlineBindings: Record<string, Binding>;
_x_sort_key: string;
}

type Binding = {
Expand Down
42 changes: 42 additions & 0 deletions packages/sort/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@alpinets/sort",
"version": "0.0.1",
"description": "The rugged, minimal TypeScript framework",
"author": "Eric Kwoka <[email protected]> (https://thekwoka.net/)",
"license": "MIT",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./src": {
"import": "./src/index.ts"
},
"./package.json": "./package.json"
},
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src"
],
"scripts": {
"build": "vite build",
"coverage": "vitest run --coverage",
"lint:types": "tsc --noEmit",
"prebuild": "rm -rf dist",
"test": "vitest"
},
"dependencies": {
"sortablejs": "1.15.2"
},
"peerDependencies": {
"@alpinets/alpinets": "workspace:^"
},
"devDependencies": {
"@types/sortablejs": "1.15.8",
"vite": "5.3.5",
"vitest": "2.0.5"
}
}
186 changes: 186 additions & 0 deletions packages/sort/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import type { PluginCallback } from '@alpinets/alpinets';
import Alpine from '@alpinets/alpinets';
import { ElementWithXAttributes } from '@alpinets/alpinets';
import { Utilities } from '@alpinets/alpinets/src/types';
// @ts-expect-error
import Sortable from 'sortablejs';

export const Sort: PluginCallback = (Alpine) => {
Alpine.directive(
'sort',
(
el,
{ value, modifiers, expression },
{ evaluate, evaluateLater, cleanup },
) => {
if (value === 'config') {
return; // This will get handled by the main directive...
}

if (value === 'handle') {
return; // This will get handled by the main directive...
}

if (value === 'group') {
return; // This will get handled by the main directive...
}

// Supporting both `x-sort:item` AND `x-sort:key` (key for BC)...
if (value === 'key' || value === 'item') {
if ([undefined, null, ''].includes(expression)) return;

el._x_sort_key = evaluate(expression);

return;
}

const preferences = {
hideGhost: !modifiers.includes('ghost'),
useHandles: !!el.querySelector('[x-sort\\:handle]'),
group: getGroupName(el, modifiers),
};

const handleSort = generateSortHandler(expression, evaluateLater);

const config = getConfigurationOverrides(el, modifiers, evaluate);

const sortable = initSortable(
el,
config,
preferences,
(key, position) => {
handleSort(key, position);
},
);

cleanup(() => sortable.destroy());
},
);
};

function generateSortHandler(
expression: string,
evaluateLater: Utilities['evaluateLater'],
) {
// No handler was passed to x-sort...
// biome-ignore lint/suspicious/noEmptyBlockStatements: Intentional No-op
if ([undefined, null, ''].includes(expression)) return () => {};

const handle = evaluateLater(expression);

return (key: string, position: number) => {
// In the case of `x-sort="handleSort"`, let us call it manually...
Alpine.dontAutoEvaluateFunctions(() => {
handle(
// If a function is returned, call it with the key/position params...
(received) => {
if (typeof received === 'function') received(key, position);
},
// Provide $key and $position to the scope in case they want to call their own function...
{
scope: {
// Supporting both `$item` AND `$key` ($key for BC)...
$key: key,
$item: key,
$position: position,
},
},
);
});
};
}

function getConfigurationOverrides(
el: ElementWithXAttributes,
_modifiers: string[],
evaluate: Utilities['evaluate'],
) {
return el.hasAttribute('x-sort:config')
? evaluate(el.getAttribute('x-sort:config'))
: {};
}

function initSortable(el, config, preferences, handle) {
let ghostRef;

const options = {
animation: 150,

handle: preferences.useHandles ? '[x-sort\\:handle]' : null,

group: preferences.group,

filter(e) {
// Normally, we would just filter out any elements without `[x-sort:item]`
// on them, however for backwards compatibility (when we didn't require
// `[x-sort:item]`) we will check for x-sort\\:item being used at all
if (!el.querySelector('[x-sort\\:item]')) return false;

const itemHasAttribute = e.target.closest('[x-sort\\:item]');

return itemHasAttribute ? false : true;
},

onSort(e) {
// If item has been dragged between groups...
if (e.from !== e.to) {
// And this is the group it was dragged FROM...
if (e.to !== e.target) {
return; // Don't do anything, because the other group will call the handler...
}
}

const key = e.item._x_sort_key;
const position = e.newIndex;

if (key !== undefined || key !== null) {
handle(key, position);
}
},

onStart() {
document.body.classList.add('sorting');

ghostRef = document.querySelector('.sortable-ghost');

if (preferences.hideGhost && ghostRef) ghostRef.style.opacity = '0';
},

onEnd() {
document.body.classList.remove('sorting');

if (preferences.hideGhost && ghostRef) ghostRef.style.opacity = '1';

ghostRef = undefined;

keepElementsWithinMorphMarkers(el);
},
};

return new Sortable(el, { ...options, ...config });
}

function keepElementsWithinMorphMarkers(el) {
let cursor = el.firstChild;

while (cursor.nextSibling) {
if (cursor.textContent.trim() === '[if ENDBLOCK]><![endif]') {
el.append(cursor);
break;
}

cursor = cursor.nextSibling;
}
}

function getGroupName(el, modifiers) {
if (el.hasAttribute('x-sort:group')) {
return el.getAttribute('x-sort:group');
}

return modifiers.indexOf('group') !== -1
? modifiers[modifiers.indexOf('group') + 1]
: null;
}

export default Sort;
5 changes: 5 additions & 0 deletions packages/sort/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["src/**/*", "tests/**/*"],
"exclude": ["**/node_modules", "**/dist"]
}
52 changes: 52 additions & 0 deletions packages/sort/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// <reference types="vitest" />
import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import ExternalDeps from 'vite-plugin-external-deps';
import WorkspaceSource from 'vite-plugin-workspace-source';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
root: resolve(__dirname),
plugins: [
dts({
entryRoot: resolve(__dirname, 'src'),
tsconfigPath: resolve(__dirname, 'tsconfig.json'),
}),
tsconfigPaths(),
ExternalDeps(),
WorkspaceSource(),
],
define: {
'import.meta.vitest': 'undefined',
'import.meta.DEBUG': 'false',
},
build: {
target: 'esnext',
outDir: resolve(__dirname, 'dist'),
lib: {
entry: resolve(__dirname, 'src', 'index.ts'),
formats: ['es'],
},
minify: false,
rollupOptions: {
output: {
preserveModules: true,
preserveModulesRoot: 'src',
entryFileNames: ({ name: fileName }) => {
return `${fileName}.js`;
},
sourcemap: true,
},
external: [/node_modules/],
},
},
test: {
globals: true,
include: ['./**/*{.spec,.test}.{ts,tsx}'],
includeSource: ['./**/*.{ts,tsx}'],
reporters: ['dot'],
deps: {},
passWithNoTests: true,
},
});
30 changes: 30 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions size.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,15 @@
"pretty": "447 B",
"raw": 447
}
},
"sort": {
"minified": {
"pretty": "78.2 kB",
"raw": 78168
},
"brotli": {
"pretty": "25.3 kB",
"raw": 25287
}
}
}

0 comments on commit 2d0b191

Please sign in to comment.