Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Adds Sort Plugin #58

Merged
merged 1 commit into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <eric@thekwoka.net> (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
}
}
}