mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-12-22 17:11:43 +03:00
Turn drag & drop into a Svelte action
There are lots of things that different drop zones have in common, so this commit abstracts that away.
This commit is contained in:
parent
ebe1124ee4
commit
d2f57f7797
@ -7,7 +7,7 @@
|
||||
</head>
|
||||
|
||||
<body
|
||||
class="h-full w-full select-none bg-light-200 font-sans text-base text-light-800 antialiased dark:bg-dark-900 dark:text-light-300"
|
||||
class="h-full w-full cursor-default select-none bg-light-200 font-sans text-base text-light-800 antialiased dark:bg-dark-900 dark:text-light-300"
|
||||
style="height: 100vh"
|
||||
>
|
||||
%sveltekit.body%
|
||||
|
@ -325,41 +325,13 @@ input[type='checkbox'].large {
|
||||
.drag-zone-marker {
|
||||
@apply border-green-450 bg-green-200 dark:bg-green-470;
|
||||
}
|
||||
.drag-zone-active.drag-zone-hover .drag-zone-marker,
|
||||
.drag-zone-active + #new-branch-dz.drag-zone-hover .drag-zone-marker {
|
||||
@apply bg-green-300 dark:bg-green-460;
|
||||
.drag-zone-active.drag-zone-hover .drag-zone-marker {
|
||||
@apply border-green-500 bg-green-300 dark:bg-green-460;
|
||||
}
|
||||
.drag-zone-hover {
|
||||
@apply border-green-500;
|
||||
}
|
||||
.drag-zone-active .no-changes {
|
||||
.drag-zone-active .no-changes,
|
||||
.drag-zone-active .call-to-action {
|
||||
@apply hidden;
|
||||
}
|
||||
.drag-zone-active .drag-zone-marker {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
/* drag & drop ugly stuff */
|
||||
#new-branch-dz.new-branch-active {
|
||||
@apply visible flex;
|
||||
}
|
||||
.drag-zone-active + #new-branch-dz .call-to-action {
|
||||
@apply hidden;
|
||||
}
|
||||
.drag-zone-active + #new-branch-dz .drag-zone-marker {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
.drag-zone-hover + #new-branch-dz .call-to-action {
|
||||
@apply hidden;
|
||||
}
|
||||
.drag-zone-hover + #new-branch-dz .drag-zone-marker {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
#new-branch-dz.drag-zone-hover .call-to-action {
|
||||
@apply hidden;
|
||||
}
|
||||
#new-branch-dz.drag-zone-hover .drag-zone-marker {
|
||||
@apply block bg-green-300 dark:bg-green-460;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
import type { VirtualBranchOperations } from './vbranches';
|
||||
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/userSettings';
|
||||
import { setContext } from 'svelte';
|
||||
import { dzHighlight } from './dropZone';
|
||||
|
||||
export let projectId: string;
|
||||
export let projectPath: string;
|
||||
@ -19,7 +20,7 @@
|
||||
let priorPosition = 0;
|
||||
let dropPosition = 0;
|
||||
|
||||
const hoverClass = 'drag-zone-hover';
|
||||
const dzType = 'text/branch';
|
||||
|
||||
function handleEmpty() {
|
||||
const emptyIndex = branches.findIndex((item) => !item.files || item.files.length == 0);
|
||||
@ -34,23 +35,8 @@
|
||||
bind:this={dropZone}
|
||||
id="branch-lanes"
|
||||
class="flex max-w-full flex-shrink flex-grow snap-x items-start overflow-x-auto overflow-y-hidden bg-light-200 px-2 dark:bg-dark-1000"
|
||||
on:dragenter={(e) => {
|
||||
if (!e.dataTransfer?.types.includes('text/branch')) {
|
||||
return;
|
||||
}
|
||||
dropZone.classList.add(hoverClass);
|
||||
}}
|
||||
on:dragend={(e) => {
|
||||
if (!e.dataTransfer?.types.includes('text/branch')) {
|
||||
return;
|
||||
}
|
||||
dropZone.classList.remove(hoverClass);
|
||||
}}
|
||||
use:dzHighlight={{ type: dzType }}
|
||||
on:dragover={(e) => {
|
||||
if (!e.dataTransfer?.types.includes('text/branch')) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault(); // Only when text/branch
|
||||
const children = [...e.currentTarget.children];
|
||||
dropPosition = 0;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
@ -69,7 +55,6 @@
|
||||
}
|
||||
}}
|
||||
on:drop={(e) => {
|
||||
dropZone.classList.remove(hoverClass);
|
||||
if (priorPosition != dropPosition) {
|
||||
const el = branches.splice(priorPosition, 1);
|
||||
branches.splice(dropPosition, 0, ...el);
|
||||
@ -85,7 +70,7 @@
|
||||
<Lane
|
||||
on:dragstart={(e) => {
|
||||
if (!e.dataTransfer) return;
|
||||
e.dataTransfer.setData('text/branch', id);
|
||||
e.dataTransfer.setData(dzType, id);
|
||||
dragged = e.currentTarget;
|
||||
priorPosition = Array.from(dropZone.children).indexOf(dragged);
|
||||
}}
|
||||
|
@ -1,7 +1,3 @@
|
||||
<script lang="ts" context="module">
|
||||
const zones = new Set<HTMLDivElement>();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import type { Commit, File, Hunk } from '$lib/api/ipc/vbranches';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
@ -15,6 +11,7 @@
|
||||
import type { VirtualBranchOperations } from './vbranches';
|
||||
import PopupMenu from '../../../lib/components/PopupMenu/PopupMenu.svelte';
|
||||
import PopupMenuItem from '../../../lib/components/PopupMenu/PopupMenuItem.svelte';
|
||||
import { dzHighlight } from './dropZone';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
empty: never;
|
||||
@ -40,15 +37,9 @@
|
||||
let isPushing = false;
|
||||
let popupMenu: PopupMenu;
|
||||
let meatballButton: HTMLButtonElement;
|
||||
let dropZone: HTMLDivElement;
|
||||
|
||||
const hoverClass = 'drag-zone-hover';
|
||||
const hunkType = 'text/hunk';
|
||||
|
||||
onMount(() => {
|
||||
zones.add(dropZone);
|
||||
return () => zones.delete(dropZone);
|
||||
});
|
||||
const dzType = 'text/hunk';
|
||||
|
||||
function updateBranchOwnership() {
|
||||
const ownership = files
|
||||
@ -134,37 +125,17 @@
|
||||
|
||||
<div
|
||||
draggable="true"
|
||||
bind:this={dropZone}
|
||||
class="flex max-h-full min-w-[24rem] max-w-[120ch] shrink-0 snap-center flex-col overflow-y-auto bg-light-200 py-2 px-3 transition-width dark:bg-dark-1000 dark:text-dark-100"
|
||||
class:w-full={maximized}
|
||||
class:w-96={!maximized}
|
||||
class="flex max-h-full min-w-[24rem] max-w-[120ch] shrink-0 cursor-grabbing snap-center flex-col overflow-y-auto bg-light-200 py-2 px-3 transition-width dark:bg-dark-1000 dark:text-dark-100"
|
||||
use:dzHighlight={{ type: dzType, hover: hoverClass, active: 'drag-zone-active' }}
|
||||
on:dragstart
|
||||
on:dragenter={(e) => {
|
||||
if (!e.dataTransfer?.types.includes(hunkType)) {
|
||||
return;
|
||||
}
|
||||
dropZone.classList.add(hoverClass);
|
||||
}}
|
||||
on:dragleave|stopPropagation={(e) => {
|
||||
if (!e.dataTransfer?.types.includes(hunkType)) {
|
||||
return;
|
||||
}
|
||||
if (!isChildOf(e.target, dropZone)) {
|
||||
dropZone.classList.remove(hoverClass);
|
||||
}
|
||||
}}
|
||||
on:dragover|stopPropagation={(e) => {
|
||||
if (e.dataTransfer?.types.includes(hunkType)) e.preventDefault();
|
||||
}}
|
||||
on:dragend={(e) => {
|
||||
dropZone.classList.remove(hoverClass);
|
||||
}}
|
||||
on:dragend
|
||||
on:drop|stopPropagation={(e) => {
|
||||
if (!e.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
dropZone.classList.remove(hoverClass);
|
||||
const data = e.dataTransfer.getData(hunkType);
|
||||
const data = e.dataTransfer.getData(dzType);
|
||||
const ownership = files
|
||||
.map((file) => file.id + ':' + file.hunks.map((hunk) => hunk.id).join(','))
|
||||
.join('\n');
|
||||
@ -251,23 +222,12 @@
|
||||
filepath={file.path}
|
||||
expanded={file.expanded}
|
||||
hunks={file.hunks}
|
||||
{dzType}
|
||||
{maximized}
|
||||
on:update={(e) => {
|
||||
handleFileUpdate(file.id, e.detail);
|
||||
}}
|
||||
on:expanded={(e) => {
|
||||
setExpandedWithCache(file, e.detail);
|
||||
expandFromCache();
|
||||
}}
|
||||
on:drag={(e) => {
|
||||
zones.forEach((zone) => {
|
||||
if (zone != dropZone) {
|
||||
e.detail
|
||||
? zone.classList.add('drag-zone-active')
|
||||
: zone.classList.remove('drag-zone-active');
|
||||
}
|
||||
});
|
||||
}}
|
||||
{projectPath}
|
||||
/>
|
||||
{/each}
|
||||
|
@ -10,6 +10,7 @@
|
||||
import PopupMenuItem from '$lib/components/PopupMenu/PopupMenuItem.svelte';
|
||||
import { SETTINGS_CONTEXT, type SettingsStore } from '$lib/userSettings';
|
||||
import { getContext } from 'svelte';
|
||||
import { dzTrigger } from './dropZone';
|
||||
|
||||
const userSettings = getContext<SettingsStore>(SETTINGS_CONTEXT);
|
||||
|
||||
@ -18,11 +19,10 @@
|
||||
export let filepath: string;
|
||||
export let hunks: Hunk[];
|
||||
export let maximized: boolean;
|
||||
export let dzType: string;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
expanded: boolean;
|
||||
update: Hunk[];
|
||||
drag: boolean;
|
||||
}>();
|
||||
export let expanded: boolean | undefined;
|
||||
|
||||
@ -49,19 +49,16 @@
|
||||
function getFirstLineNumber(diff: string): number {
|
||||
return parseInt(diff.split('\n')[0].split('@@')[1].trim().split(' ')[0].split(',')[0].slice(1));
|
||||
}
|
||||
|
||||
function getAllHunksOwnership(): string {
|
||||
return id + ':' + hunks.map((h) => h.id).join(',');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
draggable="true"
|
||||
on:dragstart|stopPropagation={(e) => {
|
||||
if (!e.dataTransfer) return;
|
||||
e.dataTransfer.setData('text/hunk', id + ':' + hunks.map((h) => h.id).join(','));
|
||||
dispatch('drag', true);
|
||||
return true;
|
||||
}}
|
||||
on:dragend|stopPropagation={(e) => {
|
||||
dispatch('drag', false);
|
||||
}}
|
||||
use:dzTrigger={{ type: dzType }}
|
||||
on:dragstart={(e) => e.dataTransfer?.setData('text/hunk', getAllHunksOwnership())}
|
||||
class="changed-file flex w-full flex-col justify-center gap-2 rounded-lg border border-light-300 bg-light-50 text-light-900 dark:border-dark-400 dark:bg-dark-700 dark:text-light-300"
|
||||
>
|
||||
<div class="items-cente flex px-2 pt-2">
|
||||
@ -89,15 +86,8 @@
|
||||
{#each hunks || [] as hunk (hunk.id)}
|
||||
<div
|
||||
draggable="true"
|
||||
on:dragstart|stopPropagation={(e) => {
|
||||
if (!e.dataTransfer) return;
|
||||
e.dataTransfer.setData('text/hunk', id + ':' + hunk.id);
|
||||
dispatch('drag', true);
|
||||
return false;
|
||||
}}
|
||||
on:dragend|stopPropagation={(e) => {
|
||||
dispatch('drag', false);
|
||||
}}
|
||||
use:dzTrigger={{ type: dzType }}
|
||||
on:dragstart={(e) => e.dataTransfer?.setData('text/hunk', id + ':' + hunk.id)}
|
||||
on:contextmenu|preventDefault={(e) => popupMenu.openByMouse(e, hunk)}
|
||||
class="changed-hunk flex w-full flex-col rounded-lg border border-light-200 bg-white dark:border-dark-400 dark:bg-dark-900"
|
||||
>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Branch } from '$lib/api/ipc/vbranches';
|
||||
import { Button } from '$lib/components';
|
||||
import { dzHighlight } from './dropZone';
|
||||
import type { VirtualBranchOperations } from './vbranches';
|
||||
|
||||
export let virtualBranches: VirtualBranchOperations;
|
||||
@ -23,20 +24,11 @@
|
||||
id="new-branch-dz"
|
||||
class="h-42 ml-4 mt-16 flex w-[22.5rem] shrink-0 justify-center text-center text-light-800 dark:text-dark-100"
|
||||
bind:this={dropZone}
|
||||
on:dragover|stopPropagation={(e) => {
|
||||
if (e.dataTransfer?.types.includes('text/hunk')) e.preventDefault();
|
||||
dropZone.classList.add('drag-zone-hover');
|
||||
}}
|
||||
on:dragleave|stopPropagation={(e) => {
|
||||
if (!isChildOf(e.target, dropZone)) {
|
||||
dropZone.classList.remove('drag-zone-hover');
|
||||
}
|
||||
}}
|
||||
use:dzHighlight={{ type: 'text/hunk', hover: 'drag-zone-hover', active: 'drag-zone-active' }}
|
||||
on:drop|stopPropagation={(e) => {
|
||||
if (!e.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
dropZone.classList.remove('drag-zone-hover');
|
||||
const ownership = e.dataTransfer.getData('text/hunk');
|
||||
virtualBranches.createBranch({ ownership });
|
||||
}}
|
||||
|
@ -200,7 +200,7 @@
|
||||
</div>
|
||||
<div>{branch.ahead}/{branch.behind}</div>
|
||||
{#if !branch.mergeable}
|
||||
<div class="text-red-500 font-bold" title="Can't be merged">!</div>
|
||||
<div class="font-bold text-red-500" title="Can't be merged">!</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if branch.lastCommitTs > 0}
|
||||
|
123
src/routes/repo/[projectId]/dropZone.ts
Normal file
123
src/routes/repo/[projectId]/dropZone.ts
Normal file
@ -0,0 +1,123 @@
|
||||
const zoneMap = new Map<string, Set<HTMLElement>>();
|
||||
|
||||
export interface DzOptions {
|
||||
type: string;
|
||||
hover: string;
|
||||
active: string;
|
||||
}
|
||||
|
||||
const defaultOptions: DzOptions = {
|
||||
hover: 'drag-zone-hover',
|
||||
active: 'drag-zone-active',
|
||||
type: 'default'
|
||||
};
|
||||
|
||||
function inactivateZones(zones: Set<HTMLElement>, cssClass: string) {
|
||||
zones?.forEach((zone) => {
|
||||
zone.classList.remove(cssClass);
|
||||
});
|
||||
}
|
||||
|
||||
function activateZones(zones: Set<HTMLElement>, activeZone: HTMLElement, cssClass: string) {
|
||||
zones?.forEach((zone) => {
|
||||
if (zone !== activeZone && !isChildOf(activeZone, zone)) {
|
||||
zone.classList.add(cssClass);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getZones(type: string): Set<HTMLElement> {
|
||||
let zones = zoneMap.get(type);
|
||||
if (!zones) {
|
||||
zones = new Set([]);
|
||||
zoneMap.set(type, zones);
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
function isChildOf(child: any, parent: HTMLElement): boolean {
|
||||
if (parent === child) return false;
|
||||
if (!child.parentElement) return false;
|
||||
if (child.parentElement == parent) return true;
|
||||
return isChildOf(child.parentElement, parent);
|
||||
}
|
||||
|
||||
export function dzTrigger(node: HTMLElement, opts: Partial<DzOptions> | undefined) {
|
||||
const options = { ...defaultOptions, ...opts };
|
||||
const zones = getZones(options.type);
|
||||
|
||||
function handleDragStart(e: DragEvent) {
|
||||
activateZones(zones, node, options.active);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
node.addEventListener('dragstart', handleDragStart);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('dragstart', handleDragStart);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function dzHighlight(node: HTMLElement, opts: Partial<DzOptions> | undefined) {
|
||||
const options = { ...defaultOptions, ...opts };
|
||||
const zones = getZones(options.type);
|
||||
zones.add(node);
|
||||
|
||||
function handleDragEnter(e: DragEvent) {
|
||||
if (!e.dataTransfer?.types.includes(options.type)) {
|
||||
return;
|
||||
}
|
||||
node.classList.add(options.hover);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleDragLeave(e: DragEvent) {
|
||||
if (!e.dataTransfer?.types.includes(options.type)) {
|
||||
return;
|
||||
}
|
||||
if (!isChildOf(e.target, node)) {
|
||||
node.classList.remove(options.hover);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleDragEnd(e: DragEvent) {
|
||||
node.classList.remove(options.hover);
|
||||
inactivateZones(zones, options.active);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
if (!e.dataTransfer?.types.includes(options.type)) {
|
||||
return;
|
||||
}
|
||||
node.classList.remove(options.hover);
|
||||
inactivateZones(zones, options.active);
|
||||
}
|
||||
|
||||
function handleDragOver(e: DragEvent) {
|
||||
if (!e.dataTransfer?.types.includes(options.type)) {
|
||||
e.stopImmediatePropagation(); // Stops event from reaching `on:dragover` on the element
|
||||
}
|
||||
if (e.dataTransfer?.types.includes(options.type)) e.preventDefault();
|
||||
}
|
||||
|
||||
node.addEventListener('dragend', handleDragEnd);
|
||||
node.addEventListener('dragenter', handleDragEnter);
|
||||
node.addEventListener('dragleave', handleDragLeave);
|
||||
node.addEventListener('dragover', handleDragOver);
|
||||
node.addEventListener('drop', handleDrop);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('dragend', handleDragEnd);
|
||||
node.removeEventListener('dragenter', handleDragEnter);
|
||||
node.removeEventListener('dragleave', handleDragLeave);
|
||||
node.removeEventListener('dragover', handleDragOver);
|
||||
node.removeEventListener('drop', handleDrop);
|
||||
zones?.delete(node);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user